Skip to content

Reject present-but-empty name constraint subtrees - #15560

Open
avalyset wants to merge 1 commit into
pyca:mainfrom
avalyset:fix/rust-empty-permitted-subtrees
Open

Reject present-but-empty name constraint subtrees#15560
avalyset wants to merge 1 commit into
pyca:mainfrom
avalyset:fix/rust-empty-permitted-subtrees

Conversation

@avalyset

Copy link
Copy Markdown

Reported privately as GHSA-fw9w-89pp-fpmg. The advisory is not public, so there is nothing to link. The response there was that this is a regular bug rather than a security issue, with a request to open a public PR instead; this is that PR. We had offered no severity assessment either way.

The rule is already enforced in the Python layer

src/cryptography/x509/extensions.py:1344:

if not permitted_subtrees:
    raise ValueError(
        "permitted_subtrees must be a non-empty list or None"
    )

That check came from #6982, "Possible bug: empty sequence in NameConstraints" (opened and closed 2022-03-19), where the conclusion was explicit:

Yes, this is a bug, we should be rejecting non-none values that are less than 0 [sic] because:

GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree

The Rust path validator did not enforce it

policy/extension.rs rejected only when both subtree fields were empty, so an empty permittedSubtrees combined with a non-empty excludedSubtrees passed that check. In lib.rs:258-274 the permit flag then keeps its default:

let mut permit = true;
if let Some(permitted_subtrees) = &constraints.permitted_subtrees {
    for p in permitted_subtrees.clone() {
        ...
    }
}

With an empty sequence the loop body never runs, so permit stays true and every SAN is accepted by the permitted side.

Basis

RFC 5280 6.1.4 (g)(1) sets permitted_subtrees to the intersection of its previous value and the value in the extension; the intersection with an empty set is empty. RFC 5280 6.1.3 (b) requires the name to lie within permitted_subtrees, so with an empty one no name qualifies and every certificate below that CA should be rejected. It was accepting all of them instead.

Reaching this requires a signed CA certificate carrying such an extension, and RFC 5280 4.2.1.10 says conforming CAs must not issue one.

The change

GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) constrains both fields, so this rejects a present-but-empty permittedSubtrees and a present-but-empty excludedSubtrees. Only the first has any consequence; an empty excludedSubtrees excludes nothing, which is harmless. But it is the same ASN.1 requirement on the same type, and enforcing it for one field and not the other seemed arbitrary. Happy to drop the second check if you would rather keep the diff to the field that matters.

The existing both-empty check is kept. I checked whether it became redundant: it does not. It is the only one that covers a NameConstraints where neither field is present, which is_some_and by construction never matches.

Behaviour

The probe from the advisory, through the public API. The CA's NameConstraints has to be built as raw DER, because the Python API refuses to construct this shape at all, which is itself part of the point.

Before, against 50.0.1 from PyPI:

Python API rejects empty permitted_subtrees -> ValueError: permitted_subtrees must be a non-empty list or None
NameConstraints DER used: 30 13 a0 00 a1 0f 30 0d 82 0b 62 61 64 2e 65 78 61 6d 70 6c 65

  permitted=EMPTY,        excluded=[bad.example] -> ACCEPTED
  permitted=[ok.example], excluded=[bad.example] -> rejected: validation failed: candidates exhausted: no permitted name constraints matched SAN

After, with this branch:

  permitted=EMPTY,        excluded=[bad.example] -> rejected: validation failed: candidates exhausted: nameConstraints permittedSubtrees must not be empty
  permitted=[ok.example], excluded=[bad.example] -> rejected: validation failed: candidates exhausted: no permitted name constraints matched SAN

The second line is the control, and it is unchanged.

Tests

Three tests in policy::extension::tests, using raw DER because an empty sequence cannot be built through the writing API: empty permittedSubtrees with non-empty excludedSubtrees, empty excludedSubtrees with non-empty permittedSubtrees, and a control with both non-empty that must still be accepted.

Verified they fail without the fix: reverting the check and keeping the tests turns both rejection tests red while the control stays green.

Checks

  • cargo test --all green across the workspace, including the 43 tests in cryptography-x509-verification
  • cargo fmt --all -- --check clean
  • cargo clippy -p cryptography-x509-verification --all-targets: 8 warnings, the same 8 as on main, none on the changed lines
  • pytest tests/: 4490 passed, 199 skipped (wycheproof_root and x509_limbo_root not available locally)

GeneralSubtrees is SEQUENCE SIZE (1..MAX), so a subtree field that is
present must not be empty. An empty permittedSubtrees was accepted and
then had no effect, because the loop over the subtrees never runs, so
every name passed the permitted side instead of none.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant