rustls / rustls/rcgen

CertificateSigningRequestParams::from_der can parse the wrong key type

Open
#448 2 comments 1 reaction 0 assignees View on GitHub

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:124let alg = SignatureAlgorithm::from_oid(&alg_oid)?; where alg_oid is
    csr.signature_algorithm.algorithm.
  • src/csr.rs:131let raw = info.subject_pki.subject_public_key.data.to_vec(); keeps only the
    BIT STRING contents. info.subject_pki's own AlgorithmIdentifier is dropped.
  • src/key_pair.rs:779serialize_public_key_der rebuilds the SPKI as
    SEQUENCE { algorithm().write_oids_sign_alg(), BIT STRING der_bytes() }, and every ECDSA
    SignatureAlgorithm has oids_sign_alg: &[EC_PUBLIC_KEY, <curve>], e.g.
    PKCS_ECDSA_P384_SHA384EC_SECP_384_R1.
  • src/certificate.rs:481 writes the leaf's SPKI from that reconstruction, and
    src/certificate.rs:441 derives 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:22PublicKey::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:776PublicKeyData::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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.