Skip to content

const-oid: reject non-minimal arc encodings in BER decoder - #2441

Open
yuxi-liu-wired wants to merge 2 commits into
RustCrypto:masterfrom
yuxi-liu-wired:fix/oid-reject-non-minimal-arc
Open

yuxi-liu-wired wants to merge 2 commits into
RustCrypto:masterfrom
yuxi-liu-wired:fix/oid-reject-non-minimal-arc

Conversation

@yuxi-liu-wired

Copy link
Copy Markdown
Contributor

X.690 8.19.2 requires each subidentifier to be encoded in the fewest possible octets, so the leading octet of an arc is never 0x80. Arcs::try_next, which ObjectIdentifier::from_bytes and therefore the der decoder use, accepted that padding: 2A 80 86 48 decoded to 1.2.840, the same as the canonical 2A 86 48.

ObjectIdentifier stores and compares the original bytes, so the two encodings give OIDs that print the same but compare unequal:

let padded = ObjectIdentifier::from_bytes(&[0x2A, 0x80, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01])?;
assert_eq!(padded.to_string(), "1.2.840.113549.1.1.1");
assert_ne!(padded, rsaEncryption);  // on master

DER re-encoding keeps the padding too, so a certificate with a padded OID decodes and re-encodes without error. pyca/cryptography and OpenSSL both reject the padded form.

Fix: return Error::Base128 when the first octet of an arc is 0x80, the same error that already covers a truncated arc. Arcs that only contain 0x80 after their first octet (e.g. 81 80 00) are unaffected.

from_bytes_reject_non_minimal_arc in tests/oid.rs covers padding on the first and on later arcs, on single- and multi-octet arcs, and checks that valid 0x80 octets still decode. It fails on master. cargo hack test --feature-powerset passes on stable and on 1.85 (MSRV), the thumbv7em-none-eabi powerset build succeeds, and cargo fmt --check and cargo +1.90.0 clippy --all-features --tests are clean.

Found by differential fuzzing of x509-cert against pyca/cryptography's rust-asn1. This PR was produced by AI agents (Claude), from the fuzzing and the fix through to this description.

X.690 8.19.2 requires each subidentifier to be encoded in the fewest
possible octets, so the leading octet of an arc is never 0x80.
`Arcs::try_next` (used by `ObjectIdentifier::from_bytes` and so by the
`der` decoder) skipped such padding, so `2A 80 86 48` decoded to
`1.2.840` alongside the canonical `2A 86 48`.

Because `ObjectIdentifier` stores and compares the original bytes, the
two decode to OIDs that print the same but compare unequal: a padded
`rsaEncryption` displays as `1.2.840.113549.1.1.1` yet is `!=` the
`rsaEncryption` constant, and DER re-encoding keeps the padding.
pyca/cryptography and OpenSSL reject the padded form.

Reject a leading 0x80 octet with `Error::Base128`. Adds a regression
test.

Found by differential fuzzing of x509-cert against pyca/cryptography's
rust-asn1.
Comment thread const-oid/tests/oid.rs Outdated
Comment on lines +156 to +165

// Padding in front of a single-octet arc: `1.2.0` and `1.2.5`.
assert_eq!(
ObjectIdentifier::from_bytes(&[0x2A, 0x80, 0x00]),
Err(Error::Base128),
);
assert_eq!(
ObjectIdentifier::from_bytes(&[0x2A, 0x80, 0x80, 0x05]),
Err(Error::Base128),
);

@dishmaker dishmaker Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think one case is enough. Is it testing/covering some more lines than the 840 test?

Suggested change
// Padding in front of a single-octet arc: `1.2.0` and `1.2.5`.
assert_eq!(
ObjectIdentifier::from_bytes(&[0x2A, 0x80, 0x00]),
Err(Error::Base128),
);
assert_eq!(
ObjectIdentifier::from_bytes(&[0x2A, 0x80, 0x80, 0x05]),
Err(Error::Base128),
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread const-oid/tests/oid.rs Outdated
Comment on lines +180 to +183
assert_eq!(
ObjectIdentifier::from_bytes(&[0x2A, 0x00]).unwrap().arc(2),
Some(0),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert_eq!(
ObjectIdentifier::from_bytes(&[0x2A, 0x00]).unwrap().arc(2),
Some(0),
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread const-oid/tests/oid.rs Outdated
Comment on lines +167 to +171
// Padding on a later arc.
assert_eq!(
ObjectIdentifier::from_bytes(&[0x2A, 0x86, 0x48, 0x80, 0x01]),
Err(Error::Base128),
);

@dishmaker dishmaker Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe remove this or merge with the first 840 test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@dishmaker

Copy link
Copy Markdown
Contributor

Thanks!

Keep one padded multi-octet arc, one padded single-octet arc and the
non-leading 0x80 case.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants