Skip to content

reject non-zero unused bits in public key bit strings#15064

Merged
alex merged 3 commits into
pyca:mainfrom
dxbjavid:pubkey-bitstring-unused-bits
Jun 29, 2026
Merged

reject non-zero unused bits in public key bit strings#15064
alex merged 3 commits into
pyca:mainfrom
dxbjavid:pubkey-bitstring-unused-bits

Conversation

@dxbjavid

Copy link
Copy Markdown
Contributor

load_der_public_key and the EC private key loader read the subjectPublicKey and EC publicKey BIT STRINGs with as_bytes() but never look at the unused-bits count, so DER that declares a non-zero number of unused bits in those octet-aligned key bit strings is accepted even though it is malformed. parse_spki_for_data already rejects exactly this for the same field, so the two paths disagree on what a valid public key encoding is. This adds the same padding_bits check to both loaders, with regression tests that craft an Ed25519 SPKI and an EC private key carrying a bogus unused-bits count.

@alex

alex commented Jun 18, 2026

Copy link
Copy Markdown
Member

Is there some spec that dictates this behavior? Do other libraries enforce this?

@dxbjavid

Copy link
Copy Markdown
Contributor Author

there isn't a single clause that says "reject a non-zero count" outright. it falls out of the per-algorithm content definitions: for the key types we load the subjectPublicKey always holds a whole number of octets, so the bit length is a multiple of eight and the unused-bits count can only be zero. for the edwards/montgomery keys rfc 8410 carries the raw key bytes, ec carries the SEC1 point octet string (rfc 5480), and the rsa case wraps a der-encoded RSAPublicKey. a non-zero count contradicts that, and x.690 11.2 also requires the trailing unused bits themselves be zero under der.

honestly the strongest argument is internal consistency rather than a survey of other libraries. parse_spki_for_data already rejects exactly this for the same field, so right now load_der_public_key and that helper disagree on whether the same bytes are a valid public key, and the ec private key loader reads the point bit string the same loose way. i haven't done a broad survey across other libraries, so if you'd rather not tighten this at parse time i'm happy to drop it.

@alex alex 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.

Confirmed that BoringSSL does these checks, so we should be good once the tests are fixed up. Sorry for the slow review.

Comment on lines +381 to +383
idx = data.rfind(b"\x03\x21\x00")
assert idx != -1
data[idx + 2] = 0x01

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.

The right way to do these is to add new vectors, not this manipulation approach.

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.

switched both to committed vectors. added asymmetric/Ed25519/ed25519-pub-non-zero-unused-bits.der and asymmetric/EC/ec_private_key_non_zero_unused_bits.der, each a valid key with the unused-bits byte bumped to 1, and the tests just load and expect ValueError now. dropped the generate-and-mangle loops.

Comment on lines +401 to +402
data = bytearray(der)
data[idx + 2] = 0x01

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.

same

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, this one now loads the EC vector too.

@@ -0,0 +1,4 @@
0w �2ST���

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.

please document in test-vectors.rst

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, documented both vectors in test-vectors.rst alongside the EC and Ed25519 entries.

@alex alex 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.

Confirmed that BoringSSL does these checks.

@alex alex merged commit 4f5e6e3 into pyca:main Jun 29, 2026
69 checks passed
@dxbjavid

Copy link
Copy Markdown
Contributor Author

Thank you.

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.

2 participants