@blamejs/pki 0.8.31 → 0.8.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +401 -0
- package/README.md +66 -15
- package/bin/pki.js +716 -30
- package/examples/issue-a-certificate.js +90 -0
- package/examples/post-quantum.js +66 -0
- package/examples/read-a-certificate.js +56 -0
- package/examples/revoke-and-check.js +86 -0
- package/examples/sign-and-verify-cms.js +63 -0
- package/index.js +18 -5
- package/lib/acme.js +223 -74
- package/lib/alt-sig.js +441 -0
- package/lib/asn1-der.js +48 -25
- package/lib/attrcert-sign.js +27 -19
- package/lib/cbor-det.js +24 -14
- package/lib/cmc-build.js +9 -2
- package/lib/cmc-verify.js +14 -3
- package/lib/cmp-build.js +17 -5
- package/lib/cmp-session.js +24 -11
- package/lib/cmp-verify.js +28 -19
- package/lib/cms-compress.js +3 -1
- package/lib/cms-decrypt.js +102 -7
- package/lib/cms-encrypt.js +59 -7
- package/lib/cms-sign.js +31 -9
- package/lib/cms-verify.js +22 -18
- package/lib/composite-kem.js +30 -18
- package/lib/composite-sig.js +29 -14
- package/lib/constants.js +26 -0
- package/lib/crl-sign.js +111 -19
- package/lib/crmf-sign.js +167 -15
- package/lib/csr-sign.js +117 -6
- package/lib/ct.js +1008 -58
- package/lib/est.js +29 -16
- package/lib/framework-error.js +12 -0
- package/lib/guard-async.js +1 -1
- package/lib/guard-bytes.js +103 -20
- package/lib/guard-crypto.js +205 -3
- package/lib/guard-encoding.js +6 -1
- package/lib/guard-identifier.js +55 -8
- package/lib/guard-intrinsic.js +43 -0
- package/lib/guard-json.js +122 -9
- package/lib/guard-limits.js +12 -1
- package/lib/guard-list.js +63 -8
- package/lib/guard-name.js +167 -20
- package/lib/guard-range.js +43 -5
- package/lib/guard-text.js +41 -0
- package/lib/guard-verdict.js +39 -8
- package/lib/hpke.js +688 -42
- package/lib/http-transport.js +156 -4
- package/lib/identity-match.js +4 -4
- package/lib/inspect.js +33 -6
- package/lib/jose.js +93 -25
- package/lib/key.js +18 -13
- package/lib/lint.js +165 -1
- package/lib/merkle.js +233 -27
- package/lib/ocsp.js +27 -14
- package/lib/oid.js +14 -4
- package/lib/path-validate.js +143 -61
- package/lib/pkcs12-build.js +7 -2
- package/lib/pki-build.js +31 -7
- package/lib/possession.js +575 -0
- package/lib/related-cert.js +708 -0
- package/lib/revocation-fetch.js +6 -6
- package/lib/scep.js +15 -5
- package/lib/schema-c509.js +5 -2
- package/lib/schema-cmc.js +1 -1
- package/lib/schema-cmp.js +8 -8
- package/lib/schema-cms.js +2 -15
- package/lib/schema-csr.js +69 -0
- package/lib/schema-engine.js +10 -3
- package/lib/schema-pkix.js +228 -2
- package/lib/schema-tsp.js +7 -6
- package/lib/sign-scheme.js +22 -9
- package/lib/sigstore.js +684 -168
- package/lib/sleep.js +8 -1
- package/lib/smime.js +22 -1
- package/lib/tlog.js +1235 -0
- package/lib/tls-cert-compress.js +25 -8
- package/lib/tsp-sign.js +120 -35
- package/lib/tuf.js +1395 -0
- package/lib/validator-tpm.js +4 -1
- package/lib/verify-seam.js +7 -1
- package/lib/webauthn-mds.js +41 -4
- package/lib/webauthn.js +43 -25
- package/lib/webcrypto.js +48 -46
- package/lib/x509-sign.js +225 -19
- package/package.json +2 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.possession
|
|
6
|
+
* @nav Enrollment
|
|
7
|
+
* @title Possession statements
|
|
8
|
+
* @order 238
|
|
9
|
+
* @slug possession
|
|
10
|
+
* @fullname Statement of possession: certifying a key that cannot sign
|
|
11
|
+
*
|
|
12
|
+
* @intro
|
|
13
|
+
* PKCS#10 proves possession of the key being certified by signing the request
|
|
14
|
+
* with it, which a key-establishment key cannot do. An ML-KEM, X25519 or X448 key
|
|
15
|
+
* therefore cannot be certified through a plain PKCS#10 request at all. RFC 9883
|
|
16
|
+
* answers that by signing the request with a DIFFERENT key, one the requester
|
|
17
|
+
* already holds a certificate for, and naming that certificate in an attribute:
|
|
18
|
+
*
|
|
19
|
+
* PrivateKeyPossessionStatement ::= SEQUENCE {
|
|
20
|
+
* signer IssuerAndSerialNumber,
|
|
21
|
+
* cert Certificate OPTIONAL }
|
|
22
|
+
*
|
|
23
|
+
* So the request's own signature is the proof, made by a key that is not the
|
|
24
|
+
* subject's. `pki.csr.sign` takes the statement as
|
|
25
|
+
* `spec.privateKeyPossessionStatement` and signs with the key given, which is the
|
|
26
|
+
* one case where it accepts a signing key that is not the subject's and a subject
|
|
27
|
+
* key that cannot sign. Without the statement both are still refused, so the
|
|
28
|
+
* relaxation is scoped to the request that declares it. The same value is a CRMF
|
|
29
|
+
* registration control, which is why `parse` reads either.
|
|
30
|
+
*
|
|
31
|
+
* `verifyRequest` is the CA's side, and it applies the two requirements RFC 9883
|
|
32
|
+
* states as MUSTs: the signature on the request is validated with the public key
|
|
33
|
+
* from the signature certificate, and that certificate's certification path is
|
|
34
|
+
* validated. Path validation is not optional, so the verb needs `trustAnchors` and
|
|
35
|
+
* refuses without them rather than answering a question it did not fully ask.
|
|
36
|
+
*
|
|
37
|
+
* The two name comparisons the RFC states as SHOULDs end in a question a library
|
|
38
|
+
* cannot answer: "If they are different, the certificate policy MUST describe how
|
|
39
|
+
* the CA can determine that the two subject names identify the same entity." So
|
|
40
|
+
* `subjectMatches` and `subjectAltNamesMatch` are REPORTED, and a request whose
|
|
41
|
+
* names differ resolves `valid: false` with the reason naming the policy decision
|
|
42
|
+
* rather than being refused outright. A caller whose policy resolves it reads the
|
|
43
|
+
* fields and decides.
|
|
44
|
+
*
|
|
45
|
+
* `requestsSignatureCertificate` reports the one MUST NOT: the attribute is for a
|
|
46
|
+
* key-establishment certificate, so a request that asks for a signature key usage,
|
|
47
|
+
* or that asks to certify a subject key which can only ever sign, is the misuse the
|
|
48
|
+
* RFC forbids.
|
|
49
|
+
*
|
|
50
|
+
* @card Read and check an RFC 9883 statement of possession, the attribute that lets a
|
|
51
|
+
* key-establishment key be certified by a request another key signed.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
var asn1 = require("./asn1-der");
|
|
55
|
+
var constants = require("./constants");
|
|
56
|
+
var oid = require("./oid");
|
|
57
|
+
var pkix = require("./schema-pkix");
|
|
58
|
+
var schema = require("./schema-engine");
|
|
59
|
+
var schemaX509 = require("./schema-x509");
|
|
60
|
+
var schemaCsr = require("./schema-csr");
|
|
61
|
+
var frameworkError = require("./framework-error");
|
|
62
|
+
var guard = require("./guard-all");
|
|
63
|
+
var intrinsic = require("./guard-intrinsic");
|
|
64
|
+
var seam = require("./verify-seam");
|
|
65
|
+
|
|
66
|
+
var PossessionError = frameworkError.PossessionError;
|
|
67
|
+
var _create = intrinsic.create;
|
|
68
|
+
var _assign = intrinsic.assign;
|
|
69
|
+
var _forEach = intrinsic.forEach;
|
|
70
|
+
var _map = intrinsic.map;
|
|
71
|
+
var _isArray = intrinsic.isArray;
|
|
72
|
+
var _promiseResolve = intrinsic.uncurry(intrinsic.promiseResolve);
|
|
73
|
+
/** @internal The promise constructor captured at load, used as the RECEIVER for the captured
|
|
74
|
+
* `Promise.resolve`. Handing it the live `globalThis.Promise` instead put the capture back under a
|
|
75
|
+
* replaceable constructor: `Promise.resolve` builds through its receiver, so a replacement installed
|
|
76
|
+
* after load decided what the awaited value became. Here that value is a path-validation result, and a
|
|
77
|
+
* constructor substituting `{ valid: true }` for it made a correctly signed request report `valid`
|
|
78
|
+
* while its signature certificate chained to an untrusted CA, which is the RFC 9883 sec. 4 MUST this
|
|
79
|
+
* verb exists to apply. */
|
|
80
|
+
var _Promise = intrinsic.Promise;
|
|
81
|
+
var _push = intrinsic.push;
|
|
82
|
+
var _join = intrinsic.join;
|
|
83
|
+
var _every = intrinsic.every;
|
|
84
|
+
var _filter = intrinsic.filter;
|
|
85
|
+
var _keys = intrinsic.keys;
|
|
86
|
+
var _hasOwn = intrinsic.hasOwn;
|
|
87
|
+
var _strIndexOf = intrinsic.uncurry(String.prototype.indexOf);
|
|
88
|
+
var _bufToString = intrinsic.bufToString;
|
|
89
|
+
|
|
90
|
+
function _err(code, message, cause) { return new PossessionError(code, message, cause); }
|
|
91
|
+
|
|
92
|
+
var _seam = seam.makeSeam("possession", PossessionError, "possession/bad-input");
|
|
93
|
+
/** @internal Set by path-validate at load, so the path check runs through the same validator a caller
|
|
94
|
+
* would call and this module does not hold a second copy of it. */
|
|
95
|
+
var _validatePath = null;
|
|
96
|
+
function setEngine(engine) { _seam.setEngine(engine.verifyWithSpki); _validatePath = engine.validate; }
|
|
97
|
+
|
|
98
|
+
var OID_SOP = oid.byName("statementOfPossession");
|
|
99
|
+
if (!OID_SOP) throw new Error("possession: the RFC 9883 attribute OID is not in the registry");
|
|
100
|
+
|
|
101
|
+
var NS = pkix.makeNS("possession", PossessionError, oid);
|
|
102
|
+
/** @internal The signer is an IssuerAndSerialNumber, the same type CMS names and RFC 9763 reuses, read
|
|
103
|
+
* through the one shared definition so the three cannot diverge on the structure. */
|
|
104
|
+
var STATEMENT = schema.seq([
|
|
105
|
+
schema.field("signer", pkix.issuerAndSerialNumber(NS, { code: "possession/bad-statement" })),
|
|
106
|
+
schema.optional("certificate", schema.any(), { whenUniversal: [asn1.TAGS.SEQUENCE] }),
|
|
107
|
+
], {
|
|
108
|
+
assert: "sequence", arity: { min: 1, max: 2 },
|
|
109
|
+
code: "possession/bad-statement", what: "PrivateKeyPossessionStatement",
|
|
110
|
+
build: function (m) {
|
|
111
|
+
return {
|
|
112
|
+
signer: m.fields.signer.value.result,
|
|
113
|
+
certificate: m.fields.certificate.present ? m.fields.certificate.node.bytes : null,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
var KNOWN_VERIFY_OPTS = _assign(_create(null), {
|
|
119
|
+
trustAnchors: 1, time: 1, signatureCertificate: 1, intermediates: 1,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/** @internal Whether a certificate is the one an IssuerAndSerialNumber names. Both halves are compared,
|
|
123
|
+
* because a serial is unique only within an issuer. The DN comparison is the canonical one RFC 5280
|
|
124
|
+
* sec. 7.1 states, through the shared guard, so two spellings of one name are one name. */
|
|
125
|
+
function _namesCertificate(signer, certParsed) {
|
|
126
|
+
if (signer.serialNumber !== certParsed.serialNumber) return false;
|
|
127
|
+
try {
|
|
128
|
+
return guard.name.dnEqual(signer.issuer.rdns, certParsed.issuer.rdns,
|
|
129
|
+
_err, "possession/bad-input", "the signer issuer name") === true;
|
|
130
|
+
} catch (_e) { return false; }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @primitive pki.possession.parse
|
|
135
|
+
* @signature pki.possession.parse(value) -> { signer, certificate }
|
|
136
|
+
* @since 0.8.44
|
|
137
|
+
* @status stable
|
|
138
|
+
* @spec RFC 9883 sec. 3
|
|
139
|
+
* @defends possession-statement-confusion (CWE-347)
|
|
140
|
+
* @related pki.possession.verifyRequest, pki.csr.sign, pki.crmf.build
|
|
141
|
+
*
|
|
142
|
+
* Read a `PrivateKeyPossessionStatement`. `value` is the attribute value of a PKCS#10
|
|
143
|
+
* `statementOfPossession` attribute or the value of the CRMF registration control of the same name,
|
|
144
|
+
* which RFC 9883 gives the same OID, so one reader serves both.
|
|
145
|
+
*
|
|
146
|
+
* `signer` is "the issuer name and certificate serial number of the signature certificate", and
|
|
147
|
+
* `certificate` is that certificate when the statement carries it, or `null`. The RFC allows the
|
|
148
|
+
* omission: "If the issuer of the key establishment certificate will be the same as the issuer of the
|
|
149
|
+
* signature certificate, then this component MAY be omitted", leaving the CA to look it up.
|
|
150
|
+
*
|
|
151
|
+
* A carried certificate that `signer` does not name throws `possession/signer-mismatch`. The two would
|
|
152
|
+
* describe different certificates, and therefore different keys, while only one of them signed the
|
|
153
|
+
* request.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* var kem = await pki.key.generate({ name: "ML-KEM-768" });
|
|
157
|
+
* var sig = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
|
|
158
|
+
* var kemSpki = await pki.key.export(kem.publicKey, { format: "der" });
|
|
159
|
+
* var sigSpki = await pki.key.export(sig.publicKey, { format: "der" });
|
|
160
|
+
* var sigCert = await pki.x509.sign({
|
|
161
|
+
* subject: "CN=kem.example", subjectPublicKey: sigSpki, serialNumber: 34n,
|
|
162
|
+
* notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z"),
|
|
163
|
+
* }, { key: sig.privateKey });
|
|
164
|
+
* var parsedCert = pki.schema.x509.parse(sigCert);
|
|
165
|
+
* var csr = await pki.csr.sign({
|
|
166
|
+
* subject: "CN=kem.example", subjectPublicKey: kemSpki,
|
|
167
|
+
* privateKeyPossessionStatement: {
|
|
168
|
+
* signer: { issuer: parsedCert.issuer.bytes, serialNumber: parsedCert.serialNumber },
|
|
169
|
+
* certificate: sigCert,
|
|
170
|
+
* },
|
|
171
|
+
* }, { key: sig.privateKey });
|
|
172
|
+
* var attr = pki.schema.csr.parse(csr).attributes
|
|
173
|
+
* .filter(function (a) { return a.type === pki.oid.byName("statementOfPossession"); })[0];
|
|
174
|
+
* pki.possession.parse(attr.values[0]).certificate !== null; // -> true
|
|
175
|
+
*/
|
|
176
|
+
function parse(value) {
|
|
177
|
+
var der = guard.bytes.snapshot(value, _err, "possession/bad-statement", "the possession statement");
|
|
178
|
+
var node;
|
|
179
|
+
try { node = asn1.decode(der); }
|
|
180
|
+
catch (e) { throw _err("possession/bad-statement", "the possession statement must be DER", e); }
|
|
181
|
+
var out;
|
|
182
|
+
try { out = schema.walk(STATEMENT, node, NS).result; }
|
|
183
|
+
catch (e) {
|
|
184
|
+
if (e instanceof PossessionError) throw e;
|
|
185
|
+
throw _err("possession/bad-statement", "the possession statement must be a SEQUENCE { signer, cert? } (RFC 9883 sec. 3)", e);
|
|
186
|
+
}
|
|
187
|
+
return _bindSignerToCertificate(out);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** @internal The signer-names-the-carried-certificate rule, in one place because the statement reaches a
|
|
191
|
+
* consumer through TWO doors: this module's `parse`, and the certification-request parser's
|
|
192
|
+
* recognized-attribute reader, which decodes the value in place while parsing the request. A statement
|
|
193
|
+
* naming one certificate while carrying another describes two different keys, and only one of them
|
|
194
|
+
* signed the request, so a door that skips the rule hands back a signer that does not identify the key
|
|
195
|
+
* the signature was made with. Both doors call this. */
|
|
196
|
+
function _bindSignerToCertificate(out) {
|
|
197
|
+
if (out.certificate !== null) {
|
|
198
|
+
var certParsed;
|
|
199
|
+
try { certParsed = schemaX509.parse(out.certificate); }
|
|
200
|
+
catch (e) { throw _err("possession/bad-statement", "the possession statement cert component must be an X.509 certificate", e); }
|
|
201
|
+
if (!_namesCertificate(out.signer, certParsed)) {
|
|
202
|
+
throw _err("possession/signer-mismatch",
|
|
203
|
+
"the certificate the statement carries is not the one its signer names (RFC 9883 sec. 3)");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** @internal The statement of a parsed certification request, or null when it carries none. The
|
|
210
|
+
* recognized-attribute reader decodes it in place, so the value is read again only when a caller
|
|
211
|
+
* handed over a request some other reader parsed. The in-place record has been through the SHAPE rules
|
|
212
|
+
* and not the binding rule, which lives here rather than in the request parser, so it is applied to
|
|
213
|
+
* that record on the way out and neither door returns a statement the other would refuse. */
|
|
214
|
+
function statementOf(parsedCsr) {
|
|
215
|
+
var found = _filter(parsedCsr.attributes || [], function (a) { return a.type === OID_SOP; });
|
|
216
|
+
/** @internal More than one is REFUSED rather than resolved by taking the first. Selecting `[0]` skipped
|
|
217
|
+
* every binding check on the rest, so a request carrying a sound statement followed by one naming a
|
|
218
|
+
* different certificate was accepted on the strength of the first alone. Which of them a CA should honor
|
|
219
|
+
* is stated nowhere, the same reason the extensionRequest ambiguity is refused. */
|
|
220
|
+
_assertOneAttribute(found.length, "statementOfPossession");
|
|
221
|
+
if (!found.length) return null;
|
|
222
|
+
var attr = found[0];
|
|
223
|
+
if (attr.privateKeyPossessionStatement) return _bindSignerToCertificate(attr.privateKeyPossessionStatement);
|
|
224
|
+
return parse(attr.values[0]);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** @internal One ambiguity refusal for every attribute this module selects a single instance of. A CSR's
|
|
228
|
+
* attributes are a SET and nothing orders them, so a verb that takes the first of several checks one and
|
|
229
|
+
* certifies against the others. */
|
|
230
|
+
function _assertOneAttribute(count, name) {
|
|
231
|
+
if (count > 1) {
|
|
232
|
+
throw _err("possession/bad-request", "this request carries " + count + " " + name +
|
|
233
|
+
" attributes, and which of them a CA should honor is not stated anywhere, so the RFC 9883 " +
|
|
234
|
+
"checks cannot be made against one of them alone");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** @internal A requested extension of a certification request, decoded, or null when the request asks for
|
|
239
|
+
* none. The extensions a request asks for hang off its extensionRequest attribute, so they are reached
|
|
240
|
+
* through the attribute rather than off the request itself.
|
|
241
|
+
*
|
|
242
|
+
* An extension that is PRESENT but will not decode is a fault, not an absence. The sec. 3 name comparisons
|
|
243
|
+
* and the sec. 6 prohibition read these, so reporting an unreadable one as absent leaves the check with
|
|
244
|
+
* nothing to read and the verdict saying it does not apply, while the request itself is accepted. The
|
|
245
|
+
* certification-request parser holds the extension WRAPPER to its shape and not its decoded value, so
|
|
246
|
+
* nothing upstream refuses it either. Naming the fault is the only reading that does not quietly drop a
|
|
247
|
+
* check the specification asks for. */
|
|
248
|
+
function _requestedExtension(parsedCsr, name) {
|
|
249
|
+
var wanted = oid.byName(name);
|
|
250
|
+
var attrs = parsedCsr.attributes || [];
|
|
251
|
+
for (var i = 0; i < attrs.length; i++) {
|
|
252
|
+
var exts = attrs[i].extensions || [];
|
|
253
|
+
for (var j = 0; j < exts.length; j++) {
|
|
254
|
+
if (exts[j].oid !== wanted) continue;
|
|
255
|
+
/** @internal The extension IS present, its OID having matched. `decodeExtension` reports a value it
|
|
256
|
+
* cannot read as `decoded: null` rather than throwing, which is the same thing this function would
|
|
257
|
+
* otherwise return for an extension the request never asked for, so the two have to be told apart
|
|
258
|
+
* here or the caller cannot. */
|
|
259
|
+
var record;
|
|
260
|
+
try { record = schemaCsr.decodeExtension(exts[j]); }
|
|
261
|
+
catch (e) {
|
|
262
|
+
throw _err("possession/bad-request", "the request asks for a " + name +
|
|
263
|
+
" extension this build cannot read, so the RFC 9883 check it feeds cannot be " +
|
|
264
|
+
"made; an unreadable policy field is not an absent one", e);
|
|
265
|
+
}
|
|
266
|
+
if (record.decoded === null || record.decoded === undefined) {
|
|
267
|
+
throw _err("possession/bad-request", "the request asks for a " + name +
|
|
268
|
+
" extension whose value does not decode" + (record.code ? " (" + record.code + ")" : "") +
|
|
269
|
+
", so the RFC 9883 check it feeds cannot be made; an unreadable policy field is " +
|
|
270
|
+
"not an absent one");
|
|
271
|
+
}
|
|
272
|
+
return record.decoded;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** @internal Whether a signature certificate's subjectAltName carries the entry a request asks for, each
|
|
279
|
+
* form compared under its own RFC 5280 rule by `guard.name.generalNameEqual`. Keying both lists by the
|
|
280
|
+
* rendered value instead made the comparison verbatim for every form at once, so `EXAMPLE.com` in the
|
|
281
|
+
* certificate and `example.com` in the request read as two hosts although RFC 4343 makes DNS
|
|
282
|
+
* case-insensitive, and a directoryName re-encoded to equivalent bytes read as a second name although
|
|
283
|
+
* RFC 5280 sec. 7.1 compares it attribute by attribute. A form the guard cannot hold to its rule answers
|
|
284
|
+
* "not-comparable", which is not a match: the comparison this verb reports is one it actually made. */
|
|
285
|
+
function _sanCarries(certNames, asked, pairs) {
|
|
286
|
+
var names = certNames || [];
|
|
287
|
+
for (var i = 0; i < names.length; i++) {
|
|
288
|
+
pairs.tick();
|
|
289
|
+
if (guard.name.generalNameEqual(names[i], asked, _err, "possession/bad-input",
|
|
290
|
+
"a subjectAltName entry") === "match") return true;
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @internal The key-usage bits that make a certificate a signature certificate, which RFC 9883 sec. 6
|
|
296
|
+
* states the attribute MUST NOT be used to obtain. The decoded keyUsage is a flags object, so the
|
|
297
|
+
* names are read off it rather than from a list of set bits. */
|
|
298
|
+
var SIGNATURE_USAGES = ["digitalSignature", "nonRepudiation", "keyCertSign", "cRLSign"];
|
|
299
|
+
|
|
300
|
+
/** @internal The subject keys that can ONLY sign, from the shared table in `schema-pkix`. The
|
|
301
|
+
* `lint/rfc9883/signature-certificate-requested` row reads the same one, so `pki.lint.csr` under that
|
|
302
|
+
* profile and this verb reach the same verdict about the same request: the linter grading conforming what
|
|
303
|
+
* this refuses is what the shared table removes. */
|
|
304
|
+
var _isSignatureOnlyKey = pkix.signatureOnlyKeyOid(NS);
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @primitive pki.possession.verifyRequest
|
|
308
|
+
* @signature pki.possession.verifyRequest(request, opts) -> Promise<verdict>
|
|
309
|
+
* @since 0.8.44
|
|
310
|
+
* @status stable
|
|
311
|
+
* @spec RFC 9883 sec. 3, RFC 9883 sec. 4, RFC 9883 sec. 6
|
|
312
|
+
* @defends possession-statement-forgery (CWE-347)
|
|
313
|
+
* @related pki.possession.parse, pki.csr.sign, pki.path.validate
|
|
314
|
+
*
|
|
315
|
+
* The CA's side of RFC 9883. `request` is a PKCS#10 certification request carrying a
|
|
316
|
+
* `statementOfPossession` attribute. Applies the two requirements section 3 states as MUSTs, and
|
|
317
|
+
* reports the two it states as SHOULDs.
|
|
318
|
+
*
|
|
319
|
+
* The MUSTs are enforced. "The CA MUST validate the signature on the certificate request using the
|
|
320
|
+
* public key from the signature certificate", so the signature is checked against that key and not
|
|
321
|
+
* against the key being certified, which for a key-establishment key could not have signed anything.
|
|
322
|
+
* "The CA MUST perform certification path validation for the signature certificate as specified in
|
|
323
|
+
* Section 6 of [RFC5280]", so `opts.trustAnchors` is required and the verb throws without it instead of
|
|
324
|
+
* returning a verdict that skipped a MUST.
|
|
325
|
+
*
|
|
326
|
+
* The SHOULDs are reported. Both end in "the certificate policy MUST describe how the CA can determine
|
|
327
|
+
* that the two subject names identify the same entity", which is a question this library cannot answer,
|
|
328
|
+
* so `subjectMatches` and `subjectAltNamesMatch` carry the comparison and `valid` is `false` with a
|
|
329
|
+
* reason naming the policy decision when they differ. A caller whose policy resolves it reads those
|
|
330
|
+
* fields and decides for itself.
|
|
331
|
+
*
|
|
332
|
+
* `requestsSignatureCertificate` reports the section 6 MUST NOT: "The privateKeyPossessionStatement
|
|
333
|
+
* attribute MUST NOT be used to obtain a signature certificate." A request asking for `digitalSignature`,
|
|
334
|
+
* `nonRepudiation`, `keyCertSign` or `cRLSign` is that misuse, and so is a request whose subject key can
|
|
335
|
+
* only ever sign, which section 4 states as a property of the request: "the subjectPKInfo MUST contain the
|
|
336
|
+
* public key for the key establishment algorithm". Either way `valid` is `false`.
|
|
337
|
+
*
|
|
338
|
+
* A request carrying no statement throws `possession/absent`: it is not an unverified RFC 9883 request,
|
|
339
|
+
* it is a different question, and `pki.csr.verify` is the verb for it.
|
|
340
|
+
*
|
|
341
|
+
* @opts trustAnchors Required. The anchors the signature certificate's path is validated to, in the form `pki.path.validate` takes.
|
|
342
|
+
* @opts time The instant the path is validated at, defaulting to now when omitted or `null`. A value
|
|
343
|
+
* that is present and is not a usable `Date` is `possession/bad-input`.
|
|
344
|
+
* @opts signatureCertificate The signature certificate, for a statement that omits it; refused when it is not the one `signer` names.
|
|
345
|
+
* @opts 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.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* var kem = await pki.key.generate({ name: "ML-KEM-768" });
|
|
349
|
+
* var sig = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
|
|
350
|
+
* var kemSpki = await pki.key.export(kem.publicKey, { format: "der" });
|
|
351
|
+
* var sigSpki = await pki.key.export(sig.publicKey, { format: "der" });
|
|
352
|
+
* var ca = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
|
|
353
|
+
* var caSpki = await pki.key.export(ca.publicKey, { format: "der" });
|
|
354
|
+
* var caCert = await pki.x509.sign({
|
|
355
|
+
* subject: "CN=Possession CA", subjectPublicKey: caSpki, serialNumber: 1n,
|
|
356
|
+
* notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z"),
|
|
357
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"] },
|
|
358
|
+
* }, { key: ca.privateKey });
|
|
359
|
+
* var sigCert = await pki.x509.sign({
|
|
360
|
+
* subject: "CN=kem.example", subjectPublicKey: sigSpki, serialNumber: 34n,
|
|
361
|
+
* notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z"),
|
|
362
|
+
* extensions: { keyUsage: ["digitalSignature"] },
|
|
363
|
+
* }, { cert: caCert, key: ca.privateKey });
|
|
364
|
+
* var p = pki.schema.x509.parse(sigCert);
|
|
365
|
+
* var csr = await pki.csr.sign({
|
|
366
|
+
* subject: "CN=kem.example", subjectPublicKey: kemSpki,
|
|
367
|
+
* privateKeyPossessionStatement: {
|
|
368
|
+
* signer: { issuer: p.issuer.bytes, serialNumber: p.serialNumber }, certificate: sigCert,
|
|
369
|
+
* },
|
|
370
|
+
* }, { key: sig.privateKey });
|
|
371
|
+
* var v = await pki.possession.verifyRequest(csr,
|
|
372
|
+
* { trustAnchors: [caCert], time: new Date("2027-06-01T00:00:00Z") });
|
|
373
|
+
* v.valid; // -> true
|
|
374
|
+
*/
|
|
375
|
+
function verifyRequest(request, opts) {
|
|
376
|
+
return guard.async.deferred(function () { return _verifyRequest(request, opts); });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function _verifyRequest(request, opts) {
|
|
380
|
+
guard.identifier.assertKnownKeys(opts, KNOWN_VERIFY_OPTS, _err, "possession/bad-input",
|
|
381
|
+
"pki.possession.verifyRequest has an unknown option: ");
|
|
382
|
+
var o = guard.identifier.snapshotOptions(opts, KNOWN_VERIFY_OPTS);
|
|
383
|
+
if (o.trustAnchors == null || !_isArray(o.trustAnchors) || o.trustAnchors.length === 0) {
|
|
384
|
+
throw _err("possession/bad-input",
|
|
385
|
+
"opts.trustAnchors is required: RFC 9883 sec. 4 makes certification path validation of the signature certificate a MUST, and a verdict that skipped it would not answer the question asked");
|
|
386
|
+
}
|
|
387
|
+
if (_validatePath === null) {
|
|
388
|
+
throw _err("possession/bad-input", "the possession path validator is not initialized (require pki before use)");
|
|
389
|
+
}
|
|
390
|
+
/** @internal The documented default, constructed here. Forwarded as `undefined`, it reached the path
|
|
391
|
+
* validator's always-on validity check, which requires a valid Date and refuses `path/bad-input`, so
|
|
392
|
+
* the default never applied and every caller had to supply a clock to get any verdict at all. A value
|
|
393
|
+
* that is present and is not a usable Date is refused HERE, under this verb's own code, rather than
|
|
394
|
+
* reported against the validator a caller did not call. */
|
|
395
|
+
var at = o.time;
|
|
396
|
+
if (at === undefined || at === null) at = new intrinsic.Date();
|
|
397
|
+
else guard.time.assertValid(at, _err, "possession/bad-input", "opts.time (the instant the signature certificate's path is validated at)");
|
|
398
|
+
|
|
399
|
+
var der = guard.bytes.snapshot(request, _err, "possession/bad-input", "the certification request");
|
|
400
|
+
var parsed;
|
|
401
|
+
try { parsed = schemaCsr.parse(der); }
|
|
402
|
+
catch (e) { throw _err("possession/bad-input", "the certification request must be a PKCS#10 CertificationRequest", e); }
|
|
403
|
+
|
|
404
|
+
var statement = statementOf(parsed);
|
|
405
|
+
if (statement === null) {
|
|
406
|
+
throw _err("possession/absent",
|
|
407
|
+
"the certification request carries no statementOfPossession attribute, so there is no statement to check (RFC 9883 sec. 3)");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
var sigCertDer = statement.certificate;
|
|
411
|
+
if (sigCertDer === null) {
|
|
412
|
+
if (o.signatureCertificate == null) {
|
|
413
|
+
throw _err("possession/no-certificate",
|
|
414
|
+
"the statement omits the signature certificate, so opts.signatureCertificate must supply the one its signer names (RFC 9883 sec. 3)");
|
|
415
|
+
}
|
|
416
|
+
sigCertDer = guard.bytes.snapshot(o.signatureCertificate, _err, "possession/bad-input", "opts.signatureCertificate");
|
|
417
|
+
var supplied;
|
|
418
|
+
try { supplied = schemaX509.parse(sigCertDer); }
|
|
419
|
+
catch (e) { throw _err("possession/bad-input", "opts.signatureCertificate must be an X.509 certificate", e); }
|
|
420
|
+
if (!_namesCertificate(statement.signer, supplied)) {
|
|
421
|
+
throw _err("possession/signer-mismatch",
|
|
422
|
+
"opts.signatureCertificate is not the certificate the statement's signer names (RFC 9883 sec. 3)");
|
|
423
|
+
}
|
|
424
|
+
} else if (o.signatureCertificate != null) {
|
|
425
|
+
throw _err("possession/bad-input",
|
|
426
|
+
"the statement already carries its signature certificate, so opts.signatureCertificate would name a second one");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
var sigCert = schemaX509.parse(sigCertDer);
|
|
430
|
+
var reasons = [];
|
|
431
|
+
|
|
432
|
+
/** @internal A request carrying more than one extensionRequest attribute is REFUSED before any of it is
|
|
433
|
+
* read, the way `pki.schema.csr.decodeExtensions` refuses it. The lookups below take the first attribute
|
|
434
|
+
* whose extension matches, so a second attribute's `keyUsage` was never seen: a request asking for
|
|
435
|
+
* `keyAgreement` in the first and `digitalSignature` in the second reported the RFC 9883 sec. 6
|
|
436
|
+
* prohibition as not engaged. Which attribute a CA should honor is not stated anywhere, so naming the
|
|
437
|
+
* ambiguity is the only reading that does not silently check one and certify the other. */
|
|
438
|
+
var extensionRequests = 0;
|
|
439
|
+
_forEach(parsed.attributes || [], function (a) {
|
|
440
|
+
if (a.type === oid.byName("extensionRequest")) extensionRequests += 1;
|
|
441
|
+
});
|
|
442
|
+
_assertOneAttribute(extensionRequests, "extensionRequest");
|
|
443
|
+
/** @internal The MUST NOT, decided from the usages the request asks for AND from the subject key itself.
|
|
444
|
+
* The usages alone let a request omit the keyUsage extension and report the prohibition as not engaged,
|
|
445
|
+
* while asking to certify a key that can only ever sign. The reason names which of the two fired, since
|
|
446
|
+
* one message covering both would tell an operator a request named a usage when it named none. */
|
|
447
|
+
var requestedUsage = _requestedExtension(parsed, "keyUsage");
|
|
448
|
+
var askedSignatureUsage = false;
|
|
449
|
+
if (requestedUsage !== null) {
|
|
450
|
+
_forEach(SIGNATURE_USAGES, function (u) { if (requestedUsage[u] === true) askedSignatureUsage = true; });
|
|
451
|
+
}
|
|
452
|
+
var subjectKeyOid = parsed.subjectPublicKeyInfo && parsed.subjectPublicKeyInfo.algorithm
|
|
453
|
+
? parsed.subjectPublicKeyInfo.algorithm.oid : null;
|
|
454
|
+
var subjectKeySignsOnly = _isSignatureOnlyKey(subjectKeyOid);
|
|
455
|
+
var requestsSignatureCertificate = askedSignatureUsage || subjectKeySignsOnly;
|
|
456
|
+
if (askedSignatureUsage) {
|
|
457
|
+
_push(reasons, "the request asks for a signature key usage, and RFC 9883 sec. 6 states the attribute MUST NOT be used to obtain a signature certificate");
|
|
458
|
+
}
|
|
459
|
+
if (subjectKeySignsOnly) {
|
|
460
|
+
_push(reasons, "the request asks to certify a " + (oid.name(subjectKeyOid) || subjectKeyOid) +
|
|
461
|
+
" subject key, which can only sign, and RFC 9883 sec. 4 requires the subjectPKInfo to carry the " +
|
|
462
|
+
"public key for the key establishment algorithm");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** @internal The two SHOULDs, compared and reported. */
|
|
466
|
+
var subjectMatches = false;
|
|
467
|
+
try {
|
|
468
|
+
subjectMatches = guard.name.dnEqual(parsed.subject.rdns, sigCert.subject.rdns,
|
|
469
|
+
_err, "possession/bad-input", "the request subject") === true;
|
|
470
|
+
} catch (_e) { subjectMatches = false; }
|
|
471
|
+
if (!subjectMatches) {
|
|
472
|
+
_push(reasons, "the request subject differs from the signature certificate subject, and RFC 9883 sec. 3 leaves to the certificate policy how a CA determines the two name the same entity");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
var requestedSan = _requestedSan(parsed);
|
|
476
|
+
var certSan = _certSan(sigCert);
|
|
477
|
+
var subjectAltNamesMatch = null;
|
|
478
|
+
if (requestedSan !== null) {
|
|
479
|
+
/** @internal ONE counter over the PAIRS rather than a cap on each list, because the work is the
|
|
480
|
+
* product and BOTH lists come from the request: the names it asks for, and the names in the
|
|
481
|
+
* certificate it embeds. Unbounded, a request repeating one name thousands of times made this
|
|
482
|
+
* scan every certificate name for each of them, and it runs BEFORE the signature is checked and
|
|
483
|
+
* before the certificate's path is validated, so an unauthenticated caller decided how long a CA
|
|
484
|
+
* spent inside one call. MEASURED at the cap: about 70 ms of comparison, against seconds for a
|
|
485
|
+
* 143 KB request without it. The budget is the one `pki.identityMatch` bounds the same nested name
|
|
486
|
+
* comparison with. */
|
|
487
|
+
var sanPairs = guard.limits.counter(
|
|
488
|
+
constants.LIMITS.SAN_MAX_ENTRIES * constants.LIMITS.IDENTITY_MAX_REFERENCES,
|
|
489
|
+
_err, "possession/too-many-names", "subject alternative name comparison");
|
|
490
|
+
subjectAltNamesMatch = _every(requestedSan || [], function (asked) {
|
|
491
|
+
return _sanCarries(certSan, asked, sanPairs);
|
|
492
|
+
});
|
|
493
|
+
if (!subjectAltNamesMatch) {
|
|
494
|
+
_push(reasons, "a subject alternative name the request asks for is not in the signature certificate, and RFC 9883 sec. 3 leaves to the certificate policy how a CA determines the two name the same entity");
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** @internal RFC 9883 sec. 4 makes path validation of the signature certificate a MUST, and a CA
|
|
499
|
+
* normally issues end-entity certificates from an intermediate. The validator is handed an ordered
|
|
500
|
+
* path and does not build one, so without the rest of the chain the only request that can pass is one
|
|
501
|
+
* whose signature certificate the anchor issued directly, and every other valid request is refused
|
|
502
|
+
* for a path that is in fact valid. The order is the validator's own: from the certificate nearest
|
|
503
|
+
* the anchor down to the issuer of the signature certificate, which goes last. */
|
|
504
|
+
var chain = [];
|
|
505
|
+
if (o.intermediates != null) {
|
|
506
|
+
if (!_isArray(o.intermediates)) {
|
|
507
|
+
throw _err("possession/bad-input",
|
|
508
|
+
"opts.intermediates is the rest of the signature certificate's path as an array of certificates, ordered from the one nearest a trust anchor down to the issuer of the signature certificate");
|
|
509
|
+
}
|
|
510
|
+
for (var ci = 0; ci < o.intermediates.length; ci++) {
|
|
511
|
+
guard.list.append(chain, guard.bytes.snapshot(o.intermediates[ci], _err, "possession/bad-input",
|
|
512
|
+
"opts.intermediates[" + ci + "]"));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
guard.list.append(chain, sigCertDer);
|
|
516
|
+
|
|
517
|
+
var signerMaySign = _maySign(sigCert);
|
|
518
|
+
if (!signerMaySign) {
|
|
519
|
+
_push(reasons, "the signature certificate's keyUsage does not permit signing, so its key may not " +
|
|
520
|
+
"authorize a certification request (RFC 5280 sec. 4.2.1.3)");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return _promiseResolve(_Promise, _validatePath(chain, { trustAnchors: o.trustAnchors, time: at }))
|
|
524
|
+
.then(function (pathResult) {
|
|
525
|
+
var pathValidated = pathResult.valid === true;
|
|
526
|
+
if (!pathValidated) _push(reasons, "the signature certificate's certification path is not valid, and RFC 9883 sec. 4 makes that a MUST");
|
|
527
|
+
return _seam.verify(parsed.signatureAlgorithm, parsed.signatureValue,
|
|
528
|
+
sigCert.subjectPublicKeyInfo.bytes, parsed.certificationRequestInfoBytes)
|
|
529
|
+
.then(function (verified) {
|
|
530
|
+
if (verified !== true) _push(reasons, "the request signature does not verify under the signature certificate's public key, and RFC 9883 sec. 4 makes that a MUST");
|
|
531
|
+
return guard.verdict.of({}, {
|
|
532
|
+
valid: verified === true && pathValidated && signerMaySign && !requestsSignatureCertificate &&
|
|
533
|
+
subjectMatches && subjectAltNamesMatch !== false,
|
|
534
|
+
verified: verified === true,
|
|
535
|
+
pathValidated: pathValidated,
|
|
536
|
+
signerMaySign: signerMaySign,
|
|
537
|
+
signer: statement.signer,
|
|
538
|
+
signatureCertificate: sigCertDer,
|
|
539
|
+
subjectMatches: subjectMatches,
|
|
540
|
+
subjectAltNamesMatch: subjectAltNamesMatch,
|
|
541
|
+
requestsSignatureCertificate: requestsSignatureCertificate,
|
|
542
|
+
reason: reasons.length ? _join(reasons, "; ") : undefined,
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** @internal Whether the signature certificate's key may make this signature at all. A certificate confined
|
|
549
|
+
* to `keyAgreement` does not authorize a signature over a certification request, and accepting one lets a
|
|
550
|
+
* key never issued for signing authorize issuance. The accepted set is `pki.cms`'s: a signature over
|
|
551
|
+
* content is authorized by digitalSignature, contentCommitment or nonRepudiation alike. */
|
|
552
|
+
var _maySign = pkix.keyUsagePermits(NS, ["digitalSignature", "contentCommitment", "nonRepudiation"]);
|
|
553
|
+
|
|
554
|
+
/** @internal The subjectAltName a request asks for, or null when it asks for none. */
|
|
555
|
+
function _requestedSan(parsedCsr) {
|
|
556
|
+
var d = _requestedExtension(parsedCsr, "subjectAltName");
|
|
557
|
+
return d === null ? null : (d.names || []);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** @internal The subjectAltName a certificate carries. */
|
|
561
|
+
function _certSan(parsedCert) {
|
|
562
|
+
var ext = _filter(parsedCert.extensions || [], function (e) { return e.oid === oid.byName("subjectAltName"); })[0];
|
|
563
|
+
if (ext === undefined) return [];
|
|
564
|
+
try { return schemaX509.decodeExtension(ext).decoded.names || []; }
|
|
565
|
+
catch (_e) { return []; }
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
module.exports = {
|
|
569
|
+
parse: parse,
|
|
570
|
+
verifyRequest: verifyRequest,
|
|
571
|
+
/** @internal The attribute OID and the statement reader, for the builders and verifiers that
|
|
572
|
+
* compose them; not on the curated surface. */
|
|
573
|
+
OID_SOP: OID_SOP, statementOf: statementOf, namesCertificate: _namesCertificate,
|
|
574
|
+
setEngine: setEngine,
|
|
575
|
+
};
|