Statement of possession: certifying a key that cannot sign
PrivateKeyPossessionStatement ::= SEQUENCE { signer IssuerAndSerialNumber, cert Certificate OPTIONAL }
So the request's own signature is the proof, made by a key that is not the subject's. pki.csr.sign takes the statement as spec.privateKeyPossessionStatement and signs with the key given, which is the one case where it accepts a signing key that is not the subject's and a subject key that cannot sign. Without the statement both are still refused, so the relaxation is scoped to the request that declares it. The same value is a CRMF registration control, which is why parse reads either.
verifyRequest is the CA's side, and it applies the two requirements RFC 9883 states as MUSTs: the signature on the request is validated with the public key from the signature certificate, and that certificate's certification path is validated. Path validation is not optional, so the verb needs trustAnchors and refuses without them rather than answering a question it did not fully ask.
The two name comparisons the RFC states as SHOULDs end in a question a library cannot answer: "If they are different, the certificate policy MUST describe how the CA can determine that the two subject names identify the same entity." So subjectMatches and subjectAltNamesMatch are REPORTED, and a request whose names differ resolves valid: false with the reason naming the policy decision rather than being refused outright. A caller whose policy resolves it reads the fields and decides.
requestsSignatureCertificate reports the one MUST NOT: the attribute is for a key-establishment certificate, and a request that asks for a signature key usage is the misuse the RFC forbids.
pki.possession.parse
pki.possession.parse(value) -> { signer, certificate }
Read a PrivateKeyPossessionStatement. value is the attribute value of a PKCS#10 statementOfPossession attribute or the value of the CRMF registration control of the same name, which RFC 9883 gives the same OID, so one reader serves both.
signer is "the issuer name and certificate serial number of the signature certificate", and certificate is that certificate when the statement carries it, or null. The RFC allows the omission: "If the issuer of the key establishment certificate will be the same as the issuer of the signature certificate, then this component MAY be omitted", leaving the CA to look it up.
A carried certificate that signer does not name throws possession/signer-mismatch. The two would describe different certificates, and therefore different keys, while only one of them signed the request.
Example
async function example() {
var kem = await pki.key.generate({ name: "ML-KEM-768" });
var sig = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
var kemSpki = await pki.key.export(kem.publicKey, { format: "der" });
var sigSpki = await pki.key.export(sig.publicKey, { format: "der" });
var sigCert = await pki.x509.sign({
subject: "CN=kem.example", subjectPublicKey: sigSpki, serialNumber: 34n,
notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z"),
}, { key: sig.privateKey });
var parsedCert = pki.schema.x509.parse(sigCert);
var csr = await pki.csr.sign({
subject: "CN=kem.example", subjectPublicKey: kemSpki,
privateKeyPossessionStatement: {
signer: { issuer: parsedCert.issuer.bytes, serialNumber: parsedCert.serialNumber },
certificate: sigCert,
},
}, { key: sig.privateKey });
var attr = pki.schema.csr.parse(csr).attributes
.filter(function (a) { return a.type === pki.oid.byName("statementOfPossession"); })[0];
pki.possession.parse(attr.values[0]).certificate !== null; // -> true
}
example();
References
- spec RFC 9883 sec. 3
- defends
possession-statement-confusion (CWE-347)
pki.possession.verifyRequest
pki.possession.verifyRequest(request, opts) -> Promise<verdict>
The CA's side of RFC 9883. request is a PKCS#10 certification request carrying a statementOfPossession attribute. Applies the two requirements section 4 states as MUSTs, and reports the two it states as SHOULDs.
The MUSTs are enforced. "The CA MUST validate the signature on the certificate request using the public key from the signature certificate", so the signature is checked against that key and not against the key being certified, which for a key-establishment key could not have signed anything. "The CA MUST perform certification path validation for the signature certificate as specified in Section 6 of [RFC5280]", so opts.trustAnchors is required and the verb throws without it instead of returning a verdict that skipped a MUST.
The SHOULDs are reported. Both end in "the certificate policy MUST describe how the CA can determine that the two subject names identify the same entity", which is a question this library cannot answer, so subjectMatches and subjectAltNamesMatch carry the comparison and valid is false with a reason naming the policy decision when they differ. A caller whose policy resolves it reads those fields and decides for itself.
requestsSignatureCertificate reports the MUST NOT: "The privateKeyPossessionStatement attribute MUST NOT be used to obtain a signature certificate." A request asking for digitalSignature, nonRepudiation, keyCertSign or cRLSign is that misuse, and valid is false.
A request carrying no statement throws possession/absent: it is not an unverified RFC 9883 request, it is a different question, and pki.csr.verify is the verb for it.
Options
intermediates The rest of the signature certificate's path, when an intermediate CA issued it rather than an anchor directly. Ordered as `pki.path.validate` takes a path, from the certificate nearest an anchor down to the issuer of the signature certificate, which this verb appends. Omitted, the signature certificate is validated on its own, which succeeds only when an anchor issued it.
Example
async function example() {
var kem = await pki.key.generate({ name: "ML-KEM-768" });
var sig = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
var kemSpki = await pki.key.export(kem.publicKey, { format: "der" });
var sigSpki = await pki.key.export(sig.publicKey, { format: "der" });
var ca = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
var caSpki = await pki.key.export(ca.publicKey, { format: "der" });
var caCert = await pki.x509.sign({
subject: "CN=Possession CA", subjectPublicKey: caSpki, serialNumber: 1n,
notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z"),
extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"] },
}, { key: ca.privateKey });
var sigCert = await pki.x509.sign({
subject: "CN=kem.example", subjectPublicKey: sigSpki, serialNumber: 34n,
notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z"),
extensions: { keyUsage: ["digitalSignature"] },
}, { cert: caCert, key: ca.privateKey });
var p = pki.schema.x509.parse(sigCert);
var csr = await pki.csr.sign({
subject: "CN=kem.example", subjectPublicKey: kemSpki,
privateKeyPossessionStatement: {
signer: { issuer: p.issuer.bytes, serialNumber: p.serialNumber }, certificate: sigCert,
},
}, { key: sig.privateKey });
var v = await pki.possession.verifyRequest(csr,
{ trustAnchors: [caCert], time: new Date("2027-06-01T00:00:00Z") });
v.valid; // -> true
}
example();
References
- spec RFC 9883 sec. 4
- defends
possession-statement-forgery (CWE-347)