Alternative signatures: one certificate carrying two, for an algorithm migration
signedData builds the bytes that alternative signature covers. They are the PreTBSCertificate: the tbsCertificate without its signature AlgorithmIdentifier and without the altSignatureValue extension. A CRL has the same shape in PreTBSCertList, where signature is the second element rather than the third. Those bytes never appear on the wire, so a verifier rebuilds them. This is the one place the toolkit re-encodes what it parsed, so it does it by keeping the original bytes of every component it retains and recomputing only the two SEQUENCE headers whose lengths change. Nothing is re-serialized from a decoded model, because a model that normalized any byte would either fail every verification or accept an encoding the issuer never signed.
verify checks that signature under the alternative public key of the ISSUER, which clause 9.8.4 requires, through the same engine certification-path validation uses. The algorithm is not derived here: the structure states it in altSignatureAlgorithm, and the engine binds that stated algorithm to the key it was handed, so an algorithm that does not match the key is refused rather than reported as a bad signature.
subjectAltPublicKey reads a certificate's own alternative public key, which is what a verifier needs to check the certificate BELOW it. SubjectAltPublicKeyInfo carries the same two components in the same order as SubjectPublicKeyInfo, and a field name is not encoded, so the extension value is an SPKI on the wire and an importer takes it unchanged.
Clause 7.10.3 gives a CRL the same procedure with the same two extensions, and both verbs read a CRL as readily as a certificate. subjectAltPublicKeyInfo is certificate-only, which clause 7.2.2 states in a NOTE.
pki.x509.sign and pki.crl.sign produce all of this: an altKey beside the issuing key makes the two-pass signature the clause describes, so the ordering it requires is not the caller's to get right.
pki.altSig.signedData
pki.altSig.signedData(structure) -> Buffer
The bytes an alternative signature covers, for a certificate or a CRL. ITU-T X.509 (2019) clause 7.2.2 says to "exclude the signature component and the altSignatureValue extension from the public-key certificate, and generate the digital signature over the remaining DER encoded public-key certificate", and clause 7.10.3 says the same of a CRL. Two members are named signature, so draft-truskovsky-lamps-pq-hybrid-x509 gives the excluded one a type of its own: the PreTBSCertificate of section 4 is the tbsCertificate without "the signature field (the third element in the TBSCertificate sequence)", and the PreTBSCertList of section 5 is the tbsCertList without its second element. The altSignatureValue extension is removed from the extension list of each, and the relative order of the extensions that remain is unchanged. What comes back is that structure: the toBeSigned alone, carrying one member fewer.
These are the bytes Bouncy Castle emits and checks, so an alternative signature this toolkit makes verifies there, and one made there verifies here.
A verifier has to rebuild the encoding, since it never appears on the wire. Every component retained keeps the bytes it had, and only the SEQUENCE headers whose lengths change are recomputed; nothing is re-serialized from a decoded model.
A structure carrying no altSignatureAlgorithm extension throws altsig/absent: there is no stated algorithm, so there is no alternative signature to be checked and no question these bytes answer.
Example
async function example() {
var native = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
var alt = await pki.key.generate({ name: "ML-DSA-65" });
var nativeSpki = await pki.key.export(native.publicKey, { format: "der" });
var altSpki = await pki.key.export(alt.publicKey, { format: "der" });
var certDer = await pki.x509.sign({
subject: "CN=Catalyst", subjectPublicKey: nativeSpki, serialNumber: 1n,
notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z"),
extensions: { subjectAltPublicKeyInfo: altSpki },
}, { key: native.privateKey, altKey: alt.privateKey, altPublicKey: altSpki });
pki.altSig.signedData(certDer).length > 0; // -> true
}
example();
References
- spec ITU-T X.509 (2019) clause 7.2.2 / 7.10.3 / 9.8
- defends
alternative-signature-scope-confusion (CWE-347)
pki.altSig.subjectAltPublicKey
pki.altSig.subjectAltPublicKey(certificate) -> Buffer
A certificate's own alternative public key, as the SPKI DER an importer takes. This is the key that verifies the alternative signature on a certificate this one ISSUED, which is how the alternative chain is followed: clause 9.8.4 requires the alternative signature to be "verified using the alternative public key of the issuer".
SubjectAltPublicKeyInfo carries the same two components in the same order as SubjectPublicKeyInfo, and a field name is not encoded, so the extension value is already an SPKI and is returned unchanged rather than rebuilt from its parts.
A certificate with no such extension throws altsig/absent, as does a CRL, the extension being certificate-only (clause 7.2.2, NOTE).
Example
async function example() {
var native = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
var alt = await pki.key.generate({ name: "ML-DSA-65" });
var nativeSpki = await pki.key.export(native.publicKey, { format: "der" });
var altSpki = await pki.key.export(alt.publicKey, { format: "der" });
var caDer = await pki.x509.sign({
subject: "CN=Catalyst CA", subjectPublicKey: nativeSpki, serialNumber: 1n,
notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z"),
extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"], subjectAltPublicKeyInfo: altSpki },
}, { key: native.privateKey, altKey: alt.privateKey, altPublicKey: altSpki });
// The same bytes the CA certified, ready to verify a certificate this CA issued.
pki.altSig.subjectAltPublicKey(caDer).length === altSpki.length; // -> true
}
example();
References
- spec ITU-T X.509 (2019) clause 9.8.2
- defends
alternative-key-substitution (CWE-347)
pki.altSig.verify
pki.altSig.verify(structure, issuerAltPublicKey, opts?) -> Promise<boolean>
Verify the alternative signature on a certificate or a CRL. issuerAltPublicKey is the issuer's alternative public key as SPKI DER, which clause 9.8.4 requires: "it shall be verified using the alternative public key of the issuer". pki.altSig.subjectAltPublicKey reads that key out of the issuer's own certificate. Resolves true when the signature over the bytes pki.altSig.signedData describes verifies, and false when a signature was checked and did not.
The algorithm is not derived. The structure states it in its altSignatureAlgorithm extension, and that stated algorithm is bound to the key through the same engine certification-path validation uses, so a key that does not match the stated algorithm resolves false rather than being read under some other algorithm. Clause 9.8.3's NOTE 1 is why the algorithm is a separate extension: it sits inside the bytes the alternative signature covers, so it cannot be changed without breaking it.
A structure that states no alternative signature throws, since there is nothing to check and false would read as a signature that failed.
This verb answers one question: whether the issuer's alternative key signed this structure. It does not validate a path, check revocation, or read the native signature. pki.path.validate answers those, over the native signature.
Example
async function example() {
var native = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
var alt = await pki.key.generate({ name: "ML-DSA-65" });
var nativeSpki = await pki.key.export(native.publicKey, { format: "der" });
var altSpki = await pki.key.export(alt.publicKey, { format: "der" });
var caDer = await pki.x509.sign({
subject: "CN=Catalyst CA", subjectPublicKey: nativeSpki, serialNumber: 1n,
notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z"),
extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"], subjectAltPublicKeyInfo: altSpki },
}, { key: native.privateKey, altKey: alt.privateKey, altPublicKey: altSpki });
// The CA's own alternative key verifies the alternative signature it made.
await pki.altSig.verify(caDer, pki.altSig.subjectAltPublicKey(caDer)); // -> true
}
example();
References
- spec ITU-T X.509 (2019) clause 7.2.2 / 7.10.3 / 9.8.4
- defends
alternative-signature-forgery (CWE-347)