@blamejs/pki 0.8.31 → 0.8.48
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 +393 -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 +939 -48
- 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,708 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.relatedCert
|
|
6
|
+
* @nav Signing
|
|
7
|
+
* @title Related certificates
|
|
8
|
+
* @fullname Related certificates: binding a second certificate of the same subject
|
|
9
|
+
* @order 236
|
|
10
|
+
* @slug related-cert
|
|
11
|
+
*
|
|
12
|
+
* @intro
|
|
13
|
+
* One subject, two certificates. A migration to post-quantum signatures leaves a
|
|
14
|
+
* subject holding a classical certificate and a PQC one at the same time, and a
|
|
15
|
+
* protocol that can use both needs each certificate to say the other exists.
|
|
16
|
+
* RFC 9763 does that in two structures: a CSR attribute where the requester
|
|
17
|
+
* proves it holds the certificate it is naming, and a certificate extension where
|
|
18
|
+
* the issuer records which certificate the new one relates to.
|
|
19
|
+
*
|
|
20
|
+
* `requestSignedData` builds the bytes the requester signs. Section 3.1 fixes
|
|
21
|
+
* them as the DER `IssuerAndSerialNumber` followed by the DER `BinaryTime`, and
|
|
22
|
+
* nothing else. `locationInfo` travels in the attribute and is outside those
|
|
23
|
+
* bytes, so a verified proof says the requester holds the certificate and says
|
|
24
|
+
* nothing about where that certificate can be fetched. `pki.csr.sign` takes the
|
|
25
|
+
* whole attribute as `relatedCertRequest` and encodes the first two fields from
|
|
26
|
+
* this same function, so the bytes signed and the bytes emitted cannot drift
|
|
27
|
+
* apart.
|
|
28
|
+
*
|
|
29
|
+
* `verifyRequest` checks that proof against the certificate `certID` names,
|
|
30
|
+
* through the engine the certificate path uses, so the advertised algorithm is
|
|
31
|
+
* bound to the key the same way. Handing it any other certificate throws: a proof
|
|
32
|
+
* checked against a certificate the request did not name answers a different
|
|
33
|
+
* question than the one asked. The structure names no algorithm, so the identifier
|
|
34
|
+
* comes from the same resolver the signing verbs use, which is what reaches every
|
|
35
|
+
* algorithm they sign with including an RSASSA-PSS key whose parameters belong to
|
|
36
|
+
* its identifier.
|
|
37
|
+
*
|
|
38
|
+
* `certificateHash` and `matchesCertificate` are the extension side.
|
|
39
|
+
* `pki.x509.sign` takes `extensions.relatedCertificate`, and the form handed the
|
|
40
|
+
* related certificate computes the digest here, so no caller can name one
|
|
41
|
+
* algorithm while carrying the output of another. The algorithm defaults the way
|
|
42
|
+
* section 4.1 directs, to the hash the related certificate's own signature OID
|
|
43
|
+
* indicates; a certificate signed with Ed25519 or ML-DSA indicates none, and that
|
|
44
|
+
* throws instead of falling back to a hash the document does not name. `sha1` is
|
|
45
|
+
* refused: a chosen-prefix collision on a certificate is a demonstrated attack.
|
|
46
|
+
*
|
|
47
|
+
* The extension is emitted non-critical, which section 4.1 asks for at SHOULD NOT,
|
|
48
|
+
* and a critical one still parses: `pki.lint.certificate` with the `rfc9763`
|
|
49
|
+
* profile grades it, alongside the section's MUST that the extension appear only
|
|
50
|
+
* in the end-entity certificate of a chain.
|
|
51
|
+
*
|
|
52
|
+
* @card Build the bytes a requester signs, verify the proof it made, and compute or
|
|
53
|
+
* check the digest that names a related certificate (RFC 9763).
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
var asn1 = require("./asn1-der");
|
|
57
|
+
var oid = require("./oid");
|
|
58
|
+
var pkix = require("./schema-pkix");
|
|
59
|
+
var schema = require("./schema-engine");
|
|
60
|
+
var schemaX509 = require("./schema-x509");
|
|
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
|
+
var signScheme = require("./sign-scheme");
|
|
66
|
+
var C = require("./constants");
|
|
67
|
+
|
|
68
|
+
var RelatedCertError = frameworkError.RelatedCertError;
|
|
69
|
+
var b = asn1.build;
|
|
70
|
+
var _create = intrinsic.create;
|
|
71
|
+
var _assign = intrinsic.assign;
|
|
72
|
+
var _forEach = intrinsic.forEach;
|
|
73
|
+
var _isBuffer = intrinsic.isBuffer;
|
|
74
|
+
var _bufferFrom = intrinsic.bufferFrom;
|
|
75
|
+
var _bufferConcat = intrinsic.bufferConcat;
|
|
76
|
+
var _BigInt = intrinsic.BigInt;
|
|
77
|
+
var _floor = intrinsic.floor;
|
|
78
|
+
var _String = intrinsic.String;
|
|
79
|
+
var _keys = intrinsic.keys;
|
|
80
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
81
|
+
var _charAt = intrinsic.uncurry(String.prototype.charAt);
|
|
82
|
+
var _fromCharCode = String.fromCharCode;
|
|
83
|
+
|
|
84
|
+
function _err(code, message, cause) { return new RelatedCertError(code, message, cause); }
|
|
85
|
+
|
|
86
|
+
var _seam = seam.makeSeam("related-cert", RelatedCertError, "relatedcert/bad-input");
|
|
87
|
+
|
|
88
|
+
var NS = pkix.makeNS("relatedcert", RelatedCertError, oid);
|
|
89
|
+
var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
|
|
90
|
+
/** @internal The shared `IssuerAndSerialNumber`, used to read back the identifier this module just
|
|
91
|
+
* encoded so the identity comparison has the decoded `Name` the RFC 5280 sec. 7.1 rules need. It reads
|
|
92
|
+
* our OWN bytes, not a caller's, which is what makes a second decode safe here: the caller's record was
|
|
93
|
+
* read once on the way in and these bytes are the result. */
|
|
94
|
+
var ISSUER_AND_SERIAL = pkix.issuerAndSerialNumber(NS, { code: "relatedcert/bad-input", what: "certID" });
|
|
95
|
+
function _certIdParts(der) {
|
|
96
|
+
return schema.walk(ISSUER_AND_SERIAL, asn1.decode(der), NS).result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var DIGEST_LENGTHS = pkix.digestLengths();
|
|
100
|
+
var SIG_OID_DIGEST = pkix.signatureDigestNames(oid);
|
|
101
|
+
|
|
102
|
+
/** @internal The digests this module will bind a certificate identity to. sha1 is registered and
|
|
103
|
+
* decodable, and is absent here: a chosen-prefix collision on a certificate is a demonstrated
|
|
104
|
+
* attack, so a value naming it is refused before anything is computed. A caller reading a hashValue a
|
|
105
|
+
* producer wrote under sha1 still gets the algorithm name off the parsed extension. */
|
|
106
|
+
var ALLOWED_DIGESTS = _assign(_create(null), {
|
|
107
|
+
sha256: 1, sha384: 1, sha512: 1, "sha3-256": 1, "sha3-512": 1,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/** @internal The two digests the substitution below resolves against. They only have to differ. */
|
|
111
|
+
var PROBE_DIGESTS = ["sha256", "sha512"];
|
|
112
|
+
/** @internal Whether naming a digest would change the algorithm identifier this key signs with, asked
|
|
113
|
+
* by running the substitution instead of listing which key kinds admit a choice. An EdDSA or ML-DSA
|
|
114
|
+
* key resolves to one identifier whatever digest is asked for, so naming one is a request that would
|
|
115
|
+
* be dropped; an RSA or ECDSA key resolves to a different identifier per digest. A key kind added to
|
|
116
|
+
* the signing side is covered here without a second table naming it. */
|
|
117
|
+
/** @internal The digest an id-RSASSA-PSS SPKI pins, or null when the key pins none or is not that
|
|
118
|
+
* algorithm. `signScheme.pssSpkiPinnedHash` assumes its argument IS an id-RSASSA-PSS key and complains
|
|
119
|
+
* about the parameters of anything else, so the algorithm is checked before it is asked. */
|
|
120
|
+
function _pinnedDigestName(certLike) {
|
|
121
|
+
var spki = certLike && certLike.subjectPublicKeyInfo;
|
|
122
|
+
var keyOid = spki && spki.algorithm ? spki.algorithm.oid : null;
|
|
123
|
+
if (keyOid === null || keyOid !== oid.byName("rsassaPss")) return null;
|
|
124
|
+
var pinned = signScheme.pssSpkiPinnedHash(certLike, _schemeErr);
|
|
125
|
+
if (pinned === null || pinned === undefined) return null;
|
|
126
|
+
/** @internal Returned in THIS module's vocabulary. The scheme layer answers in the WebCrypto spelling
|
|
127
|
+
* ("SHA-256") while a digestAlgorithm here is the lowercase registry name ("sha256"), so comparing the
|
|
128
|
+
* two directly reports a disagreement between two spellings of one hash. The match is made by
|
|
129
|
+
* normalizing both rather than by a second table that could drift from the first. */
|
|
130
|
+
var want = _normalizeDigestSpelling(pinned);
|
|
131
|
+
var names = _keys(ALLOWED_DIGESTS);
|
|
132
|
+
for (var i = 0; i < names.length; i++) {
|
|
133
|
+
if (_normalizeDigestSpelling(names[i]) === want) return names[i];
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
function _normalizeDigestSpelling(name) {
|
|
138
|
+
var out = "";
|
|
139
|
+
var s = _String(name);
|
|
140
|
+
for (var i = 0; i < s.length; i++) {
|
|
141
|
+
var c = _charCodeAt(s, i);
|
|
142
|
+
if (c === 0x2d) continue;
|
|
143
|
+
out += c >= 0x41 && c <= 0x5a ? _fromCharCode(c + 32) : _charAt(s, i);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function _digestChangesTheAlgorithm(certLike) {
|
|
149
|
+
/** @internal A key that RESTRICTS its hash is asked before it is probed. An id-RSASSA-PSS SPKI may pin
|
|
150
|
+
* one, and that key refuses every other digest outright, so the second probe threw before the digest the
|
|
151
|
+
* caller asked for was ever resolved and a valid proof under the key's own hash was reported as an
|
|
152
|
+
* unsupported algorithm. A pinned key admits no choice, which is the answer the probe computes for
|
|
153
|
+
* everything else, so it is returned directly rather than reached through a request the key forbids. */
|
|
154
|
+
if (_pinnedDigestName(certLike) !== null) return false;
|
|
155
|
+
var first = _resolveScheme(certLike, PROBE_DIGESTS[0]).sigAlgId;
|
|
156
|
+
var second = _resolveScheme(certLike, PROBE_DIGESTS[1]).sigAlgId;
|
|
157
|
+
return !guard.crypto.constantTimeEqual(first, second);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** @internal `combinedRsaSig` selects the combined `sha256WithRSAEncryption` form an ASN.1 signature
|
|
161
|
+
* field carries, which is what `pki.csr.sign` and `pki.x509.sign` emit; without it an RSA key resolves
|
|
162
|
+
* to the bare `rsaEncryption` a CMS signerInfo uses beside a separate digest field. */
|
|
163
|
+
function _resolveScheme(certLike, digest) {
|
|
164
|
+
return signScheme.resolveSignScheme(certLike, { combinedRsaSig: true, digestAlgorithm: digest }, true, _schemeErr);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @internal `serialNumberHex` is here because a certID read back by a parser carries it beside the
|
|
168
|
+
* serial, and this encoder takes both a caller's description and a parsed record. It is redundant
|
|
169
|
+
* with `serialNumber`, which is the authority the encoder reads, so it is checked against that rather
|
|
170
|
+
* than accepted and ignored: a value disagreeing with the serial beside it describes two different
|
|
171
|
+
* certificates, and only one of them would be signed. */
|
|
172
|
+
var KNOWN_CERT_ID_KEYS = _assign(_create(null), { issuer: 1, serialNumber: 1, serialNumberHex: 1 });
|
|
173
|
+
var KNOWN_PREIMAGE_KEYS = _assign(_create(null), { certID: 1, requestTime: 1 });
|
|
174
|
+
var KNOWN_VERIFY_OPTS = _assign(_create(null), { digestAlgorithm: 1, signatureAlgorithm: 1, maxAge: 1, at: 1 });
|
|
175
|
+
|
|
176
|
+
/** @internal The freshness check RFC 9763 sec. 3.2 requires of the certification authority: "MUST check
|
|
177
|
+
* that the BinaryTime indicated in the requestTime field is sufficiently fresh." The WINDOW is not the
|
|
178
|
+
* toolkit's to choose, the same clause saying "sufficient freshness is defined by local policy and is out
|
|
179
|
+
* of the scope of this document", so `maxAge` carries it and there is no default: the proof covers the
|
|
180
|
+
* certID and the requestTime alone (sec. 3.1), so a captured attribute replayed into a fresh request for
|
|
181
|
+
* an attacker's key verifies forever without it, and a verifier that cannot perform a MUST check must not
|
|
182
|
+
* answer true. `at` is the instant judged against, for a caller reading a request recorded earlier. */
|
|
183
|
+
function _assertFresh(requestTime, o) {
|
|
184
|
+
if (o.maxAge === undefined) {
|
|
185
|
+
throw _err("relatedcert/no-freshness-policy",
|
|
186
|
+
"pki.relatedCert.verifyRequest needs opts.maxAge, the seconds a requestTime may be old: RFC 9763 " +
|
|
187
|
+
"sec. 3.2 requires the certification authority to check that it is sufficiently fresh and leaves " +
|
|
188
|
+
"the window to local policy, and the proof covers the certID and the requestTime alone, so a " +
|
|
189
|
+
"captured attribute replayed into a new request verifies forever without one");
|
|
190
|
+
}
|
|
191
|
+
var maxAge = guard.range.authoredInteger(o.maxAge, _err, "relatedcert/bad-input", "opts.maxAge");
|
|
192
|
+
if (maxAge < 0n) throw _err("relatedcert/bad-input", "opts.maxAge must not be negative");
|
|
193
|
+
var at = o.at === undefined ? new intrinsic.Date() : o.at;
|
|
194
|
+
if (!guard.time.isDate(at)) throw _err("relatedcert/bad-input", "opts.at must be a Date");
|
|
195
|
+
guard.time.assertValid(at, _err, "relatedcert/bad-input", "opts.at");
|
|
196
|
+
var atMs = guard.time.instantOf(at);
|
|
197
|
+
var atSecs = _BigInt(_floor(atMs / C.TIME.seconds(1)));
|
|
198
|
+
/** @internal Both ends are refused. A requestTime older than the window is the replay the clause is
|
|
199
|
+
* about; one in the FUTURE is a request whose own clock disagrees with the verifier's, and admitting it
|
|
200
|
+
* would let a producer set a time that stays fresh for as long as they chose. */
|
|
201
|
+
if (requestTime + maxAge < atSecs) {
|
|
202
|
+
throw _err("relatedcert/stale-request",
|
|
203
|
+
"the requestTime is older than the " + maxAge + " seconds opts.maxAge allows (RFC 9763 sec. 3.2)");
|
|
204
|
+
}
|
|
205
|
+
if (requestTime > atSecs) {
|
|
206
|
+
throw _err("relatedcert/stale-request",
|
|
207
|
+
"the requestTime is in the future, so it names no instant this verifier can judge as fresh");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
var _NAME_SCHEMA = pkix.name(NS);
|
|
212
|
+
|
|
213
|
+
/** @internal A Name the caller supplied as DER, held to being one before it reaches a preimage the
|
|
214
|
+
* caller will sign. Bytes that are not a Name would be signed as whatever they are, and the proof
|
|
215
|
+
* would then attest to a structure no verifier can read back as an issuer. */
|
|
216
|
+
function _nameDer(value, what) {
|
|
217
|
+
var der = guard.bytes.snapshot(value, _err, "relatedcert/bad-input", what);
|
|
218
|
+
if (der.length === 0) throw _err("relatedcert/bad-input", what + " must not be empty");
|
|
219
|
+
var node;
|
|
220
|
+
try { node = asn1.decode(der); }
|
|
221
|
+
catch (e) { throw _err("relatedcert/bad-input", what + " must be a DER-encoded Name", e); }
|
|
222
|
+
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children) {
|
|
223
|
+
throw _err("relatedcert/bad-input", what + " must be a Name, which is a SEQUENCE of RelativeDistinguishedName");
|
|
224
|
+
}
|
|
225
|
+
/** @internal An EMPTY Name is a readable Name and names no issuer, so it is refused here where the
|
|
226
|
+
* schema below is content with it: `30 00` is a SEQUENCE OF nothing. A certID identifies a
|
|
227
|
+
* certificate, and this toolkit's own signer refuses an empty issuer, so accepting it would sign an
|
|
228
|
+
* identifier for a certificate `pki.x509.sign` would not emit. An empty SUBJECT is a different
|
|
229
|
+
* question and stays legal, a certificate being allowed to carry its identity in a
|
|
230
|
+
* subjectAltName. */
|
|
231
|
+
if (node.children.length === 0) {
|
|
232
|
+
throw _err("relatedcert/bad-input", what + " is an empty Name, which identifies no issuer");
|
|
233
|
+
}
|
|
234
|
+
/** @internal The outer tag is not the type. A SEQUENCE holding anything at all passed this check, so
|
|
235
|
+
* bytes no verifier can read back as an issuer were signed as whatever they are: `30 02 05 00` is a
|
|
236
|
+
* SEQUENCE carrying a NULL, and `pki.csr.sign` under `profile: "none"` emitted a request this
|
|
237
|
+
* toolkit's own parser then refuses with `csr/bad-rdn`. The node is read with the SHARED X.509 Name
|
|
238
|
+
* schema, which is the reader on the other side, so what is signed here is what a verifier can
|
|
239
|
+
* read back. */
|
|
240
|
+
try { schema.walk(_NAME_SCHEMA, node, NS); }
|
|
241
|
+
catch (e) {
|
|
242
|
+
if (e instanceof RelatedCertError) throw e;
|
|
243
|
+
throw _err("relatedcert/bad-input", what + " is not a readable Name: a SEQUENCE of " +
|
|
244
|
+
"RelativeDistinguishedName, each a SET of AttributeTypeAndValue", e);
|
|
245
|
+
}
|
|
246
|
+
return der;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** @internal BinaryTime ::= INTEGER (0..MAX), "the number of seconds, excluding leap seconds, after
|
|
250
|
+
* midnight UTC, January 1, 1970" (RFC 6019 sec. 2). A Date is read to whole seconds, which is the
|
|
251
|
+
* unit the type counts. A fractional number is refused: a truncation the caller did not ask for
|
|
252
|
+
* produces a signature over a time it did not name. */
|
|
253
|
+
function _binaryTime(value, what) {
|
|
254
|
+
if (intrinsic.types.isDate(value)) {
|
|
255
|
+
guard.time.assertValid(value, _err, "relatedcert/bad-input", what);
|
|
256
|
+
var secs = _floor(guard.time.instantOf(value) / C.TIME.seconds(1));
|
|
257
|
+
if (secs < 0) throw _err("relatedcert/bad-input", what + " must not precede the epoch (BinaryTime is INTEGER (0..MAX))");
|
|
258
|
+
return _BigInt(secs);
|
|
259
|
+
}
|
|
260
|
+
/** @internal A fractional number reaches the shared authored-integer guard and is refused there, so
|
|
261
|
+
* a truncation the caller did not ask for cannot become a signature over a time it did not name. */
|
|
262
|
+
var n = guard.range.authoredInteger(value, _err, "relatedcert/bad-input", what);
|
|
263
|
+
if (n < 0n) throw _err("relatedcert/bad-input", what + " must not be negative (BinaryTime is INTEGER (0..MAX))");
|
|
264
|
+
return n;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function _serialNumber(value, what) {
|
|
268
|
+
var n = guard.range.authoredInteger(value, _err, "relatedcert/bad-input", what);
|
|
269
|
+
if (n < 1n) throw _err("relatedcert/bad-input", what + " must be a positive integer (RFC 5280 sec. 4.1.2.2)");
|
|
270
|
+
return n;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** @internal The IssuerAndSerialNumber DER of a certID the caller described, or of one a parser
|
|
274
|
+
* returned. A parsed record carries its issuer as a Name record whose `bytes` are the raw DER, so
|
|
275
|
+
* both shapes reach the same two reads and the encoder has one input form. */
|
|
276
|
+
function _certIdDer(certID) {
|
|
277
|
+
if (certID === null || typeof certID !== "object") {
|
|
278
|
+
throw _err("relatedcert/bad-input", "certID must be an object { issuer, serialNumber }");
|
|
279
|
+
}
|
|
280
|
+
var snap = guard.identifier.snapshotOptions(certID, KNOWN_CERT_ID_KEYS);
|
|
281
|
+
guard.identifier.assertKnownKeys(certID, KNOWN_CERT_ID_KEYS, _err, "relatedcert/bad-input",
|
|
282
|
+
"certID has an unknown field: ");
|
|
283
|
+
var issuer = snap.issuer;
|
|
284
|
+
if (issuer !== null && typeof issuer === "object" && !_isBuffer(issuer)) {
|
|
285
|
+
/** @internal A Name record from a parser. `bytes` is read ONCE: a presence test followed by a second
|
|
286
|
+
* read is two reads of one member, and an accessor could satisfy the test and then supply other
|
|
287
|
+
* bytes to encode. */
|
|
288
|
+
var issuerBytes = issuer.bytes;
|
|
289
|
+
if (issuerBytes !== undefined) issuer = issuerBytes;
|
|
290
|
+
}
|
|
291
|
+
var serial = _serialNumber(snap.serialNumber, "certID.serialNumber");
|
|
292
|
+
if (snap.serialNumberHex !== undefined && snap.serialNumberHex !== null) {
|
|
293
|
+
if (typeof snap.serialNumberHex !== "string") throw _err("relatedcert/bad-input", "certID.serialNumberHex must be a hex string");
|
|
294
|
+
var fromHex;
|
|
295
|
+
try { fromHex = _BigInt("0x" + snap.serialNumberHex); }
|
|
296
|
+
catch (e) { throw _err("relatedcert/bad-input", "certID.serialNumberHex must be a hex string", e); }
|
|
297
|
+
if (fromHex !== serial) {
|
|
298
|
+
throw _err("relatedcert/bad-input", "certID.serialNumberHex does not name the same serial as certID.serialNumber");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return b.sequence([b.raw(_nameDer(issuer, "certID.issuer")), b.integer(serial)]);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* @primitive pki.relatedCert.requestSignedData
|
|
306
|
+
* @signature pki.relatedCert.requestSignedData(spec) -> Buffer
|
|
307
|
+
* @since 0.8.42
|
|
308
|
+
* @status stable
|
|
309
|
+
* @spec RFC 9763 sec. 3.1, RFC 6019 sec. 2
|
|
310
|
+
* @defends related-certificate-proof-replay (CWE-347)
|
|
311
|
+
* @related pki.relatedCert.verifyRequest, pki.csr.sign
|
|
312
|
+
*
|
|
313
|
+
* The bytes a `relatedCertRequest` proof is made over. RFC 9763 sec. 3.1 states them exactly: "the
|
|
314
|
+
* signature field contains a digital signature over the concatenation of DER-encoded
|
|
315
|
+
* IssuerAndSerialNumber and BinaryTime", signed with the key of the certificate `certID` names. `spec`
|
|
316
|
+
* takes `certID` (`{ issuer, serialNumber }`, where `issuer` is a `Name` as DER or a parsed `Name`
|
|
317
|
+
* record and `serialNumber` is a positive integer) and `requestTime` (a `Date`, or whole seconds since
|
|
318
|
+
* the epoch as a number or a bigint).
|
|
319
|
+
*
|
|
320
|
+
* `locationInfo` is outside these bytes, which is the scope the clause draws: a verified proof says
|
|
321
|
+
* the requester holds the certificate, and says nothing about where that certificate can be fetched.
|
|
322
|
+
* A caller that acts on `locationInfo` is acting on an unauthenticated field.
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* var issuer = pki.x509.parseDn("CN=Example CA, O=Example");
|
|
326
|
+
* var signMe = pki.relatedCert.requestSignedData({
|
|
327
|
+
* certID: { issuer: issuer.bytes, serialNumber: 42n },
|
|
328
|
+
* requestTime: new Date("2027-01-15T00:00:00Z"),
|
|
329
|
+
* });
|
|
330
|
+
* signMe.length > 0; // -> true; sign these bytes with the key of certificate 42
|
|
331
|
+
*/
|
|
332
|
+
function requestSignedData(spec) {
|
|
333
|
+
if (spec === null || typeof spec !== "object") {
|
|
334
|
+
throw _err("relatedcert/bad-input", "pki.relatedCert.requestSignedData takes a { certID, requestTime } object");
|
|
335
|
+
}
|
|
336
|
+
var snap = guard.identifier.snapshotOptions(spec, KNOWN_PREIMAGE_KEYS);
|
|
337
|
+
guard.identifier.assertKnownKeys(spec, KNOWN_PREIMAGE_KEYS, _err, "relatedcert/bad-input",
|
|
338
|
+
"pki.relatedCert.requestSignedData has an unknown option: ");
|
|
339
|
+
return _preimageFrom(_certIdDer(snap.certID), snap.requestTime);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** @internal The preimage built from an ALREADY-ENCODED certID. The verifier encodes the identifier once
|
|
343
|
+
* and passes the same bytes here and to the binding comparison, so a caller whose `certID` or whose
|
|
344
|
+
* `issuer` / `serialNumber` inside it answers per read cannot have one identifier hashed and another
|
|
345
|
+
* compared. That difference is what would let a proof made for one certificate verify against a second
|
|
346
|
+
* one sharing its public key. */
|
|
347
|
+
function _preimageFrom(certIdDer, requestTime) {
|
|
348
|
+
return _bufferConcat([certIdDer, b.integer(_binaryTime(requestTime, "requestTime"))]);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** @internal The digest name a caller supplied, held to the set this module will bind an identity to.
|
|
352
|
+
* A name outside it is refused whether the reason is that the algorithm is unregistered, that this
|
|
353
|
+
* build cannot compute it, or that it is sha1. */
|
|
354
|
+
function _digestName(value, what) {
|
|
355
|
+
if (typeof value !== "string") throw _err("relatedcert/bad-input", what + " must be a digest algorithm name");
|
|
356
|
+
if (!ALLOWED_DIGESTS[value]) {
|
|
357
|
+
throw _err("relatedcert/bad-input", what + " must name one of sha256 / sha384 / sha512 / sha3-256 / sha3-512; got " + guard.text.showValue(value));
|
|
358
|
+
}
|
|
359
|
+
return value;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** @internal A certificate the caller handed in, parsed once. The full parse is what holds the bytes
|
|
363
|
+
* to being a certificate at all, so a hash is never taken over something that merely starts like one. */
|
|
364
|
+
function _parseCert(certDer, what) {
|
|
365
|
+
var der = guard.bytes.snapshot(certDer, _err, "relatedcert/bad-input", what);
|
|
366
|
+
try { return { der: der, parsed: schemaX509.parse(der) }; }
|
|
367
|
+
catch (e) { throw _err("relatedcert/bad-input", what + " must be a DER-encoded X.509 certificate", e); }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @primitive pki.relatedCert.certificateHash
|
|
372
|
+
* @signature pki.relatedCert.certificateHash(certificate, digestAlgorithm?) -> { hashAlgorithm, hashValue }
|
|
373
|
+
* @since 0.8.42
|
|
374
|
+
* @status stable
|
|
375
|
+
* @spec RFC 9763 sec. 4.1
|
|
376
|
+
* @defends related-certificate-substitution (CWE-347)
|
|
377
|
+
* @related pki.relatedCert.matchesCertificate, pki.x509.sign
|
|
378
|
+
*
|
|
379
|
+
* The `RelatedCertificate` value naming a certificate: the digest of the whole certificate, with the
|
|
380
|
+
* algorithm that produced it. RFC 9763 sec. 4.1 hashes "the entire related certificate", so the input
|
|
381
|
+
* is the certificate's full DER rather than its `tbsCertificate`.
|
|
382
|
+
*
|
|
383
|
+
* With no `digestAlgorithm`, the algorithm is derived the way sec. 4.1 directs: "If there is a hash
|
|
384
|
+
* algorithm explicitly indicated by the related certificate's signature OID (e.g.,
|
|
385
|
+
* ecdsa-with-SHA512), that hash algorithm SHOULD also be used for this extension." A certificate
|
|
386
|
+
* whose signature OID indicates no hash, such as one signed with Ed25519 or ML-DSA, leaves that
|
|
387
|
+
* derivation without an answer and throws `relatedcert/no-digest`; name the algorithm to resolve it.
|
|
388
|
+
*
|
|
389
|
+
* `sha256`, `sha384`, `sha512`, `sha3-256` and `sha3-512` are accepted. `sha1` is refused, including
|
|
390
|
+
* when it is what the certificate's own signature OID indicates.
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* var kp = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
|
|
394
|
+
* var spki = await pki.key.export(kp.publicKey, { format: "der" });
|
|
395
|
+
* var related = await pki.x509.sign({ subject: "CN=Held", subjectPublicKey: spki, serialNumber: 42n,
|
|
396
|
+
* notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z") }, { key: kp.privateKey });
|
|
397
|
+
* pki.relatedCert.certificateHash(related).hashAlgorithm; // -> "sha256", from this certificate's signature OID
|
|
398
|
+
* pki.relatedCert.certificateHash(related, "sha512").hashValue.length; // -> 64
|
|
399
|
+
*/
|
|
400
|
+
function certificateHash(certificate, digestAlgorithm) {
|
|
401
|
+
var c = _parseCert(certificate, "the related certificate");
|
|
402
|
+
var name;
|
|
403
|
+
if (digestAlgorithm === undefined || digestAlgorithm === null) {
|
|
404
|
+
var indicated = SIG_OID_DIGEST[c.parsed.signatureAlgorithm.oid];
|
|
405
|
+
if (indicated === undefined) {
|
|
406
|
+
throw _err("relatedcert/no-digest",
|
|
407
|
+
"the related certificate's signature algorithm (" + (c.parsed.signatureAlgorithm.name || c.parsed.signatureAlgorithm.oid) +
|
|
408
|
+
") indicates no hash algorithm, so name one (RFC 9763 sec. 4.1)");
|
|
409
|
+
}
|
|
410
|
+
if (!ALLOWED_DIGESTS[indicated]) {
|
|
411
|
+
throw _err("relatedcert/bad-input",
|
|
412
|
+
"the related certificate's signature algorithm indicates " + indicated +
|
|
413
|
+
", which is not used to bind a certificate identity here, so name a digest algorithm");
|
|
414
|
+
}
|
|
415
|
+
name = indicated;
|
|
416
|
+
} else {
|
|
417
|
+
name = _digestName(digestAlgorithm, "digestAlgorithm");
|
|
418
|
+
}
|
|
419
|
+
return { hashAlgorithm: name, hashValue: guard.crypto.digest(name, c.der) };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** @internal A digest value with the algorithm that produced it, held to the set this module acts on
|
|
423
|
+
* and to that algorithm's output length. Three callers read a value this way: the extension builder
|
|
424
|
+
* before emitting one, the comparison below before trusting one, and the parser through the length
|
|
425
|
+
* table they share, so a length rule cannot hold on one side of the round trip and not the other. */
|
|
426
|
+
function digestValue(hashAlgorithm, hashValue) {
|
|
427
|
+
var name = _digestName(hashAlgorithm, "the hashAlgorithm");
|
|
428
|
+
var value = guard.bytes.snapshot(hashValue, _err, "relatedcert/bad-input", "the hashValue");
|
|
429
|
+
var want = DIGEST_LENGTHS[name];
|
|
430
|
+
if (want !== undefined && value.length !== want) {
|
|
431
|
+
throw _err("relatedcert/bad-input", "the hashValue is " + value.length + " octets, and " + name + " produces " + want);
|
|
432
|
+
}
|
|
433
|
+
if (value.length === 0) throw _err("relatedcert/bad-input", "the hashValue must not be empty");
|
|
434
|
+
return { hashAlgorithm: name, hashValue: value };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* @primitive pki.relatedCert.matchesCertificate
|
|
439
|
+
* @signature pki.relatedCert.matchesCertificate(relatedCertificate, certificate) -> boolean
|
|
440
|
+
* @since 0.8.42
|
|
441
|
+
* @status stable
|
|
442
|
+
* @spec RFC 9763 sec. 4.1
|
|
443
|
+
* @defends related-certificate-substitution (CWE-347)
|
|
444
|
+
* @related pki.relatedCert.certificateHash, pki.schema.x509.parse
|
|
445
|
+
*
|
|
446
|
+
* Whether a parsed `relatedCertificate` extension value names the given certificate. Takes the value
|
|
447
|
+
* a `pki.schema.x509.parse` extension carries, `{ hashAlgorithm, hashValue }`, and the candidate
|
|
448
|
+
* certificate's DER, recomputes the digest under the algorithm the value names, and compares in
|
|
449
|
+
* constant time.
|
|
450
|
+
*
|
|
451
|
+
* A value this build cannot act on throws. A `false` from an algorithm nobody computed says the
|
|
452
|
+
* certificate does not match, when what happened is that no comparison was made. Only a digest that
|
|
453
|
+
* was computed and disagreed returns `false`.
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* var kp = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
|
|
457
|
+
* var spki = await pki.key.export(kp.publicKey, { format: "der" });
|
|
458
|
+
* var related = await pki.x509.sign({ subject: "CN=Held", subjectPublicKey: spki, serialNumber: 42n,
|
|
459
|
+
* notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z") }, { key: kp.privateKey });
|
|
460
|
+
* var leaf = await pki.x509.sign({ subject: "CN=New Key", subjectPublicKey: spki, serialNumber: 43n,
|
|
461
|
+
* notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z"),
|
|
462
|
+
* extensions: { relatedCertificate: { relatedCertificate: related } } }, { key: kp.privateKey });
|
|
463
|
+
* var ext = pki.schema.x509.parse(leaf).extensions
|
|
464
|
+
* .filter(function (e) { return e.oid === pki.oid.byName("relatedCertificate"); })[0];
|
|
465
|
+
* pki.relatedCert.matchesCertificate(pki.schema.x509.decodeExtension(ext).decoded, related); // -> true
|
|
466
|
+
*/
|
|
467
|
+
function matchesCertificate(relatedCertificate, certificate) {
|
|
468
|
+
if (relatedCertificate === null || typeof relatedCertificate !== "object") {
|
|
469
|
+
throw _err("relatedcert/bad-input", "the relatedCertificate value must be an object { hashAlgorithm, hashValue }");
|
|
470
|
+
}
|
|
471
|
+
var v = digestValue(relatedCertificate.hashAlgorithm, relatedCertificate.hashValue);
|
|
472
|
+
var c = _parseCert(certificate, "the candidate certificate");
|
|
473
|
+
/** @internal Through the captured digest. Capturing `createHash` alone leaves `update` and `digest`
|
|
474
|
+
* on the live hash prototype, so a replacement decided what the comparison was OVER: measured with
|
|
475
|
+
* two certificates differing only in serial number, an `update` that hashed the first turned the
|
|
476
|
+
* second's verdict from false to true, which is the binding this extension exists to make. */
|
|
477
|
+
return guard.crypto.constantTimeEqual(guard.crypto.digest(v.hashAlgorithm, c.der), v.hashValue);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** @internal The AlgorithmIdentifier the proof's key signs with, resolved through the same scheme
|
|
481
|
+
* resolver every signing verb here uses, so the identifier a proof is verified under is the one this
|
|
482
|
+
* toolkit would have emitted for that key. Assembling it by hand from a key OID and a digest name is
|
|
483
|
+
* what produced an identifier with the parameters field ABSENT, which RFC 4055 sec. 5 requires to be
|
|
484
|
+
* an explicit NULL for the RSA algorithms; every RSA proof then failed to verify.
|
|
485
|
+
*
|
|
486
|
+
* The KEY decides the algorithm, not the certificate's signatureAlgorithm: that field names what the
|
|
487
|
+
* ISSUER signed the certificate with, whose key may be of another kind entirely, while the proof is
|
|
488
|
+
* made by the SUBJECT's key. The DIGEST still comes from that field, which is the relation sec. 4.1
|
|
489
|
+
* names. */
|
|
490
|
+
function _proofAlgorithm(parsed, digestAlgorithm) {
|
|
491
|
+
var keyName = parsed.subjectPublicKeyInfo.algorithm.name || parsed.subjectPublicKeyInfo.algorithm.oid;
|
|
492
|
+
var digest = null;
|
|
493
|
+
if (digestAlgorithm === undefined || digestAlgorithm === null) {
|
|
494
|
+
var indicated = SIG_OID_DIGEST[parsed.signatureAlgorithm.oid];
|
|
495
|
+
if (indicated !== undefined) {
|
|
496
|
+
digest = _digestName(indicated, "the digest the related certificate's signature algorithm indicates");
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
digest = _digestName(digestAlgorithm, "digestAlgorithm");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
var certLike = { subjectPublicKeyInfo: parsed.subjectPublicKeyInfo };
|
|
503
|
+
var takesADigest;
|
|
504
|
+
try { takesADigest = _digestChangesTheAlgorithm(certLike); }
|
|
505
|
+
catch (e) {
|
|
506
|
+
throw _err("relatedcert/unsupported-algorithm",
|
|
507
|
+
"a proof made by a " + keyName + " key is not resolved here, so name signatureAlgorithm as a DER AlgorithmIdentifier",
|
|
508
|
+
e);
|
|
509
|
+
}
|
|
510
|
+
if (!takesADigest) {
|
|
511
|
+
/** @internal A key that admits no choice comes in two kinds, and they answer a named digest
|
|
512
|
+
* differently. A key whose algorithm carries its own digest (EdDSA, ML-DSA) can be handed no digest at
|
|
513
|
+
* all, so naming one is a request that would be dropped. A key whose SPKI PINS a digest has exactly
|
|
514
|
+
* one, and naming THAT one agrees with the key: refusing it would reject the only digest the key can
|
|
515
|
+
* be used with, while naming a different one is the real disagreement and is named as such. */
|
|
516
|
+
var pinnedDigest = _pinnedDigestName(certLike);
|
|
517
|
+
if (digestAlgorithm !== undefined && digestAlgorithm !== null) {
|
|
518
|
+
if (pinnedDigest === null) {
|
|
519
|
+
throw _err("relatedcert/bad-input",
|
|
520
|
+
"a " + keyName + " key admits no digest choice, so digestAlgorithm must be omitted");
|
|
521
|
+
}
|
|
522
|
+
if (digest !== pinnedDigest) {
|
|
523
|
+
throw _err("relatedcert/bad-input", "the " + keyName + " key is restricted to " + pinnedDigest +
|
|
524
|
+
", so digestAlgorithm must be that or omitted, not " + digest);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return _readAlgorithmIdentifier(
|
|
528
|
+
_resolveScheme(certLike, pinnedDigest === null ? PROBE_DIGESTS[0] : pinnedDigest).sigAlgId, keyName);
|
|
529
|
+
}
|
|
530
|
+
if (digest === null) {
|
|
531
|
+
throw _err("relatedcert/no-digest",
|
|
532
|
+
"the related certificate's signature algorithm indicates no hash algorithm, so name digestAlgorithm (RFC 9763 sec. 4.1)");
|
|
533
|
+
}
|
|
534
|
+
var scheme;
|
|
535
|
+
try { scheme = _resolveScheme(certLike, digest); }
|
|
536
|
+
catch (e) {
|
|
537
|
+
throw _err("relatedcert/unsupported-algorithm",
|
|
538
|
+
"no signature algorithm pairs a " + keyName + " key with " + digest, e);
|
|
539
|
+
}
|
|
540
|
+
return _readAlgorithmIdentifier(scheme.sigAlgId, keyName);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function _schemeErr(kind, message, cause) { return new RelatedCertError("relatedcert/" + kind, message, cause); }
|
|
544
|
+
|
|
545
|
+
/** @internal The resolver hands back the AlgorithmIdentifier as DER, and the seam reads a decoded
|
|
546
|
+
* record, so it is read through the shared sub-schema: the parameters field then reaches the engine as
|
|
547
|
+
* the resolver wrote it. */
|
|
548
|
+
function _readAlgorithmIdentifier(der, keyName) {
|
|
549
|
+
try { return schema.walk(ALGORITHM_IDENTIFIER, asn1.decode(der), NS).result; }
|
|
550
|
+
catch (e) {
|
|
551
|
+
throw _err("relatedcert/unsupported-algorithm",
|
|
552
|
+
"the signature algorithm resolved for a " + keyName + " key is not a readable AlgorithmIdentifier", e);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** @internal An AlgorithmIdentifier the caller named as DER, read through the shared sub-schema so it
|
|
557
|
+
* is held to the same shape a parsed one is. */
|
|
558
|
+
function _namedAlgorithm(value) {
|
|
559
|
+
var der = guard.bytes.snapshot(value, _err, "relatedcert/bad-input", "signatureAlgorithm");
|
|
560
|
+
try { return schema.walk(ALGORITHM_IDENTIFIER, asn1.decode(der), NS).result; }
|
|
561
|
+
catch (e) { throw _err("relatedcert/bad-input", "signatureAlgorithm must be a DER-encoded AlgorithmIdentifier", e); }
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* @primitive pki.relatedCert.verifyRequest
|
|
566
|
+
* @signature pki.relatedCert.verifyRequest(requesterCertificate, certificate, opts?) -> Promise<boolean>
|
|
567
|
+
* @since 0.8.42
|
|
568
|
+
* @status stable
|
|
569
|
+
* @spec RFC 9763 sec. 3.1
|
|
570
|
+
* @defends related-certificate-proof-forgery (CWE-347)
|
|
571
|
+
* @related pki.relatedCert.requestSignedData, pki.schema.csr.parse, pki.path.validate
|
|
572
|
+
*
|
|
573
|
+
* Verify the proof of possession in a `relatedCertRequest` CSR attribute. `requesterCertificate` is
|
|
574
|
+
* the value `pki.schema.csr.parse` carries on that attribute, and `certificate` is the DER of the
|
|
575
|
+
* certificate its `certID` names. Returns `true` when the signature over the bytes
|
|
576
|
+
* `pki.relatedCert.requestSignedData` describes verifies under that certificate's subject key, and
|
|
577
|
+
* `false` when a signature was checked and did not verify.
|
|
578
|
+
*
|
|
579
|
+
* The certificate handed in must be the one `certID` names: an issuer and serial that do not match
|
|
580
|
+
* throw `relatedcert/cert-mismatch`, since verifying a proof against a certificate the request did
|
|
581
|
+
* not name answers a different question than the one asked.
|
|
582
|
+
*
|
|
583
|
+
* The structure carries no algorithm identifier, so one is derived: the key decides the algorithm,
|
|
584
|
+
* resolved through the same resolver `pki.x509.sign` and `pki.csr.sign` use, and the digest comes from
|
|
585
|
+
* the hash the certificate's signature OID indicates, which is the relation sec. 4.1 names for the
|
|
586
|
+
* sibling extension. Every algorithm those verbs sign with is therefore reached, including an
|
|
587
|
+
* RSASSA-PSS key whose parameters are part of its algorithm identifier. `opts.digestAlgorithm` names
|
|
588
|
+
* the digest instead, and a key whose algorithm fixes its own digest, such as an EdDSA or ML-DSA key,
|
|
589
|
+
* refuses that option instead of ignoring it. `opts.signatureAlgorithm` takes a DER
|
|
590
|
+
* `AlgorithmIdentifier` outright. A key that cannot sign, such as an ML-KEM or X25519 key, throws
|
|
591
|
+
* `relatedcert/unsupported-algorithm`.
|
|
592
|
+
*
|
|
593
|
+
* What the proof covers is `certID` and `requestTime` only. `locationInfo` is outside it, so a
|
|
594
|
+
* `true` here does not authenticate where the certificate may be fetched.
|
|
595
|
+
*
|
|
596
|
+
* The signature is read the way every other ASN.1 signature field is, so an ECDSA proof is the DER
|
|
597
|
+
* `SEQUENCE { r, s }` of RFC 5480 sec. 2, not the fixed-width `r || s` a WebCrypto `sign` returns.
|
|
598
|
+
* Handing over the fixed-width form resolves `false` rather than throwing, because a signature that
|
|
599
|
+
* does not decode is a signature that does not verify; the example re-encodes it.
|
|
600
|
+
*
|
|
601
|
+
* `opts.maxAge` is REQUIRED, in seconds. RFC 9763 sec. 3.2 makes the freshness check the certification
|
|
602
|
+
* authority's: it "MUST check that the BinaryTime indicated in the requestTime field is sufficiently
|
|
603
|
+
* fresh", and the same clause leaves the window to local policy, so there is no window this verb could
|
|
604
|
+
* choose for you. Without one a captured attribute copied into a new request for another key verifies
|
|
605
|
+
* forever, the proof covering the certID and the requestTime alone, so a missing policy is
|
|
606
|
+
* `relatedcert/no-freshness-policy` rather than a default. A `requestTime` older than the window, or in the
|
|
607
|
+
* future, is `relatedcert/stale-request`.
|
|
608
|
+
*
|
|
609
|
+
* @opts maxAge REQUIRED. The seconds a `requestTime` may be old, which RFC 9763 sec. 3.2 leaves to local policy. A whole non-negative integer.
|
|
610
|
+
* @opts at The instant to judge freshness against, as a `Date`. The current time when omitted; pass one to read a request recorded earlier.
|
|
611
|
+
* @opts digestAlgorithm The digest for the proof: `"sha256"` / `"sha384"` / `"sha512"` / `"sha3-256"` / `"sha3-512"`. Derived from the certificate's signature OID when omitted, and refused for a key whose algorithm fixes its own digest.
|
|
612
|
+
* @opts signatureAlgorithm A DER `AlgorithmIdentifier` naming the proof's signature algorithm outright, in place of the derivation.
|
|
613
|
+
*
|
|
614
|
+
* @example
|
|
615
|
+
* var kp = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
|
|
616
|
+
* var spki = await pki.key.export(kp.publicKey, { format: "der" });
|
|
617
|
+
* var held = await pki.x509.sign({ subject: "CN=Held", subjectPublicKey: spki, serialNumber: 42n,
|
|
618
|
+
* notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z") }, { key: kp.privateKey });
|
|
619
|
+
* var parsed = pki.schema.x509.parse(held);
|
|
620
|
+
* var certID = { issuer: parsed.issuer.bytes, serialNumber: parsed.serialNumber };
|
|
621
|
+
* var signMe = pki.relatedCert.requestSignedData({ certID: certID, requestTime: 1800000000 });
|
|
622
|
+
* // An ECDSA proof goes into the BIT STRING as DER SEQUENCE { r, s }. WebCrypto returns the
|
|
623
|
+
* // fixed-width r || s instead, so it is re-encoded here.
|
|
624
|
+
* var raw = Buffer.from(await pki.webcrypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, kp.privateKey, signMe));
|
|
625
|
+
* var half = raw.length / 2;
|
|
626
|
+
* var proof = pki.asn1.build.sequence([
|
|
627
|
+
* pki.asn1.build.integer(BigInt("0x" + raw.subarray(0, half).toString("hex"))),
|
|
628
|
+
* pki.asn1.build.integer(BigInt("0x" + raw.subarray(half).toString("hex"))),
|
|
629
|
+
* ]);
|
|
630
|
+
* var csr = await pki.csr.sign({ subject: "CN=New Key", subjectPublicKey: spki,
|
|
631
|
+
* relatedCertRequest: { certID: certID, requestTime: 1800000000,
|
|
632
|
+
* locationInfo: ["https://certs.example/held.cer"], signature: proof } }, { key: kp.privateKey });
|
|
633
|
+
* var attr = pki.schema.csr.parse(csr).attributes
|
|
634
|
+
* .filter(function (a) { return a.type === pki.oid.byName("relatedCertRequest"); })[0];
|
|
635
|
+
* // The window is the certification authority's policy, so it is named: five minutes here, judged
|
|
636
|
+
* // against the instant the request was made, which is what a CA reading a recorded request does.
|
|
637
|
+
* await pki.relatedCert.verifyRequest(attr.relatedCertRequest, held,
|
|
638
|
+
* { maxAge: 300, at: new Date(1800000060 * 1000) }); // -> true
|
|
639
|
+
*/
|
|
640
|
+
function verifyRequest(requesterCertificate, certificate, opts) {
|
|
641
|
+
/** @internal Every refusal reaches the caller as a rejection, so a verb that returns a promise never
|
|
642
|
+
* also throws past it: a caller handling one path and not the other would miss the refusals. */
|
|
643
|
+
return guard.async.deferred(function () { return _verifyRequest(requesterCertificate, certificate, opts); });
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function _verifyRequest(requesterCertificate, certificate, opts) {
|
|
647
|
+
if (requesterCertificate === null || typeof requesterCertificate !== "object") {
|
|
648
|
+
throw _err("relatedcert/bad-input", "pki.relatedCert.verifyRequest takes the parsed relatedCertRequest attribute value");
|
|
649
|
+
}
|
|
650
|
+
var o = guard.identifier.snapshotOptions(opts, KNOWN_VERIFY_OPTS);
|
|
651
|
+
guard.identifier.assertKnownKeys(opts, KNOWN_VERIFY_OPTS, _err, "relatedcert/bad-input",
|
|
652
|
+
"pki.relatedCert.verifyRequest has an unknown option: ");
|
|
653
|
+
|
|
654
|
+
var c = _parseCert(certificate, "the related certificate");
|
|
655
|
+
/** @internal The preimage is rebuilt from the request's own fields, so what is verified is what the
|
|
656
|
+
* request says, not what the certificate says: a mismatch between the two is reported below, and
|
|
657
|
+
* hashing the certificate's values would cover it over. */
|
|
658
|
+
var named = _certIdDer(requesterCertificate.certID);
|
|
659
|
+
/** @internal Read ONCE into the value both the freshness check and the preimage use, so a request whose
|
|
660
|
+
* `requestTime` is an accessor cannot present a fresh instant to the check and a different one to the
|
|
661
|
+
* bytes the proof is verified over. */
|
|
662
|
+
var requestSecs = _binaryTime(requesterCertificate.requestTime, "requestTime");
|
|
663
|
+
_assertFresh(requestSecs, o);
|
|
664
|
+
var preimage = _bufferConcat([named, b.integer(requestSecs)]);
|
|
665
|
+
/** @internal The IDENTITY check is by name, not by bytes. RFC 9763 sec. 3.2 says the CA "extracts the
|
|
666
|
+
* IssuerAndSerialNumber from the indicated certificate and compares this value against the
|
|
667
|
+
* IssuerAndSerialNumber provided in the certID field", and `issuer` is a `Name`, so RFC 5280 sec. 7.1
|
|
668
|
+
* governs: two encodings of one name are one name. Comparing the DER refused a conforming request, a
|
|
669
|
+
* certificate holding its issuer CN as a PrintableString against a request naming the same CN as a
|
|
670
|
+
* UTF8String. The PREIMAGE above is still the request's own bytes, which is what sec. 3.1 says the
|
|
671
|
+
* signature covers, so only the comparison folds encodings and the signed bytes are untouched.
|
|
672
|
+
* The comparison is no longer constant-time and does not need to be: an issuer name and a serial
|
|
673
|
+
* number are both carried in the certificate and in the request, so there is no secret for an early
|
|
674
|
+
* exit to leak. The constant-time compare remains where it answers about a digest. */
|
|
675
|
+
var namedParts = _certIdParts(named);
|
|
676
|
+
if (namedParts.serialNumber !== c.parsed.serialNumber ||
|
|
677
|
+
!guard.name.dnEqual(namedParts.issuer.rdns, c.parsed.issuer.rdns, _err,
|
|
678
|
+
"relatedcert/cert-mismatch", "the certID issuer")) {
|
|
679
|
+
throw _err("relatedcert/cert-mismatch",
|
|
680
|
+
"the certificate given is not the one certID names (RFC 9763 sec. 3.2)");
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
var sigAlg = o.signatureAlgorithm !== undefined && o.signatureAlgorithm !== null
|
|
684
|
+
? _namedAlgorithm(o.signatureAlgorithm)
|
|
685
|
+
: _proofAlgorithm(c.parsed, o.digestAlgorithm);
|
|
686
|
+
|
|
687
|
+
var sig = requesterCertificate.signature;
|
|
688
|
+
/** @internal `bytes` and `unusedBits` are read ONCE each, for the reason given at `certID.issuer`. */
|
|
689
|
+
var sigBytes = sig !== null && typeof sig === "object" && !_isBuffer(sig) ? sig.bytes : undefined;
|
|
690
|
+
if (sigBytes !== undefined) {
|
|
691
|
+
sig = { unusedBits: sig.unusedBits, bytes: guard.bytes.snapshot(sigBytes, _err, "relatedcert/bad-input", "the proof signature") };
|
|
692
|
+
} else {
|
|
693
|
+
sig = { unusedBits: 0, bytes: guard.bytes.snapshot(sig, _err, "relatedcert/bad-input", "the proof signature") };
|
|
694
|
+
}
|
|
695
|
+
if (sig.bytes.length === 0) throw _err("relatedcert/bad-input", "the proof signature must not be empty");
|
|
696
|
+
|
|
697
|
+
return _seam.verify(sigAlg, sig, c.parsed.subjectPublicKeyInfo.bytes, preimage);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
module.exports = {
|
|
701
|
+
requestSignedData: requestSignedData,
|
|
702
|
+
verifyRequest: verifyRequest,
|
|
703
|
+
certificateHash: certificateHash,
|
|
704
|
+
matchesCertificate: matchesCertificate,
|
|
705
|
+
/** @internal Read a digest value the way the comparison reads one; not on the curated surface. */
|
|
706
|
+
digestValue: digestValue,
|
|
707
|
+
setEngine: _seam.setEngine,
|
|
708
|
+
};
|