Skip to content

Reject CONTEXT-SPECIFIC tag class in serial number parser#249

Open
afldl wants to merge 7 commits into
rusticata:masterfrom
afldl:fix-tag-class-validation
Open

Reject CONTEXT-SPECIFIC tag class in serial number parser#249
afldl wants to merge 7 commits into
rusticata:masterfrom
afldl:fix-tag-class-validation

Conversation

@afldl

@afldl afldl commented Jun 10, 2026

Copy link
Copy Markdown

Problem

parse_serial() checks that the tag number equals Tag::Integer but does not verify the tag class. A CONTEXT-SPECIFIC [2] tag byte (0x82) has the same tag number as UNIVERSAL INTEGER (0x02) but a different class per X.690 §8.1.2. This makes the parser accept certificates with mismatched tag classes.

13 of 14 tested X.509 implementations reject this mutation. Only x509-parser and mbedTLS accept. See #248.

Fix

Check any.class() == Class::Universal before comparing the tag number.

     let (rem, any) = Any::parse_der(input).map_err(|_| X509Error::InvalidSerial)?;
+    if any.class() != Class::Universal {
+        return Err(Err::Error(X509Error::InvalidSerial));
+    }
     any.tag()
         .assert_eq(Tag::Integer)

Test

Single-byte PoC: offset 0x0d of a standard RSA-2048 self-signed cert, tag byte changed from 0x02 to 0x82.

Test added: test_serial_rejects_context_specific_tag. All 66 existing tests pass.

@cpu

cpu commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

You can ignore the clippy & audit failings in CI. I believe both are going to be fixed by #223 Please just keep the commits related to the issue/fix.

@chifflier chifflier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hi,
Thank you for the report and PR.
The parser is indeed a bit permissive, and the proposed fix is correct.
I just suggested a few changes - can you update the PR?

GeneralName::DNSName(ref s) | GeneralName::RFC822Name(ref s)
if !s.as_bytes().iter().all(u8::is_ascii) =>
{
l.warn(&format!("Invalid charset in 'SAN' entry '{s}'"));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This change seems unrelated to the problem (and cargo fmt does not give the same result, FYI). Please remove this from the patch

Comment thread src/validate/structure.rs
Comment on lines +143 to +146
GeneralName::DNSName(ref s) | GeneralName::RFC822Name(ref s)
if !s.as_bytes().iter().all(u8::is_ascii) =>
{
l.warn(&format!("Invalid charset in 'SAN' entry '{s}'"));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ditto (unrelated)

Comment thread src/x509.rs
Comment on lines +616 to +618
if any.class() != Class::Universal {
return Err(Err::Error(X509Error::InvalidSerial));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To stay consistent with the next test, this should be changed to use .class().assert_eq() (see suggestion)

Suggested change
if any.class() != Class::Universal {
return Err(Err::Error(X509Error::InvalidSerial));
}
any.class()
.assert_eq(Class::Universal)
.map_err(|_| X509Error::InvalidSerial)?;

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.

ASN.1 tag class not validated — UNIVERSAL vs CONTEXT-SPECIFIC confusion (X.690 §8.1.2)

3 participants