CertificateSigningRequestParams::from_der can parse the wrong key type
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 505
- Forks
- 157
- Avg merge
- 39m
- Merged PRs (30d)
- 1
Description
Version: rcgen 0.14.9 (default-features = false, features = ["aws_lc_rs", "x509-parser"];
Summary
When parsing a CSR, rcgen keeps only the raw public key bits and derives the key's
AlgorithmIdentifier from the CSR's signature algorithm. For ECDSA that algorithm names a
curve, so a CSR whose signature hash does not match its key's curve comes back out as a different
key. Signing it produces a certificate whose subjectPublicKeyInfo claims one curve over a point
from another - resulting in an invalid public key.
Reproducer
Cargo.toml:
[dependencies]
rcgen = { version = "0.14.9", default-features = false, features = ["aws_lc_rs", "x509-parser"] }
The CSR below is a plain OpenSSL request — a P-256 key, self-signed with ecdsa-with-SHA384. Such a CSR is not recommended, but also not forbidden as per RFC 5480 §4.
openssl ecparam -name prime256v1 -genkey -noout -out p256.key
openssl req -new -key p256.key -sha384 -subj /CN=example.com -outform DER -out p256.csr
use rcgen::{CertificateSigningRequestParams, PublicKeyData};
const CSR_HEX: &str = "\
3081fa3081a102010030163114301206035504030c0b6578616d706c652e636f6d3059301306\
072a8648ce3d020106082a8648ce3d03010703420004e96cb902a24cd0a748b038003facde9f\
d674918c5e10795106cdf03c3535d6d5542d5816ad97094e8bf459f1c3a647caef2f5efa4b36\
4c74e49770536c40008fa029302706092a864886f70d01090e311a301830160603551d11040f\
300d820b6578616d706c652e636f6d300a06082a8648ce3d0403030348003045022036d3561c\
ed3827f8fc4224d21d3bb2e36042316ba2fc6deb4cbbf9be130d1f82022100baccc313da1c38\
46a030f517bdae54b6ab2565bb7b7912b5d1bb4f6b4b1110ac";
/// `1.2.840.10045.3.1.7` (prime256v1), as encoded inside an AlgorithmIdentifier.
const PRIME256V1: &[u8] = &[0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07];
/// `1.3.132.0.34` (secp384r1).
const SECP384R1: &[u8] = &[0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22];
fn main() {
let der: Vec<u8> = (0..CSR_HEX.len() / 2)
.map(|i| u8::from_str_radix(&CSR_HEX[i * 2..i * 2 + 2], 16).unwrap())
.collect();
// Parses and verifies the self-signature: this is a well-formed request.
let csr = CertificateSigningRequestParams::from_der(&der.clone().into()).unwrap();
let spki = csr.public_key.subject_public_key_info();
assert!(contains(&der, PRIME256V1), "the CSR encodes prime256v1");
assert!(contains(&spki, SECP384R1), "but rcgen hands back secp384r1");
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
haystack.windows(needle.len()).any(|w| w == needle)
}
Both assertions hold. The SPKI rcgen produces is:
3056 3010 0607 2a8648ce3d0201 -- id-ecPublicKey
0605 2b81040022 -- secp384r1
0342 0004 e96cb9... -- a 65-byte (P-256) point
Feeding that through CertificateParams::signed_by yields a certificate OpenSSL rejects:
$ openssl x509 -in leaf.pem -noout -text
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Unable to load Public Key
error:03000072:digital envelope routines:X509_PUBKEY_get0:decode error
Expected behaviour
The certificate's subjectPublicKeyInfo should be the one the CSR carried. Failing that, from_der
should return an error rather than substitute a different key.
Where it goes wrong
src/csr.rs:124—let alg = SignatureAlgorithm::from_oid(&alg_oid)?;wherealg_oidis
csr.signature_algorithm.algorithm.src/csr.rs:131—let raw = info.subject_pki.subject_public_key.data.to_vec();keeps only the
BIT STRING contents.info.subject_pki's ownAlgorithmIdentifieris dropped.src/key_pair.rs:779—serialize_public_key_derrebuilds the SPKI as
SEQUENCE { algorithm().write_oids_sign_alg(), BIT STRING der_bytes() }, and every ECDSA
SignatureAlgorithmhasoids_sign_alg: &[EC_PUBLIC_KEY, <curve>], e.g.
PKCS_ECDSA_P384_SHA384→EC_SECP_384_R1.src/certificate.rs:481writes the leaf's SPKI from that reconstruction, and
src/certificate.rs:441derives the subjectKeyIdentifier from it too — so the SKI is a hash
of a key that does not exist.
The documentation states the invariant that the code does not check:
src/csr.rs:22—PublicKey::algorithm: "The algorithm used to generate the public key and
sign the CSR." These are two different things in a CSR, and only the second is read.src/key_pair.rs:776—PublicKeyData::algorithm: "The algorithm used by the key pair." The
CSR impl returns the signature algorithm instead.
SignatureAlgorithm conflates "signature algorithm" with "key algorithm", which is coherent when
generating a KeyPair (there the value does pick a curve) but not when parsing a request, where the
two are independent inputs.
Scope
ECDSA only, and any mismatched pairing, in either direction:
| CSR subject key | CSR signature algorithm | SPKI rcgen emits |
|---|---|---|
| P-256 | ecdsa-with-SHA384 |
secp384r1 |
| P-256 | ecdsa-with-SHA512 |
secp521r1 |
| P-384 | ecdsa-with-SHA256 |
prime256v1 |
RSA is unaffected (every PKCS_RSA_* shares oids_sign_alg: &[RSA_ENCRYPTION]), as is Ed25519.
LLM Disclosure
This bug was found during an automated code review by Claude Opus 5, verified by a human (me).
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/csr.rs at the parsing lines identified in the report, then trace PublicKeyData and SPKI reconstruction in src/key_pair.rs and src/certificate.rs. Reproduce the P-256 CSR signed with ecdsa-with-SHA384 and verify that parsing either preserves the CSR's subjectPublicKeyInfo or returns an error, without producing a mismatched curve or subject key identifier.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cryptography, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100