Alternative signatures: one certificate carrying two, for an algorithm migration
signedData builds the bytes that alternative signature covers, and clause 7.2.2 states them exactly: the certificate with the outer signature component and the altSignatureValue extension both removed. The same clause is explicit that a verifier has to rebuild that encoding, since the bytes never appear on the wire in that form. 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 three 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. Clause 7.2.2 states them for a certificate: "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". Clause 7.10.3 says the same of a CRL. The signature component is the outer BIT STRING, which the sibling clause identifies by calling the native signature the value generated into it, so what comes back is a SEQUENCE of the toBeSigned and the outer algorithm identifier.
The same clause requires a verifier to rebuild that 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)