Skip to content

feat(x509): ECDSA CA signers, and two certificate defects they uncovered - #82

Merged
erossignon merged 1 commit into
masterfrom
feat/ecdsa-ca-signer
Aug 22, 2026
Merged

erossignon merged 1 commit into
masterfrom
feat/ecdsa-ca-signer

Conversation

@erossignon

Copy link
Copy Markdown
Member

Adds ECDSA support to CaSigner, and fixes two certificate defects that only became visible once an EC-keyed CA was exercised end to end.

Downstream of node-opcua-pki#50, which needs this released before its CI can pass.

1. ECDSA signers (feat)

An ECDSA signer could be described by CaSignAlgorithm but never adapted: webCryptoFromSigner has to import the signer's public key, and an SPKI import cannot infer a curve it was not given.

Failed to normalize algorithm: passed algorithm can not be converted to
'EcKeyImportParams' because 'namedCurve' is required in 'EcKeyImportParams'.

The ECDSA arm now carries namedCurve, and that is the whole fix.

Worth recording, because it was previously believed otherwise: the r || s to DER re-encoding that X.509 needs is already done by the certificate generator. No signature handling changed. Certificates produced this way verify under openssl verify and report Signature Algorithm: ecdsa-with-SHA256.

Declaring the wrong curve is safe rather than subtly broken: the SPKI bytes state the real curve, so importKey rejects the mismatch instead of yielding a key that signs into the void. There is a test for that.

ecdsaSignatureDerToP1363

sign() is defined to return what SubtleCrypto.sign returns, which for ECDSA is the fixed-width r || s of IEEE P1363. Cloud KMS, AWS KMS and PKCS#11 tokens all return DER instead, so this is the part an integrator gets wrong, and the failure mode is a certificate that simply does not verify. The helper does the conversion, refuses input that is not DER (i.e. calling it twice), and pads coordinates to the curve width rather than the input width.

2. EC certificates could never be verified (fix)

createVerify was handed the signature algorithm identifier. That works for RSA only because OpenSSL resolves sha256WithRSAEncryption to a digest. ECC algorithms have no name in the OID table, so an EC certificate arrived as 1.2.840.10045.4.3.2 and threw Invalid digest.

Every EC certificate and CRL was unverifiable, whether or not its signature was good. The digest is named inside the algorithm either way, so it is now named directly. Unrecognized identifiers still pass through untouched, which leaves RSASSA-PSS — whose digest lives in the parameters — exactly as it was.

3. keyCertSign on CA-issued certificates (fix) — behaviour change

createCertificateFromCsr took its extensions from getAttributes, which describes a self-signed certificate: it grants keyCertSign (historically, because such a certificate signs itself) and omits keyAgreement. Applied to a certificate an issuing CA signs, that told every validator the subject may issue certificates of its own.

before after openssl ca
keyCertSign true false false
keyAgreement false true true

The profile now matches openssl's [usr_cert], which is what makes a natively issued certificate interchangeable with an openssl-issued one. Subordinate CAs are unaffected and still get the CA profile.

This changes the extensions on certificates issued through this primitive. Nothing re-signs existing certificates, and the old ones remain valid; they simply carry a usage bit they should not have.

Verification

  • 279 tests passing, 6 skipped, lint and format clean.
  • New coverage: ECDSA adapter and certificate generation, wrong-curve rejection, ecdsaSignatureDerToP1363 against a real DER signature from node:crypto plus its padding and malformed-input edges, EC self-signed verification (and the negative case, so the fix cannot turn "cannot verify" into "verifies anything"), and the issued-certificate profile.
  • Cross-checked against openssl in the node-opcua-pki branch: EC-issued chains and EC-signed CRLs both verify, and all three CA backends now emit an identical profile.

Notes for release

Suggested version 5.9.0 — one feature, two fixes.

The CaSignAlgorithm ECDSA arm gains a required namedCurve, so { name: "ECDSA", hash } becomes a type error. Any such code failed at runtime already.

Curve coverage

P-256, P-384 and P-521, with SHA-256/384/512. That set is what WebCrypto offers. Of the curves OPC UA names for application instance certificates (OPC 10000-12, 7.8.4.10 to 7.8.4.16), this covers nistP256 and nistP384; brainpoolP256r1, brainpoolP384r1, curve25519 and curve448 would each need a signing path that does not go through WebCrypto.

🤖 Generated with Claude Code

An ECDSA signer could be described but never adapted: CaSignAlgorithm
named no curve, and an SPKI import cannot infer one. The ECDSA arm now
carries namedCurve, and that is the whole fix - the r||s to DER
re-encoding X.509 needs is already done by the certificate generator.
Adds ecdsaSignatureDerToP1363, since KMS signers hand back DER.

Driving an EC-keyed CA end to end found two more. createVerify was given
the signature algorithm identifier, which resolves to a digest for RSA
only, so every EC certificate threw "Invalid digest" and could not be
verified whether or not its signature was good. And
createCertificateFromCsr took its extensions from the self-signed
profile, putting keyCertSign on certificates an issuing CA signs; it now
matches openssl's [usr_cert], which is a change to what it emits.
@erossignon
erossignon force-pushed the feat/ecdsa-ca-signer branch from 7a8bbc6 to ef7955b Compare August 22, 2026 17:02
@erossignon
erossignon merged commit 8a7f2f7 into master Aug 22, 2026
20 checks passed
@erossignon
erossignon deleted the feat/ecdsa-ca-signer branch August 22, 2026 17:06
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.

1 participant