RustCrypto / RustCrypto/formats

x509-cert - GeneralSubtree - distinguish absent minimum from explicitly encoded DEFAULT 0

Open
#2,428 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
338
Forks
188
Avg merge
4d 6h
Merged PRs (30d)
15

Description

Issue

During the development of an X.509 extension validator using x509-cert, I encountered the following behavior:

The current NameConstraints representation does not appear to distinguish an absent minimum from an explicitly encoded DEFAULT 0.

Initial analysis

x509-cert currently represents GeneralSubtree.minimum as:

#[asn1(
    context_specific = "0",
    tag_mode = "IMPLICIT",
    default = "Default::default"
)]
pub minimum: u32,

This may create a problem for strict validation of nameConstraints.

GeneralSubtree is defined by RFC 5280 as:

GeneralSubtree ::= SEQUENCE {
     base                    GeneralName,
     minimum         [0]     BaseDistance DEFAULT 0,
     maximum         [1]     BaseDistance OPTIONAL }

For the RFC 5280 profile, minimum must be zero and maximum must be absent. Some profiles/specifications, such as the CA/B Forum Baseline Requirements, state the corresponding encoding requirement more directly: minimum MUST NOT be present and maximum MUST NOT be present, as specified in section 7.1.2.10.8:

https://github.com/cabforum/servercert/blob/main/docs/BR.md#712108-ca-certificate-name-constraints

The issue is that an explicitly encoded minimum = 0 currently appears to be accepted and normalized to the same Rust value as an omitted minimum.

NameConstraints examples

The following two cases both pass:

#[cfg(test)]
use x509_cert::{der::Decode, ext::pkix::constraints::name::NameConstraints};

#[test]
fn name_constraints_omitted_minimum() {
    // NameConstraints ::= SEQUENCE {
    //     permittedSubtrees [0] IMPLICIT GeneralSubtrees OPTIONAL
    // }
    //
    // GeneralSubtree ::= SEQUENCE {
    //     base     dNSName "test.test"
    //     minimum  omitted
    // }
    let der = [
        0x30, 0x0f, // SEQUENCE
        0xa0, 0x0d, //   [0] permittedSubtrees
        0x30, 0x0b, //     SEQUENCE (GeneralSubtree)
        0x82, 0x09, //       [2] dNSName
        b't', b'e', b's', b't', b'.', b't', b'e', b's', b't',
    ];

    let nc = NameConstraints::from_der(&der).unwrap();
    let subtree = &nc.permitted_subtrees.as_ref().unwrap()[0];

    assert_eq!(subtree.minimum, 0);
    assert_eq!(subtree.maximum, None);
}

#[test]
fn name_constraints_explicit_default_minimum() {
    // Same NameConstraints as above, except GeneralSubtree contains:
    //
    //     minimum [0] = 0
    //
    // explicitly.
    let der = [
        0x30, 0x12, // SEQUENCE
        0xa0, 0x10, //   [0] permittedSubtrees
        0x30, 0x0e, //     SEQUENCE (GeneralSubtree)
        0x82, 0x09, //       [2] dNSName
        b't', b'e', b's', b't', b'.', b't', b'e', b's', b't',
        0x80, 0x01, 0x00, // [0] minimum = 0
    ];

    let nc = NameConstraints::from_der(&der).unwrap();
    let subtree = &nc.permitted_subtrees.as_ref().unwrap()[0];

    assert_eq!(subtree.minimum, 0);
    assert_eq!(subtree.maximum, None);
}

Consequently, it seems that a downstream NameConstraints validator cannot currently distinguish minimum being absent from minimum being explicitly present with value 0.

Is there an intended way for downstream consumers to make this distinction?

Alternatively, since X.690 requires a component equal to its DEFAULT value to be omitted from DER encoding, should the second example instead be rejected by NameConstraints::from_der()?

X.690 reference

https://www.itu.int/myworkspace/t-rec/item?id=14472&lang=en&page=publication

11.5 Set and sequence components with default value
The encoding of a set value or sequence value shall not include an encoding for any component value which is equal to its default value.

Potential upstream cause

If the intended behavior is for NameConstraints::from_der() to reject the explicitly encoded DEFAULT 0, then this may be a potential issue upstream in der_derive, rather than something specific to the GeneralSubtree representation in x509-cert.

The same behavior can be reproduced independently of x509-cert:

use der::{Decode, Sequence};

#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
struct TestDefault {
    #[asn1(
        context_specific = "0",
        tag_mode = "IMPLICIT",
        default = "Default::default"
    )]
    value: u32,
}

#[test]
fn der_accepts_omitted_default_value() {
    let der = [
        0x30, 0x00, // SEQUENCE, value omitted
    ];

    let value = TestDefault::from_der(&der).unwrap();
    assert_eq!(value.value, 0);
}

#[test]
fn der_accepts_non_default_value() {
    let der = [
        0x30, 0x03,
        0x80, 0x01, 0x01, // [0] value = 1
    ];

    let value = TestDefault::from_der(&der).unwrap();
    assert_eq!(value.value, 1);
}

// FAILED
#[test]
fn der_rejects_explicit_default_value() {
    // TestDefault ::= SEQUENCE {
    //     value [0] INTEGER DEFAULT 0
    // }
    //
    // Under X.690 §11.5, explicitly encoding value
    // = 0 does not appear to be a valid DER
    // encoding because the component equals its
    // DEFAULT value.
    let der = [
        0x30, 0x03,       // SEQUENCE
        0x80, 0x01, 0x00, // [0] IMPLICIT INTEGER 0
    ];

    assert!(TestDefault::from_der(&der).is_err());
}

The first two tests pass, while der_rejects_explicit_default_value fails because TestDefault::from_der(&der) returns Ok.

This suggests the behavior may originate in the handling of #[asn1(default = "...")] rather than in the x509-cert GeneralSubtree representation itself.

If Decode::from_der() is intended to enforce DER canonicality, should #[asn1(default = "...")] reject a present component when its decoded value equals the declared default?

Thank you for your attention.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the independent TestDefault DER tests and the #[asn1(default = "Default::default")] handling in der_derive, then compare that behavior with x509-cert's GeneralSubtree and NameConstraints::from_der(). Determine the intended handling of an explicitly encoded default and add regression coverage showing the selected behavior for both examples.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
cryptography
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.