@blamejs/pki 0.6.14 → 0.6.16
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 +27 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +140 -286
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/cmp-session.js
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
-
//
|
|
5
|
-
// @internal -- the pki.cmp.session implementation. The operator-facing @module pki.cmp home is
|
|
4
|
+
// @internal
|
|
6
5
|
// lib/cmp-build.js; this file adds the @primitive pki.cmp.session block and re-exports the cmp message
|
|
7
|
-
// surface (build / transfer / wellKnownUrl / verify) so the whole pki.cmp namespace wires through here
|
|
8
|
-
// (index.js requires this file as `cmp`).
|
|
9
|
-
//
|
|
10
|
-
// pki.cmp.session -- the stateful CMP enrollment-transaction orchestrator (the pki.acme.client analogue).
|
|
11
|
-
// It composes the shipped message layer (build / transfer / verify) into a single enroll(request): mint a
|
|
12
|
-
// stable transactionID, build + protect + transfer a request, verify every response's protection before
|
|
13
|
-
// reading its body, chain the nonces (recipNonce echoes the peer's senderNonce, a fresh senderNonce per
|
|
14
|
-
// request) and the transactionID (RFC 9810 sec. 5.1.1 anti-replay / anti-interleave), interpret the
|
|
15
|
-
// CertResponse PKIStatus (grant -> extract the cert, waiting -> a bounded pollReq/pollRep loop, rejection
|
|
16
|
-
// -> a terminal verdict), and confirm (certConf -> pkiConf, unless implicitConfirm was granted). The
|
|
17
|
-
// single invariant is transfer -> verify (fail-closed) -> only then read the body off the verdict.
|
|
18
6
|
|
|
19
|
-
var cmp = require("./cmp-verify");
|
|
7
|
+
var cmp = require("./cmp-verify");
|
|
20
8
|
var asn1 = require("./asn1-der");
|
|
21
9
|
var oid = require("./oid");
|
|
22
10
|
var x509 = require("./schema-x509");
|
|
@@ -36,25 +24,17 @@ var retryAfter = require("./http-retry-after");
|
|
|
36
24
|
|
|
37
25
|
var CmpError = frameworkError.CmpError;
|
|
38
26
|
function _err(code, message, cause) { return new CmpError(code, message, cause); }
|
|
39
|
-
var OID_IMPLICIT_CONFIRM = oid.byName("implicitConfirm");
|
|
27
|
+
var OID_IMPLICIT_CONFIRM = oid.byName("implicitConfirm");
|
|
40
28
|
|
|
41
|
-
// The certification-path build/validate engine, injected by path-validate (the crl/ocsp/cmp-verify seam) so
|
|
42
|
-
// the session can validate the issued leaf certificate's signature + chain; x509.parse is structural only.
|
|
43
29
|
var _engine = null;
|
|
44
30
|
function setEngine(engine) { _engine = engine; }
|
|
45
31
|
|
|
46
|
-
// The signature algorithms whose OID conveys no message hash: EdDSA (the hash is the scheme) and the FIPS
|
|
47
|
-
// PQC signatures (ML-DSA / SLH-DSA, internally hashed). Only these may take the certConf SHA-256 + explicit
|
|
48
|
-
// hashAlg fallback (RFC 9810 sec. 5.3.18); any other indeterminate/non-signature AlgorithmIdentifier (e.g.
|
|
49
|
-
// rsaEncryption, an ML-KEM OID) is refused. Resolved to a dotted-OID set at load (never a runtime lookup).
|
|
50
32
|
var HASHLESS_SIG_OIDS = {};
|
|
51
33
|
(function () {
|
|
52
34
|
var names = ["Ed25519", "Ed448", "id-ml-dsa-44", "id-ml-dsa-65", "id-ml-dsa-87"];
|
|
53
35
|
["sha2", "shake"].forEach(function (h) { ["128s", "128f", "192s", "192f", "256s", "256f"].forEach(function (s) { names.push("id-slh-dsa-" + h + "-" + s); }); });
|
|
54
36
|
names.forEach(function (n) { var o = oid.byName(n); if (o) HASHLESS_SIG_OIDS[o] = 1; });
|
|
55
37
|
})();
|
|
56
|
-
// Hash-conveying signature algorithms -> the certConf hash, dispatched by the IMMUTABLE OID (never the mutable
|
|
57
|
-
// display name, which pki.oid.register can override). Resolved to a dotted-OID map at load.
|
|
58
38
|
var SIG_OID_TO_HASH = {};
|
|
59
39
|
[["sha256WithRSAEncryption", "SHA-256"], ["sha384WithRSAEncryption", "SHA-384"], ["sha512WithRSAEncryption", "SHA-512"],
|
|
60
40
|
["ecdsaWithSHA256", "SHA-256"], ["ecdsaWithSHA384", "SHA-384"], ["ecdsaWithSHA512", "SHA-512"]].forEach(function (row) {
|
|
@@ -62,20 +42,11 @@ var SIG_OID_TO_HASH = {};
|
|
|
62
42
|
});
|
|
63
43
|
var OID_RSASSA_PSS = oid.byName("rsassaPss");
|
|
64
44
|
var OID_RSA_ENCRYPTION = oid.byName("rsaEncryption");
|
|
65
|
-
// A CRL states the point it speaks for in this extension (RFC 5280 sec. 5.2.5); taken at load, by
|
|
66
|
-
// identifier, so a later pki.oid.register cannot rename the extension the scope is read from.
|
|
67
45
|
var OID_IDP = oid.byName("issuingDistributionPoint");
|
|
68
|
-
// The two extensions that decide whether a certificate can act as an authority, taken at load and
|
|
69
|
-
// matched by identifier. pki.oid.register replaces an OID's display name, so a test asking whether
|
|
70
|
-
// an extension is NAMED basicConstraints reads a present one as absent after a rename, and every
|
|
71
|
-
// valid root key update is refused.
|
|
72
46
|
var OID_BASIC_CONSTRAINTS = oid.byName("basicConstraints");
|
|
73
47
|
var OID_KEY_USAGE = oid.byName("keyUsage");
|
|
74
|
-
// Message-digest OID -> the WebCrypto digest name, for the RSASSA-PSS hashAlgorithm parameter, dispatched by
|
|
75
|
-
// the immutable OID (never oid.name, which pki.oid.register can rename). Resolved to a dotted-OID map at load.
|
|
76
48
|
var HASH_OID_TO_DIGEST = {};
|
|
77
49
|
[["sha256", "SHA-256"], ["sha384", "SHA-384"], ["sha512", "SHA-512"]].forEach(function (row) { var o = oid.byName(row[0]); if (o) HASH_OID_TO_DIGEST[o] = row[1]; });
|
|
78
|
-
// A composite signature's declared prehash (COMPOSITE_ALGS[oid].ph) -> the certConf hashAlg field value.
|
|
79
50
|
var COMPOSITE_PH_HASHALG = { "SHA-256": "sha256", "SHA-384": "sha384", "SHA-512": "sha512" };
|
|
80
51
|
|
|
81
52
|
var KNOWN_SESSION_OPTS = {
|
|
@@ -85,34 +56,15 @@ var KNOWN_SESSION_OPTS = {
|
|
|
85
56
|
acceptCert: 1, senderKID: 1, recipKID: 1, expectedSender: 1,
|
|
86
57
|
};
|
|
87
58
|
|
|
88
|
-
// The read-only transcript retains at most this many responses' worth of payload bytes (a response is bounded by
|
|
89
|
-
// the transport's maxResponseBytes). Bounding it PROPORTIONALLY to the per-response cap keeps the ceiling above
|
|
90
|
-
// any single legitimate response while still preventing a polling loop from multiplying a padded-response flood
|
|
91
|
-
// across legs into memory exhaustion; a tightened maxResponseBytes tightens the transcript ceiling in step.
|
|
92
59
|
var TRANSCRIPT_RETAIN_RESPONSES = 2;
|
|
93
60
|
var DEFAULT_MAX_POLLS = 20;
|
|
94
|
-
var DEFAULT_MAX_TOTAL_WAIT = retryAfter.MAX_RETRY_AFTER_SECONDS;
|
|
95
|
-
var DEFAULT_CERT_REQ_ID = 0;
|
|
96
|
-
var P10CR_CERT_REQ_ID = -1;
|
|
97
|
-
var ENROLL_ARMS = { ir: 1, cr: 1, kur: 1, p10cr: 1 };
|
|
98
|
-
// The response body arm each enrollment request is answered by (RFC 9810 sec. 5.3.2/5.3.4): ir->ip, cr/p10cr
|
|
99
|
-
// ->cp, kur->kup. A response on a different cert-response arm than the request is misrouted, never confirmed.
|
|
61
|
+
var DEFAULT_MAX_TOTAL_WAIT = retryAfter.MAX_RETRY_AFTER_SECONDS;
|
|
62
|
+
var DEFAULT_CERT_REQ_ID = 0;
|
|
63
|
+
var P10CR_CERT_REQ_ID = -1;
|
|
64
|
+
var ENROLL_ARMS = { ir: 1, cr: 1, kur: 1, p10cr: 1 };
|
|
100
65
|
var RESPONSE_ARM = { ir: "ip", cr: "cp", kur: "kup", p10cr: "cp", rr: "rp", genm: "genp" };
|
|
101
|
-
// A pollReq for a non-enrollment operation refers to the WHOLE message rather than to a
|
|
102
|
-
// CertResponse element inside it. RFC 9483 sec. 4.4: an entity "sends a poll request with certReqId
|
|
103
|
-
// 0 if referring to the CertResponse element contained in the ip/cp/kup message, else -1 to refer
|
|
104
|
-
// to the whole message". A revocation or a support message has no CertResponse element, so the
|
|
105
|
-
// second half of that sentence is the one that applies and the id is -1. The 0 is not a default for
|
|
106
|
-
// polling; it is the id OF an element these operations do not carry.
|
|
107
66
|
var WHOLE_MESSAGE_CERT_REQ_ID = -1;
|
|
108
67
|
|
|
109
|
-
// The four support messages RFC 9483 sec. 4.3 defines, keyed by the name a caller asks for.
|
|
110
|
-
// `requestOid` and `responseOid` differ only for rootCaCert (sec. 4.3.2), which is the reason
|
|
111
|
-
// the pair is a table rather than one field: a response echoing the request OID is a different
|
|
112
|
-
// operation's answer, and only the table can say so. `value` is absent for an operation whose
|
|
113
|
-
// request carries no infoValue (sec. 4.3.1 and sec. 4.3.3 both say it MUST be absent).
|
|
114
|
-
// The table is null-prototype: the key comes from a caller's request object, so a plain literal
|
|
115
|
-
// would answer `toString` or `constructor` with an inherited value and dispatch on it.
|
|
116
68
|
var INFO_OPS = Object.assign(Object.create(null), {
|
|
117
69
|
caCerts: { name: "caCerts", requestOid: "caCerts", responseOid: "caCerts", value: false, read: "readCaCerts" },
|
|
118
70
|
rootCaCert: { name: "rootCaCert", requestOid: "rootCaCert", responseOid: "rootCaKeyUpdate", value: "cert", read: "readRootCaKeyUpdate" },
|
|
@@ -120,45 +72,9 @@ var INFO_OPS = Object.assign(Object.create(null), {
|
|
|
120
72
|
crlUpdate: { name: "crlUpdate", requestOid: "crlStatusList", responseOid: "crls", value: "crlStatus", read: "readCrls" },
|
|
121
73
|
});
|
|
122
74
|
var KNOWN_REVOKE_KEYS = { certificate: 1, certDetails: 1, reason: 1 };
|
|
123
|
-
// The CRLReason names a revocation may state, read off the same shared table pki.crl.sign and
|
|
124
|
-
// pki.cmp.build encode from, so the three cannot drift apart on what a reason is called. Checked at
|
|
125
|
-
// this door so a mistyped name reads as the request-shape error it is, before the transport engages.
|
|
126
|
-
// A keySpec algId (RFC 9483 sec. 4.3.3, RFC 9480 sec. 2.16) MUST "give an algorithm other than RSA",
|
|
127
|
-
// and that is the ONLY constraint the responder is held to: the control names a public-key type an
|
|
128
|
-
// entity MAY be asked to generate, one AttributeTypeAndValue per algorithm the CA supports, and the
|
|
129
|
-
// entity picks one it can do. So the check refuses an algId ONLY when it names an RSA algorithm (whose
|
|
130
|
-
// requirement is stated with rsaKeyLen instead); every other value -- a non-RSA algorithm this toolkit
|
|
131
|
-
// knows, or an OID it does not recognize -- is surfaced to the caller. Refusing an unrecognized
|
|
132
|
-
// algorithm would break interop with a CA offering one newer than this registry AND drop the other
|
|
133
|
-
// algorithms the same keySpec offers, since one bad element fails the whole exchange.
|
|
134
|
-
//
|
|
135
|
-
// "RSA" is decided by OID FAMILY, not a hand-list. An RSA-DEDICATED arc -- one whose members are RSA --
|
|
136
|
-
// is matched by its prefix, so a standardized member this registry has not named is caught with the ones
|
|
137
|
-
// it has: PKCS#1 (rsaEncryption, PSS, OAEP, every sha*/md*WithRSAEncryption), the TeleTrusT rsaSignature
|
|
138
|
-
// arc (RSA with SHA-1 and the RIPEMD family), the TeleTrusT signatureScheme arc (the ISO/IEC 9796-2 RSA
|
|
139
|
-
// signature-with-message-recovery schemes -- deterministic 1.3.36.3.4.2 and randomized .4.3 -- plus ISO/IEC
|
|
140
|
-
// 9796-1 on .4.1), and the BSI TR-03110 id-TA-RSA arc (EAC Terminal Authentication, RSA v1.5/PSS over
|
|
141
|
-
// SHA-1/256/512). Each prefix is the arc a registered member sits directly under, read from the registry,
|
|
142
|
-
// so no dotted-decimal literal appears here and pki.oid.register cannot move it. The strict-descendant
|
|
143
|
-
// match skips an arc NODE itself, so a scheme's bare arc node (the 9796 1.3.36.3.4.1/.2/.3 nodes) is named
|
|
144
|
-
// off-arc below; the generic signatureScheme PARENT (1.3.36.3.4) and the sibling authentication arc
|
|
145
|
-
// (1.3.36.3.5, whose .5.3 children are ECC) are NOT RSA-dedicated, so neither is prefix-matched. The PKCS#1
|
|
146
|
-
// arc also carries two auxiliary identifiers that are NOT RSA algorithms --
|
|
147
|
-
// mgf1 and pSpecified, the mask-generation and OAEP-label parameters -- so those are excluded from the
|
|
148
|
-
// match by name (RSA_ARC_EXCLUDE) and surfaced. RSA also sits off those arcs on ones it SHARES with non-RSA
|
|
149
|
-
// algorithms, where no prefix can isolate it, so those are listed by name: id-rsa-kem (S/MIME) and
|
|
150
|
-
// id-kem-rsa (ISO 18033-2); the RSASSA-PKCS1-v1_5-with-SHA-3 set on the NIST signature arc, shared with
|
|
151
|
-
// ML-DSA and SLH-DSA; RSASSA-PSS-with-SHAKE (RFC 8692) on the PKIX arc; every RSA identifier the OIW SIG
|
|
152
|
-
// assigned on the OIW Secsig arc (the md4/md5-WithRSA, rsaSignature, mdc2/sha-WithRSA, rsaKeyTransport,
|
|
153
|
-
// and md2/md5/sha1-WithRSASignature identifiers), which is shared with DES, DSA, DH, and the hashes so no
|
|
154
|
-
// prefix isolates it; and the X.500 directory RSA OIDs on the 2.5.8 arc -- rsa (2.5.8.1.1), distinct from
|
|
155
|
-
// PKCS#1 rsaEncryption, and sqMod-nWithRSA / mdc2WithRSA on the signatureAlgorithm sub-arc; and the
|
|
156
|
-
// Chinese GM/T sm3WithRSAEncryption (1.2.156.10197.1.504), an RSA-over-SM3 signature on the SM arc it
|
|
157
|
-
// shares with SM2 and SM3. A composite signature naming RSA as one half is a composite key, not an RSA
|
|
158
|
-
// one, and lives on the PKIX arc, so it is outside all of these.
|
|
159
75
|
var RSA_ARCS = ["rsaEncryption", "rsaSignatureWithripemd160", "id-TA-RSA-v1-5-SHA-256",
|
|
160
76
|
"sigS-ISO9796-1-DFUE", "sigS-ISO9796-2Withrsa", "sigS-ISO9796-2rndWithrsa"].map(function (n) {
|
|
161
|
-
return oid.toArcs(oid.byName(n)).slice(0, -1);
|
|
77
|
+
return oid.toArcs(oid.byName(n)).slice(0, -1);
|
|
162
78
|
});
|
|
163
79
|
var RSA_OFF_ARC = Object.create(null);
|
|
164
80
|
["id-rsa-kem", "id-kem-rsa",
|
|
@@ -168,50 +84,25 @@ var RSA_OFF_ARC = Object.create(null);
|
|
|
168
84
|
"md4WithRSA", "md5WithRSA", "md4WithRSAEncryption", "rsaSignature", "mdc2WithRSASignature",
|
|
169
85
|
"shaWithRSAEncryption", "rsaKeyTransport",
|
|
170
86
|
"md2WithRSASignature", "md5WithRSASignature", "sha1WithRSASignature",
|
|
171
|
-
"rsa",
|
|
172
|
-
"sqMod-nWithRSA", "mdc2WithRSA",
|
|
173
|
-
"sm3WithRSAEncryption",
|
|
174
|
-
// The TeleTrusT signatureScheme (ISO/IEC 9796-2) sub-arcs are prefix-matched in RSA_ARCS, but the strict-
|
|
175
|
-
// descendant match skips each arc NODE itself, so the three bare nodes are named here (1.3.36.3.4.1/.2/.3).
|
|
87
|
+
"rsa",
|
|
88
|
+
"sqMod-nWithRSA", "mdc2WithRSA",
|
|
89
|
+
"sm3WithRSAEncryption",
|
|
176
90
|
"sigS-ISO9796-1", "sigS-ISO9796-2", "sigS-ISO9796-2rnd",
|
|
177
91
|
].forEach(function (n) { var d = oid.byName(n); if (d) RSA_OFF_ARC[d] = 1; });
|
|
178
|
-
// The PKCS#1 arc carries two identifiers that are NOT RSA algorithms: mgf1 (the mask-generation function
|
|
179
|
-
// RSA-PSS and RSA-OAEP parametrize) and pSpecified (the OAEP label source). The prefix match would sweep
|
|
180
|
-
// them in, but a keySpec algId naming one states no RSA key requirement -- the entity does not generate an
|
|
181
|
-
// "mgf1 key" -- so they are surfaced, not refused, and are excluded from the arc match by name.
|
|
182
92
|
var RSA_ARC_EXCLUDE = Object.create(null);
|
|
183
93
|
["mgf1", "pSpecified"].forEach(function (n) { var d = oid.byName(n); if (d) RSA_ARC_EXCLUDE[d] = 1; });
|
|
184
94
|
|
|
185
95
|
var CRL_REASON_NAMES = Object.create(null);
|
|
186
96
|
Object.keys(constants.NAMES.CRL_REASON).forEach(function (v) { CRL_REASON_NAMES[constants.NAMES.CRL_REASON[v]] = 1; });
|
|
187
97
|
|
|
188
|
-
// PKIStatus codes (RFC 9810 sec. 5.2.3): 0 accepted, 1 grantedWithMods, 2 rejection, 3 waiting.
|
|
189
98
|
function _isGranted(code) { return code === 0 || code === 1; }
|
|
190
|
-
// The PKIStatus name surfaced to an acceptance policy so it can distinguish a clean grant from a modified one.
|
|
191
99
|
var PKI_STATUS_NAMES = { 0: "accepted", 1: "grantedWithMods", 2: "rejection", 3: "waiting" };
|
|
192
100
|
|
|
193
|
-
// certReqId equality across the encode side (a number or bigint in the request) and the decode side (a BigInt
|
|
194
|
-
// off the wire): compare as BigInts so two distinct large ids above 2^53 never collide under Number rounding.
|
|
195
101
|
function _certReqIdEq(a, b) { return a != null && b != null && BigInt(a) === BigInt(b); }
|
|
196
102
|
|
|
197
|
-
// Normalize a certificate-list option (the constructor + cmp.verify accept a lone Buffer / PEM OR an array) to
|
|
198
|
-
// an array the path engine requires; a copy so a caller's array is never mutated by the caPubs append.
|
|
199
103
|
function _asCertList(v) { return v == null ? [] : (Array.isArray(v) ? v.slice() : [v]); }
|
|
200
104
|
|
|
201
|
-
// Append session-supplied pool material (a CA-delivered caPubs, a cached protection-signer chain part-derived
|
|
202
|
-
// from the response's unsigned extraCerts) to a `base` pool (the caller's own intermediates) up to the path
|
|
203
|
-
// builder's candidate ceiling, deduped against each other AND the base's Buffer entries so a copy of an existing
|
|
204
|
-
// candidate never spends a slot. The base is never truncated (if it alone exceeds the ceiling, that is a genuine
|
|
205
|
-
// config error path.build reports). So neither a legitimate caPubs nor a meddler's extraCerts flood can push a
|
|
206
|
-
// valid caller pool over the ceiling and fail an otherwise-valid grant. Used for both the response signer path
|
|
207
|
-
// (cmp.verify) and the issued-leaf path (path.build), the two places session material joins a caller pool.
|
|
208
105
|
function _boundedPool(base, added) {
|
|
209
|
-
// Dedup the base (the priority pool) first: duplicate copies would otherwise inflate its length, drive the
|
|
210
|
-
// remaining room to zero, and evict genuinely needed added material even though the distinct candidate count is
|
|
211
|
-
// small (path.build dedups internally, so dropping exact duplicates is behavior-preserving). An uncanonicalizable
|
|
212
|
-
// entry is kept as-is (path.build judges it) and cannot dedup an added cert. The base holds the priority material
|
|
213
|
-
// (a signed response's own delivered issuers + the cached chain, or the grant's caPubs) so it is never truncated;
|
|
214
|
-
// the added caller pool fills the remaining room to the candidate ceiling.
|
|
215
106
|
var ceiling = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES;
|
|
216
107
|
var seen = Object.create(null), distinctBase = [];
|
|
217
108
|
base.forEach(function (c) { var k = _certIdentity(c); if (k == null) { distinctBase.push(c); return; } if (!seen[k]) { seen[k] = 1; distinctBase.push(c); } });
|
|
@@ -221,82 +112,35 @@ function _boundedPool(base, added) {
|
|
|
221
112
|
added.forEach(function (c) { var k = _certIdentity(c); if (k != null && seen[k]) return; if (k != null) seen[k] = 1; useful.push(c); });
|
|
222
113
|
return distinctBase.concat(useful.slice(0, room));
|
|
223
114
|
}
|
|
224
|
-
// A canonical byte identity for a certificate in any form path.build accepts (a DER Buffer, a PEM string, or
|
|
225
|
-
// an already-parsed certificate object), so a caller intermediate supplied as PEM or parsed still dedups against
|
|
226
|
-
// a byte-identical DER caPubs / cached certificate and does not spend a scarce candidate slot on a copy. The
|
|
227
|
-
// identity is the tbsCertificate bytes plus the signature (mirroring cmp.verify's _certKey): a meddler controls
|
|
228
|
-
// the unsigned extraCerts ordering, so a corrupted-signature copy sharing a valid issuer's TBS must not collapse
|
|
229
|
-
// onto it and evict the valid one. Returns null when the identity cannot be derived; a non-deduped entry is safe
|
|
230
|
-
// (a redundant slot), a wrong merge (dropping a distinct or the only valid certificate) is not.
|
|
231
|
-
// The identity comes from the same derivation every other certificate door uses: the bytes the
|
|
232
|
-
// parser recorded, never the fields of the object handed in. This is a dedupe, not a verdict,
|
|
233
|
-
// and no collision attack on it is apparent, since carrying another certificate's exact tbsBytes and
|
|
234
|
-
// signature means being that certificate. But "no attack is apparent" is the reasoning that put a
|
|
235
|
-
// completeness-only door on nine deciding boundaries, so it is not the reasoning this uses: one
|
|
236
|
-
// derivation for certificates, everywhere, and the exceptions have to argue for themselves.
|
|
237
|
-
//
|
|
238
|
-
// Failure returns null instead of throwing, which is this function's own contract and is why the
|
|
239
|
-
// door is wrapped: an underivable identity is a redundant pool slot, while a wrong merge (dropping
|
|
240
|
-
// a distinct or the only valid certificate) is not. So a rebuilt entry simply does not dedupe.
|
|
241
115
|
function _certIdentity(cert) {
|
|
242
116
|
try {
|
|
243
117
|
var p = guard.parsed.acceptDerived(cert, "certificate", x509.parse, _err, "cmp/bad-input", "a pool certificate");
|
|
244
118
|
if (!guard.parsed.isCert(p)) return null;
|
|
245
119
|
return p.tbsBytes.toString("base64") + "|" + p.signatureValue.bytes.toString("base64");
|
|
246
120
|
} catch (_e) {
|
|
247
|
-
return null;
|
|
121
|
+
return null;
|
|
248
122
|
}
|
|
249
123
|
}
|
|
250
124
|
|
|
251
|
-
// A canonical identity for a SubjectPublicKeyInfo: the algorithm OID + the AlgorithmIdentifier parameters +
|
|
252
|
-
// the raw subjectPublicKey BIT STRING. The parameters are part of the key identity (an EC curve OID, an
|
|
253
|
-
// RSASSA-PSS constraint set) and are kept, with one exception: rsaEncryption, whose parameters are NULL or
|
|
254
|
-
// omitted, both naming the same key. So the issued-cert key-match compares keys (with their constraints), not byte
|
|
255
|
-
// encodings, and neither rejects an equivalent rsaEncryption re-encoding nor accepts a constraint-changed key.
|
|
256
125
|
function _spkiKeyIdentity(spkiDer) {
|
|
257
|
-
var node = asn1.decode(spkiDer);
|
|
126
|
+
var node = asn1.decode(spkiDer);
|
|
258
127
|
var algId = node.children[0];
|
|
259
128
|
var algOid = asn1.read.oid(algId.children[0]);
|
|
260
129
|
var pn = algId.children[1];
|
|
261
130
|
var params;
|
|
262
131
|
if (algOid === OID_RSA_ENCRYPTION) {
|
|
263
|
-
// rsaEncryption parameters MUST be absent or a NULL (RFC 3279 sec. 2.3.1). Normalize only those two
|
|
264
|
-
// equivalent forms to "" so a NULL-vs-omitted re-encoding matches. Any other value (a malformed empty OCTET
|
|
265
|
-
// STRING the parser/importer may tolerate, or a changed parameter) keeps its bytes, so a parameter-changed
|
|
266
|
-
// certificate a stricter consumer rejects gets a distinct identity and the key-match refuses it.
|
|
267
132
|
params = (pn == null || _isDerNull(pn)) ? "" : pn.bytes.toString("latin1");
|
|
268
133
|
} else {
|
|
269
|
-
// Every other algorithm's parameters ARE part of the key identity (an EC curve OID, an RSASSA-PSS constraint set).
|
|
270
134
|
params = pn ? pn.bytes.toString("latin1") : "";
|
|
271
135
|
}
|
|
272
136
|
return algOid + "|" + params + "|" + node.children[1].bytes.toString("latin1");
|
|
273
137
|
}
|
|
274
|
-
// A well-formed DER NULL node (universal tag 5, empty content) -- the only rsaEncryption parameter form treated
|
|
275
|
-
// as equivalent to an absent parameter.
|
|
276
138
|
function _isDerNull(pn) { return pn.tagClass === "universal" && pn.tagNumber === 5 && pn.content.length === 0; }
|
|
277
139
|
|
|
278
|
-
// The bounded, distinct, parseable extraCerts of an already-verified response, cached so a later leg that
|
|
279
|
-
// omits extraCerts can rebuild the signer path, mirroring cmp.verify's own extraCerts bounding: dedup, drop
|
|
280
|
-
// any non-X.509 entry, cap at MAX_EXTRA_CERTS, and stop after MAX_EXTRA_SCAN entries. So a meddler appending a
|
|
281
|
-
// flood of unsigned certs cannot make an otherwise-valid enrollment fail when the cache reaches path.build.
|
|
282
140
|
var MAX_EXTRA_CERTS = 32, MAX_EXTRA_SCAN = 256;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// no priority attempt has to choose between them, and a chain assembled from both sources validates in one build.
|
|
287
|
-
var CAPUBS_MAX = 2 * MAX_EXTRA_CERTS; // the leaf's authenticated issuer material (caPubs), bounded so caPubs + cached chain + the caller pool fit under the ceiling
|
|
288
|
-
// The caller intermediates pool is capped below the path-builder ceiling by the room the session's own material
|
|
289
|
-
// can occupy. A signature session reserves CAPUBS_MAX (a grant's caPubs, leaf validation) plus MAX_EXTRA_CERTS (the
|
|
290
|
-
// response's extraCerts + cached signer chain, the signer-path pool). A MAC session authenticates the response by
|
|
291
|
-
// the shared secret and never adds a signer chain (its cache gate is isSig), so it reserves CAPUBS_MAX alone; its
|
|
292
|
-
// sole pool is _validateLeaf's caPubs + caller. So every candidate pool the session builds holds the whole caller
|
|
293
|
-
// pool and all authenticated material at once: one build, no priority choice, no retry.
|
|
294
|
-
var SESSION_MAX_INTERMEDIATES = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES - CAPUBS_MAX - MAX_EXTRA_CERTS; // signature
|
|
295
|
-
var SESSION_MAX_INTERMEDIATES_MAC = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES - CAPUBS_MAX; // MAC (no signer chain reserved)
|
|
296
|
-
// A verify verdict whose failure a DIFFERENT candidate pool or the cached/prebound signer might still recover:
|
|
297
|
-
// extraCerts is outside the protected part, so a signer that did not resolve, a wrong-key/wrong-subject decoy
|
|
298
|
-
// selected first, or an untrusted chain can all be retried. A transaction-integrity failure (transactionID /
|
|
299
|
-
// recipNonce mismatch) is deliberately absent: it is decoy-independent, and re-running would only mask the desync.
|
|
141
|
+
var CAPUBS_MAX = 2 * MAX_EXTRA_CERTS;
|
|
142
|
+
var SESSION_MAX_INTERMEDIATES = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES - CAPUBS_MAX - MAX_EXTRA_CERTS;
|
|
143
|
+
var SESSION_MAX_INTERMEDIATES_MAC = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES - CAPUBS_MAX;
|
|
300
144
|
function _isRecoverableVerify(code) {
|
|
301
145
|
return code === "cmp/signer-cert-not-found" || code === "cmp/protection-failed" ||
|
|
302
146
|
code === "cmp/sender-mismatch" || code === "cmp/untrusted-signer";
|
|
@@ -312,15 +156,12 @@ function _responseExtraCerts(responseBytes) {
|
|
|
312
156
|
if (seen[key]) continue;
|
|
313
157
|
seen[key] = true;
|
|
314
158
|
try { x509.parse(c); }
|
|
315
|
-
catch (_e) { continue; }
|
|
316
|
-
out.push(Buffer.from(c));
|
|
159
|
+
catch (_e) { continue; }
|
|
160
|
+
out.push(Buffer.from(c));
|
|
317
161
|
}
|
|
318
162
|
return out;
|
|
319
163
|
}
|
|
320
164
|
|
|
321
|
-
// Normalize a caller-supplied CRMF certReqId to the value the session echoes + matches, the same way
|
|
322
|
-
// crmf-sign._certReqId encodes it: a number or bigint is kept; a string (decimal or 0x-hex) is parsed via
|
|
323
|
-
// BigInt so a supported form ("5" / "0x5") is not silently replaced by the default; anything else -> `dflt`.
|
|
324
165
|
function _normalizeCertReqId(cid, dflt) {
|
|
325
166
|
if (typeof cid === "bigint" || typeof cid === "number") return cid;
|
|
326
167
|
if (typeof cid === "string") {
|
|
@@ -330,9 +171,6 @@ function _normalizeCertReqId(cid, dflt) {
|
|
|
330
171
|
return dflt;
|
|
331
172
|
}
|
|
332
173
|
|
|
333
|
-
// The hash inside RSASSA-PSS-params (the hashAlgorithm [0] AlgorithmIdentifier OID) -- for id-RSASSA-PSS the
|
|
334
|
-
// digest lives in the parameters, not the OID name (RFC 4055). Returns "SHA-256" / "SHA-384" / "SHA-512", or
|
|
335
|
-
// null when the params are absent / unreadable / name an unmapped hash (the caller then declares SHA-256).
|
|
336
174
|
function _pssDigest(paramsBytes) {
|
|
337
175
|
if (!Buffer.isBuffer(paramsBytes) || paramsBytes.length === 0) return null;
|
|
338
176
|
try {
|
|
@@ -343,31 +181,18 @@ function _pssDigest(paramsBytes) {
|
|
|
343
181
|
if (f.tagClass === "context" && f.tagNumber === 0 && f.children && f.children.length === 1) {
|
|
344
182
|
var algSeq = f.children[0];
|
|
345
183
|
if (!algSeq.children || algSeq.children.length < 1) return null;
|
|
346
|
-
return HASH_OID_TO_DIGEST[asn1.read.oid(algSeq.children[0])] || null;
|
|
184
|
+
return HASH_OID_TO_DIGEST[asn1.read.oid(algSeq.children[0])] || null;
|
|
347
185
|
}
|
|
348
186
|
}
|
|
349
187
|
} catch (_e) { /* allow:swallow-unverified a malformed PSS-params blob falls back to null -> the caller's declared-SHA-256 path; a display-hash inference never throws */ return null; }
|
|
350
188
|
return null;
|
|
351
189
|
}
|
|
352
190
|
|
|
353
|
-
// The certConf certHash algorithm (RFC 9810 sec. 5.3.18): use the same hash the certificate signature uses.
|
|
354
|
-
// If the signatureAlgorithm OID conveys the hash (sha256WithRSAEncryption / ecdsaWithSHA384 / ...) use that
|
|
355
|
-
// hash and omit hashAlg. For id-RSASSA-PSS the hash is carried in the params, so decode it and likewise omit
|
|
356
|
-
// hashAlg. Only when the hash is genuinely not conveyed (Ed25519 / Ed448; ML-DSA / SLH-DSA) is SHA-256 used
|
|
357
|
-
// and declared in the explicit hashAlg field so the CA recomputes certHash under the same stated hash.
|
|
358
191
|
function _certConfHash(certDer) {
|
|
359
192
|
var sa;
|
|
360
193
|
try { sa = x509.parse(certDer).signatureAlgorithm; }
|
|
361
|
-
catch (e) { throw _err("cmp/bad-cert-response", "the issued certificate is unexpectedly unparseable at certConf", e); }
|
|
362
|
-
// Dispatch by the IMMUTABLE signatureAlgorithm OID (never the mutable display name). A hashless SIGNATURE
|
|
363
|
-
// (EdDSA / ML-DSA / SLH-DSA) -> SHA-256 + an explicit hashAlg; a hash-conveying signature -> its hash, no
|
|
364
|
-
// hashAlg; id-RSASSA-PSS -> the hash from its parameters, no hashAlg.
|
|
194
|
+
catch (e) { throw _err("cmp/bad-cert-response", "the issued certificate is unexpectedly unparseable at certConf", e); }
|
|
365
195
|
if (HASHLESS_SIG_OIDS[sa.oid]) return { digest: "SHA-256", hashAlg: "sha256" };
|
|
366
|
-
// A composite signature (draft-ietf-lamps-pq-composite-sigs): compute certHash under the composite's own
|
|
367
|
-
// declared prehash digest (COMPOSITE_ALGS[oid].ph) and declare it in the explicit hashAlg. If the prehash is
|
|
368
|
-
// not a certConf-representable hash (SHAKE256, which the CMP CertStatus hashAlg cannot name), fail closed:
|
|
369
|
-
// substituting SHA-256 would send a certHash under a hash that contradicts the signature's declared prehash,
|
|
370
|
-
// which a conforming CA rejects. Refuse, never confirm under a false algorithm (RFC 9810 sec. 5.3.18).
|
|
371
196
|
var comp = compositeSig.COMPOSITE_ALGS[sa.oid];
|
|
372
197
|
if (comp) {
|
|
373
198
|
var ha = COMPOSITE_PH_HASHALG[comp.ph];
|
|
@@ -380,9 +205,6 @@ function _certConfHash(certDer) {
|
|
|
380
205
|
if (pd) return { digest: pd, hashAlg: null };
|
|
381
206
|
throw _err("cmp/bad-cert-response", "the issued RSASSA-PSS certificate's hash cannot be resolved from its parameters (RFC 4055); the certConf hash is indeterminate");
|
|
382
207
|
}
|
|
383
|
-
// Any other AlgorithmIdentifier (an unregistered OID, or a registered non-signature / indeterminate one
|
|
384
|
-
// such as rsaEncryption or an ML-KEM OID): the required certConf hash cannot be determined. Do not guess
|
|
385
|
-
// SHA-256 (a wrong certHash the CA rejects); fail the transaction closed before confirming.
|
|
386
208
|
throw _err("cmp/bad-cert-response", "the issued certificate's signature algorithm does not determine a certConf hash (an unrecognized or non-signature algorithm); the transaction is refused (RFC 9810 sec. 5.3.18)");
|
|
387
209
|
}
|
|
388
210
|
|
|
@@ -483,59 +305,26 @@ function session(opts) {
|
|
|
483
305
|
if (opts == null) opts = {};
|
|
484
306
|
guard.identifier.assertPlainRecord(opts, _err, "cmp/bad-input", "opts");
|
|
485
307
|
guard.identifier.assertKnownKeys(opts, KNOWN_SESSION_OPTS, _err, "cmp/bad-input", "unknown session opts field ");
|
|
486
|
-
// Work on a SHALLOW COPY so a later normalization (e.g. an empty MAC trustAnchors list) never mutates the
|
|
487
|
-
// caller's object -- which may be frozen or reused across sessions -- and normalizing a frozen input does not
|
|
488
|
-
// throw a raw TypeError. Only top-level fields are reassigned; nested values are read, never mutated.
|
|
489
308
|
opts = Object.assign({}, opts);
|
|
490
309
|
if (typeof opts.url !== "string" || !opts.url) throw _err("cmp/bad-input", "opts.url (the CMP endpoint) is required");
|
|
491
310
|
|
|
492
|
-
// EXACTLY ONE protection flavor -- signature (key + cert) XOR PBMAC1 (mac), mirroring cmp.build / cmp.verify.
|
|
493
311
|
var isSig = opts.key != null || opts.cert != null;
|
|
494
312
|
var isMac = opts.mac != null;
|
|
495
313
|
if (isSig === isMac) throw _err("cmp/bad-input", "supply EXACTLY ONE protection flavor: { key, cert } (signature) OR { mac } (PBMAC1)");
|
|
496
314
|
if (isSig && (opts.key == null || opts.cert == null)) throw _err("cmp/bad-input", "signature protection requires BOTH opts.key and opts.cert");
|
|
497
|
-
// Signature protection can only be AUTHENTICATED against a trust anchor: without one, a response signer is
|
|
498
|
-
// resolved from the message's own (unsigned) extraCerts and can be any self-signed key an attacker on the
|
|
499
|
-
// transport supplies. Require trustAnchors up front for the signature flavor. A MAC (PBMAC1) session
|
|
500
|
-
// authenticates the RESPONSE by the shared secret, so anchors are OPTIONAL there -- but when supplied they
|
|
501
|
-
// still validate the ISSUED certificate's own signature + chain before it is confirmed (the MAC authenticates
|
|
502
|
-
// the exchange, not the embedded X.509 signature); they are NOT forwarded to the MAC response verify.
|
|
503
315
|
if (isSig) {
|
|
504
316
|
var hasAnchors = opts.trustAnchors != null && !(Array.isArray(opts.trustAnchors) && opts.trustAnchors.length === 0);
|
|
505
317
|
if (!hasAnchors) throw _err("cmp/bad-input", "signature protection requires opts.trustAnchors to authenticate the CA's response signer (RFC 9483 sec. 3.2)");
|
|
506
318
|
}
|
|
507
|
-
// A MAC session's anchors are OPTIONAL (used only to validate the issued certificate's own chain, never the MAC
|
|
508
|
-
// response authentication): an empty list means "no issued-cert chain validation", exactly like omitting it. The
|
|
509
|
-
// signature branch above already rejects [] outright. Normalize the MAC empty list to absent so _validateLeaf /
|
|
510
|
-
// _verifyOpts treat it uniformly -- otherwise the empty trust store reaches _engine.build and rejects the issued
|
|
511
|
-
// certificate only in _finish, AFTER the authenticated grant has consumed the one-shot session. This assigns to
|
|
512
|
-
// the shallow COPY made above, never the caller's (possibly frozen) options object.
|
|
513
319
|
if (isMac && Array.isArray(opts.trustAnchors) && opts.trustAnchors.length === 0) opts.trustAnchors = null;
|
|
514
|
-
// Parse + validate each supplied anchor + intermediate NOW (through the SAME path-builder normalization the
|
|
515
|
-
// verify + issued-cert validation use), so an unusable trust input (a non-certificate string, a malformed
|
|
516
|
-
// tuple, junk bytes) is a construction error rather than a failure discovered only after the one-shot
|
|
517
|
-
// transaction has already engaged the transport.
|
|
518
320
|
if (opts.trustAnchors != null && _engine && _engine.toAnchor) _asCertList(opts.trustAnchors).forEach(function (a) { try { _engine.toAnchor(a); } catch (e) { throw _err("cmp/bad-input", "opts.trustAnchors: each entry must be a certificate (DER/PEM/parsed) or a { name, publicKey, algorithm } anchor tuple -- " + ((e && e.message) || e), e); } });
|
|
519
321
|
if (opts.intermediates != null && _engine && _engine.coerceCert) _asCertList(opts.intermediates).forEach(function (c) { try { _engine.coerceCert(c); } catch (e) { throw _err("cmp/bad-input", "opts.intermediates: each entry must be a certificate (DER/PEM/parsed) -- " + ((e && e.message) || e), e); } });
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
// empty-subject CA named only by a directoryName SAN -- which a subject-string pin reads as an unmatchable null
|
|
523
|
-
// -- is bound correctly. Accept the documented DER Buffer / PEM string / already-parsed certificate at
|
|
524
|
-
// construction; a non-certificate value would otherwise consume the one-shot transaction before the first
|
|
525
|
-
// verify could reject it. The parsed form drives the identity bind; the DER (kept when supplied as bytes/PEM)
|
|
526
|
-
// resolves a first response that omits its extraCerts (below).
|
|
527
|
-
var _expectedSenderCert = null; // parsed, for the senderBoundToCert identity pin
|
|
528
|
-
var _expectedSenderDer = null; // DER, for the omitted-extraCerts signer fallback (null when given the already-parsed form, which retains no source DER)
|
|
322
|
+
var _expectedSenderCert = null;
|
|
323
|
+
var _expectedSenderDer = null;
|
|
529
324
|
if (opts.expectedSender != null) {
|
|
530
325
|
var _es = opts.expectedSender;
|
|
531
326
|
try {
|
|
532
|
-
if (_es && Buffer.isBuffer(_es.tbsBytes)) {
|
|
533
|
-
// The door's RETURN is what gets pinned, not the object handed in. coerceCert re-derives the
|
|
534
|
-
// certificate from the bytes its parser read, so calling it only as a check and then storing
|
|
535
|
-
// the caller's object keeps every edit the re-derivation exists to discard: this pin is
|
|
536
|
-
// compared against each response signer's subject and SAN, so an edited one would accept a
|
|
537
|
-
// different CMP signer than the caller meant to pin. A validator that normalizes has its
|
|
538
|
-
// return value as its contract -- using it as a predicate throws that contract away.
|
|
327
|
+
if (_es && Buffer.isBuffer(_es.tbsBytes)) {
|
|
539
328
|
_expectedSenderCert = (_engine && _engine.coerceCert) ? _engine.coerceCert(_es) : _es;
|
|
540
329
|
}
|
|
541
330
|
else if (guard.bytes.isByteSource(_es)) { _expectedSenderDer = guard.bytes.snapshotSource(_es, CmpError, "cmp/bad-input", "opts.expectedSender"); _expectedSenderCert = x509.parse(_expectedSenderDer); }
|
|
@@ -546,11 +335,6 @@ function session(opts) {
|
|
|
546
335
|
throw _err("cmp/bad-input", "opts.expectedSender must be the CA signer certificate (DER BufferSource / PEM / parsed) so every signed response can be bound to it -- " + ((e && e.message) || e), e);
|
|
547
336
|
}
|
|
548
337
|
}
|
|
549
|
-
// Reject a DISTINCT intermediate pool exceeding the per-flavor cap (the candidate ceiling MINUS the room reserved
|
|
550
|
-
// for the CA's own authenticated material). The caller pool is never truncated, so the whole pool + that material
|
|
551
|
-
// must fit under path.build's ceiling; an oversized one would otherwise raise path/bad-input only at the first
|
|
552
|
-
// verify, after the one-shot transaction is consumed. A MAC session reserves LESS (no signer chain), so its cap is
|
|
553
|
-
// higher. (Duplicates collapse, so the count is by distinct id.)
|
|
554
338
|
var maxIntermediates = isSig ? SESSION_MAX_INTERMEDIATES : SESSION_MAX_INTERMEDIATES_MAC;
|
|
555
339
|
if (opts.intermediates != null) {
|
|
556
340
|
var seenInt = Object.create(null), distinctInt = 0;
|
|
@@ -558,102 +342,61 @@ function session(opts) {
|
|
|
558
342
|
if (distinctInt > maxIntermediates) throw _err("cmp/bad-input", "opts.intermediates has " + distinctInt + " distinct certificates, exceeding the " + maxIntermediates + " limit (room is reserved below the path-builder ceiling for the CA's own delivered issuer certificates" + (isSig ? " and signer chain" : "") + ")");
|
|
559
343
|
}
|
|
560
344
|
|
|
561
|
-
// A certificate-acceptance policy must be a function -- a non-function value (a config typo) would otherwise
|
|
562
|
-
// be silently skipped, auto-accepting a grantedWithMods certificate the caller meant to vet. Fail at config.
|
|
563
345
|
if (opts.acceptCert != null && typeof opts.acceptCert !== "function") throw _err("cmp/bad-input", "opts.acceptCert must be a function (certDer, info) => boolean");
|
|
564
|
-
// implicitConfirm must be a boolean -- a truthy non-boolean (the string "false", say) would silently REQUEST
|
|
565
|
-
// implicit confirmation and reverse the caller's confirmation policy. Reject a non-boolean at construction.
|
|
566
346
|
if (opts.implicitConfirm != null && typeof opts.implicitConfirm !== "boolean") throw _err("cmp/bad-input", "opts.implicitConfirm must be a boolean");
|
|
567
|
-
// sleep, when supplied, must be a function -- a non-function value would silently fall back to the REAL timer,
|
|
568
|
-
// reversing the caller's intent (a bounded / injected sleeper) into a real-time wait. Reject it at construction.
|
|
569
347
|
if (opts.sleep != null && typeof opts.sleep !== "function") throw _err("cmp/bad-input", "opts.sleep must be a function (delayMs) => Promise");
|
|
570
|
-
// The transport, when supplied, must be a function -- a non-function value passes cmp.transfer's LOCAL url/budget
|
|
571
|
-
// checks and then throws a raw TypeError when the transport is invoked, before any request is sent. That local
|
|
572
|
-
// type error carries none of the typed cmp/* local codes, so the send path would mark the one-shot session
|
|
573
|
-
// consumed on it. Reject it at construction so the transport call can only throw AFTER it has engaged the network.
|
|
574
348
|
if (opts.transport != null && typeof opts.transport !== "function") throw _err("cmp/bad-input", "opts.transport must be a function (url, reqDer, opts) => Promise<{ responseBytes, status }>");
|
|
575
|
-
// acceptCert and implicitConfirm are incompatible: under implicit confirmation the CA treats the certificate
|
|
576
|
-
// as delivered at grant time, so there is no reject leg to honor a veto -- a policy could only produce a
|
|
577
|
-
// misleading rejected verdict for a certificate the CA still considers issued. Refuse the combination up front.
|
|
578
349
|
if (opts.acceptCert != null && opts.implicitConfirm) throw _err("cmp/bad-input", "opts.acceptCert cannot be combined with opts.implicitConfirm -- implicit confirmation leaves no certConf leg to reject on (drop implicitConfirm to vet a grant)");
|
|
579
350
|
|
|
580
|
-
// The verify/validation clock, when supplied, must be a valid Date -- else path.build would only reject it while
|
|
581
|
-
// verifying a response, AFTER the one-shot transaction has already engaged the transport and been consumed.
|
|
582
351
|
if (opts.time != null) guard.time.assertValid(opts.time, _err, "cmp/bad-input", "opts.time (the verify/validation clock)");
|
|
583
|
-
// Poll budgets, validated at construction (NaN / negative / over-max -> cmp/bad-input, never a disabled bound).
|
|
584
352
|
var maxPolls = guard.limits.cap(opts.maxPolls, "opts.maxPolls", DEFAULT_MAX_POLLS, { E: _err, code: "cmp/bad-input", min: 1, max: 1000 });
|
|
585
353
|
var maxTotalWait = guard.limits.cap(opts.maxTotalWait, "opts.maxTotalWait", DEFAULT_MAX_TOTAL_WAIT, { E: _err, code: "cmp/bad-input", min: 0, max: retryAfter.MAX_RETRY_AFTER_SECONDS });
|
|
586
|
-
var sleep = opts.sleep || sleepUtil.sleep;
|
|
354
|
+
var sleep = opts.sleep || sleepUtil.sleep;
|
|
587
355
|
|
|
588
|
-
// Transaction identity: mint ONE 128-bit transactionID (stable for the whole transaction), held here.
|
|
589
356
|
var transactionID = Buffer.from(webcrypto.webcrypto.getRandomValues(new Uint8Array(16)));
|
|
590
|
-
var lastPeerNonce = null;
|
|
591
|
-
var haveResponse = false;
|
|
592
|
-
var cachedSignerCert = null;
|
|
593
|
-
var pinnedSignerCert = null, signerPinned = false;
|
|
594
|
-
var cachedChain = [];
|
|
595
|
-
var caPubsAccum = [];
|
|
596
|
-
var caPubsSeen = Object.create(null);
|
|
597
|
-
var caPubsBytes = 0;
|
|
598
|
-
var caPubsWaitingCount = 0;
|
|
599
|
-
var activeCertReqId = DEFAULT_CERT_REQ_ID;
|
|
600
|
-
var expectedRespArm = "ip";
|
|
601
|
-
// Which operation is in flight: it decides how the expected arm's body is read, and, for an error
|
|
602
|
-
// body, whether a `waiting` status announces delayed delivery. RFC 9483 sec. 4.4 puts that status
|
|
603
|
-
// in the ip/cp/kup of an enrollment and in an error message for every other operation, so the two
|
|
604
|
-
// readings are exclusive rather than a leniency applied to both.
|
|
357
|
+
var lastPeerNonce = null;
|
|
358
|
+
var haveResponse = false;
|
|
359
|
+
var cachedSignerCert = null;
|
|
360
|
+
var pinnedSignerCert = null, signerPinned = false;
|
|
361
|
+
var cachedChain = [];
|
|
362
|
+
var caPubsAccum = [];
|
|
363
|
+
var caPubsSeen = Object.create(null);
|
|
364
|
+
var caPubsBytes = 0;
|
|
365
|
+
var caPubsWaitingCount = 0;
|
|
366
|
+
var activeCertReqId = DEFAULT_CERT_REQ_ID;
|
|
367
|
+
var expectedRespArm = "ip";
|
|
605
368
|
var txnKind = "enroll";
|
|
606
|
-
var expectedInfoOp = null;
|
|
607
|
-
var crlQuery = null;
|
|
608
|
-
var requestedSpki = null;
|
|
609
|
-
var inFlight = false, completed = false, started = false;
|
|
369
|
+
var expectedInfoOp = null;
|
|
370
|
+
var crlQuery = null;
|
|
371
|
+
var requestedSpki = null;
|
|
372
|
+
var inFlight = false, completed = false, started = false;
|
|
610
373
|
var transcript = [];
|
|
611
|
-
var transcriptBytes = 0;
|
|
612
|
-
// The transcript-retention ceiling: TRANSCRIPT_RETAIN_RESPONSES times the effective per-response byte cap. A
|
|
613
|
-
// non-positive / non-finite maxResponseBytes falls back to the default here (a bad value throws in the transport
|
|
614
|
-
// on the first transfer, before any response is recorded) so the ceiling is ALWAYS a positive finite number --
|
|
615
|
-
// a NaN ceiling would make `total > ceiling` perpetually false and silently disable the bound.
|
|
374
|
+
var transcriptBytes = 0;
|
|
616
375
|
var _perRespCap = (typeof opts.maxResponseBytes === "number" && isFinite(opts.maxResponseBytes) && opts.maxResponseBytes > 0) ? opts.maxResponseBytes : constants.LIMITS.HTTP_MAX_RESPONSE_BYTES;
|
|
617
376
|
var transcriptCap = _perRespCap * TRANSCRIPT_RETAIN_RESPONSES;
|
|
618
|
-
// The caPubs byte budget, proportional to the per-response cap and above one full response, so the GRANT leg's
|
|
619
|
-
// own caPubs (bounded by one response) always fit after evicting waiting entries -- while the total stays bounded
|
|
620
|
-
// (a count-only cap would still admit ~16 GiB of DER-limit-sized certs) independent of the candidate-count cap.
|
|
621
377
|
var caPubsByteBudget = _perRespCap * TRANSCRIPT_RETAIN_RESPONSES;
|
|
622
378
|
|
|
623
|
-
// Sender / recipient routing (RFC 9810 sec. 5.1.1). The sender NAMES the requester: signature protection
|
|
624
|
-
// defaults it to the signer cert's own subject DN; a MAC transaction (no cert) has no established name, so
|
|
625
|
-
// it defaults to the NULL-DN (an empty RDNSequence). The recipient (the CA) defaults to the NULL-DN too --
|
|
626
|
-
// the CA is addressed by the endpoint URL, not by a name we can assert. opts.sender / opts.recipient override.
|
|
627
379
|
var NULL_DN = { directoryName: [] };
|
|
628
380
|
var defaultSender = NULL_DN;
|
|
629
381
|
if (isSig) {
|
|
630
382
|
try {
|
|
631
383
|
var _signerSubject = x509.parse(opts.cert).subject;
|
|
632
|
-
// An EMPTY-subject certificate (RFC 5280 sec. 4.1.2.6) is identified only by its subjectAltName, and a
|
|
633
|
-
// conforming peer binds the sender to that SAN (never to the empty subject). Defaulting the sender to the
|
|
634
|
-
// empty DN would name the requester with a value no peer accepts, so REQUIRE opts.sender for such a cert.
|
|
635
384
|
if (_signerSubject.rdns && _signerSubject.rdns.length > 0) defaultSender = { directoryName: _signerSubject.bytes };
|
|
636
385
|
else if (opts.sender == null) throw _err("cmp/bad-input", "the signature-protection certificate has an empty subject, so it cannot name the request sender -- an empty-subject certificate is identified by its subjectAltName; set opts.sender explicitly (e.g. a subjectAltName identity the CA will bind the sender to)");
|
|
637
386
|
} catch (e) {
|
|
638
387
|
if (e && e.isCmpError) throw e;
|
|
639
|
-
defaultSender = NULL_DN;
|
|
388
|
+
defaultSender = NULL_DN;
|
|
640
389
|
}
|
|
641
390
|
}
|
|
642
391
|
|
|
643
|
-
// The header the session OWNS on every request (transaction identity + routing), plus the protection opts.
|
|
644
392
|
function _baseHeader(fresh) {
|
|
645
393
|
var h = { transactionID: transactionID, senderNonce: fresh };
|
|
646
|
-
// The first request carries no recipNonce; every FOLLOW-UP leg MUST echo the previous response's
|
|
647
|
-
// senderNonce (RFC 9810 sec. 5.1.1). A prior response that omitted its senderNonce leaves nothing to
|
|
648
|
-
// echo -- the anti-replay chain is broken, so fail closed rather than send an unchained request.
|
|
649
394
|
if (haveResponse) {
|
|
650
395
|
if (!Buffer.isBuffer(lastPeerNonce) || lastPeerNonce.length === 0) throw _err("cmp/bad-nonce", "the previous CMP response omitted its senderNonce, so this follow-up request cannot echo it as recipNonce (RFC 9810 sec. 5.1.1)");
|
|
651
396
|
h.recipNonce = lastPeerNonce;
|
|
652
397
|
}
|
|
653
398
|
h.sender = opts.sender != null ? opts.sender : defaultSender;
|
|
654
399
|
h.recipient = opts.recipient != null ? opts.recipient : NULL_DN;
|
|
655
|
-
// Key identifiers the endpoint uses to select a credential -- a PBMAC1 shared secret (senderKID) among several,
|
|
656
|
-
// or the recipient key (recipKID). Propagated on EVERY leg so the CA resolves the same credential throughout.
|
|
657
400
|
if (opts.senderKID != null) h.senderKID = opts.senderKID;
|
|
658
401
|
if (opts.recipKID != null) h.recipKID = opts.recipKID;
|
|
659
402
|
return h;
|
|
@@ -666,24 +409,9 @@ function session(opts) {
|
|
|
666
409
|
return o;
|
|
667
410
|
}
|
|
668
411
|
function _verifyOpts(fresh, signerCertOverride, extraIntermediates, responseExtra) {
|
|
669
|
-
// Verify the RESPONSE protection + bind it to THIS exchange via the opt-in echo checks. The CA's signer
|
|
670
|
-
// cert is resolved from the response extraCerts (RFC 9483 sec. 3.3); trustAnchors chain it. A MAC response
|
|
671
|
-
// verifies under the shared secret. transactionID + expectRecipNonce fail-close a mismatched response.
|
|
672
412
|
var o = { transactionID: transactionID, expectRecipNonce: fresh };
|
|
673
413
|
if (isMac) o.sharedSecret = opts.mac.secret;
|
|
674
|
-
// Forward trustAnchors to the RESPONSE verify ONLY for the signature flavor -- the MAC branch of cmp.verify
|
|
675
|
-
// rejects them as a stray signature credential. A MAC session's anchors are used solely for issued-cert
|
|
676
|
-
// validation (_validateLeaf), never the MAC response authentication (which the shared secret establishes).
|
|
677
414
|
else if (opts.trustAnchors != null) o.trustAnchors = opts.trustAnchors;
|
|
678
|
-
// Give cmp.verify ONE bounded pool that already contains the signer's OWN delivered issuers, so it needs no
|
|
679
|
-
// room RESERVED for an internal append (no pre-verify count to estimate, no duplicate to over-reserve). Order
|
|
680
|
-
// by priority: the response's extraCerts first (when responseExtra is passed), then the cached signer chain,
|
|
681
|
-
// then the broad caller pool fills to the ceiling -- _boundedPool dedups the whole thing, so a response cert
|
|
682
|
-
// duplicating a caller one spends no slot, and cmp.verify's re-append of the response extraCerts is all
|
|
683
|
-
// duplicates path.build collapses. The signer itself may sit in this pool as a redundant candidate (a slot);
|
|
684
|
-
// rather than try to IDENTIFY it among extraCerts (senderKID can select one that is not first), the caller of
|
|
685
|
-
// _send retries with responseExtra=null (caller pool at the full ceiling) if this response-prioritized pool
|
|
686
|
-
// truncated a needed caller issuer -- the two attempts cover both truncation priorities without identifying the signer.
|
|
687
415
|
var issuers = Array.isArray(responseExtra) ? responseExtra : [];
|
|
688
416
|
var extra = [];
|
|
689
417
|
if (Array.isArray(extraIntermediates)) {
|
|
@@ -694,9 +422,6 @@ function session(opts) {
|
|
|
694
422
|
var ints = _boundedPool(issuers.concat(extra), _asCertList(opts.intermediates));
|
|
695
423
|
if (ints.length) o.intermediates = ints;
|
|
696
424
|
if (opts.time != null) o.time = opts.time;
|
|
697
|
-
// An explicit signerCert takes ABSOLUTE precedence in cmp.verify, so it is passed ONLY as a fallback for a
|
|
698
|
-
// response that cannot resolve its own signer -- never by default, else a clustered CA that rotates the
|
|
699
|
-
// protection cert mid-transaction (a valid later cert in that leg's extraCerts) would verify under the wrong key.
|
|
700
425
|
if (signerCertOverride != null) o.signerCert = signerCertOverride;
|
|
701
426
|
return o;
|
|
702
427
|
}
|
|
@@ -706,20 +431,11 @@ function session(opts) {
|
|
|
706
431
|
return o;
|
|
707
432
|
}
|
|
708
433
|
|
|
709
|
-
// ONE leg: mint a fresh senderNonce, build+protect the request, transfer it, VERIFY the response BEFORE
|
|
710
|
-
// reading its body (a !valid verdict is a HARD STOP throw), chain the peer nonce, record both directions.
|
|
711
434
|
async function _send(bodySpec, arm) {
|
|
712
435
|
var fresh = Buffer.from(webcrypto.webcrypto.getRandomValues(new Uint8Array(16)));
|
|
713
436
|
var header = Object.assign(_baseHeader(fresh), ENROLL_ARMS[arm] && opts.implicitConfirm ? { generalInfo: [{ infoType: "implicitConfirm" }] } : {});
|
|
714
437
|
var reqDer = await cmp.build({ header: header, body: bodySpec }, _buildOpts());
|
|
715
438
|
_recordTranscript({ direction: "out", arm: arm, bytes: reqDer });
|
|
716
|
-
// The one-shot `started` flag must flip iff a request MAY have reached the transport, so the caller can fix a
|
|
717
|
-
// purely-local config error (url parse, transfer-budget caps, TLS-anchor check) and retry, but a request that
|
|
718
|
-
// reached the CA is never silently replayed under the same transactionID/nonce. cmp.transfer runs those local
|
|
719
|
-
// checks BEFORE it invokes the transport. A CUSTOM transport, though, can itself throw the SAME cmp/* codes as
|
|
720
|
-
// those preflight checks AFTER receiving the request, so the error code cannot distinguish the two -- WRAP it so
|
|
721
|
-
// `engaged` flips the instant cmp.transfer hands it the request (past its preflight). The DEFAULT transport never
|
|
722
|
-
// reuses the preflight codes for its own network errors, so for it those codes reliably mean a pre-send failure.
|
|
723
439
|
var engaged = false;
|
|
724
440
|
var topts = _transferOpts();
|
|
725
441
|
if (opts.transport != null) topts.transport = function (a) { engaged = true; return opts.transport(a); };
|
|
@@ -733,162 +449,76 @@ function session(opts) {
|
|
|
733
449
|
if (reached) started = true;
|
|
734
450
|
throw e;
|
|
735
451
|
}
|
|
736
|
-
started = true;
|
|
737
|
-
// The response's OWN deduped extraCerts (the signer + its delivered issuers) -- _verifyOpts folds them into the
|
|
738
|
-
// signer-path pool as priority so cmp.verify needs no reserved room for its internal append. cmp.transfer has
|
|
739
|
-
// already parsed res.responseBytes as a PKIMessage (its cmp.parse gate on every return path), so this cannot throw.
|
|
452
|
+
started = true;
|
|
740
453
|
var responseExtra = _responseExtraCerts(res.responseBytes);
|
|
741
|
-
// The response's OWN issuers, the CACHED chain from an earlier authenticated leg, AND the whole caller pool fit
|
|
742
|
-
// in ONE candidate pool (SESSION_MAX_INTERMEDIATES reserves room below the ceiling), so a single verify sees every
|
|
743
|
-
// candidate -- no priority attempt has to choose between them, and a signer path assembled from any of the three
|
|
744
|
-
// sources builds. cachedChain is included in the PRIMARY (not only the fallback) so a same-identity signer
|
|
745
|
-
// rotation -- a later leg's extraCerts carries the rotated signer B, but B's issuer was delivered only alongside
|
|
746
|
-
// signer A on an earlier leg -- resolves B from its own extraCerts AND chains it via the cached issuer, rather
|
|
747
|
-
// than reporting B untrusted and falling through to a fallback that (forcing signer A's key) cannot verify B's
|
|
748
|
-
// signature. No signer identification is needed (senderKID may name a cert that is not extraCerts[0]); every
|
|
749
|
-
// candidate is simply present. cachedChain is [] before any response has authenticated, so this is inert then.
|
|
750
454
|
var verdict = await cmp.verify(res.responseBytes, _verifyOpts(fresh, null, cachedChain, responseExtra));
|
|
751
|
-
// Fallback: a signature-flavor response's OWN signer resolution can fail FOUR ways that the earlier,
|
|
752
|
-
// authenticated signer still recovers, all because extraCerts is OUTSIDE the protected part and a meddler
|
|
753
|
-
// controls which cert the resolver selects -- (a) NO signer resolved (a later leg omitted extraCerts once the
|
|
754
|
-
// recipient holds the cert, RFC 9483 sec. 3.3) -> signer-cert-not-found; (b) a same-subject decoy with a
|
|
755
|
-
// DIFFERENT key selected first -> protection-failed; (c) a decoy carrying the REAL signer's key but a WRONG
|
|
756
|
-
// subject -> the signature verifies yet the header sender does not bind to it -> sender-mismatch; (d) a decoy
|
|
757
|
-
// carrying the REAL signer's key + identity but chaining to an UNTRUSTED issuer -> verifies yet trust fails
|
|
758
|
-
// -> untrusted-signer. All retry against the cached (already trusted) signer + its chain, whose subject IS the
|
|
759
|
-
// authenticated sender and whose key is fixed. The response's own certs are still preferred on the FIRST
|
|
760
|
-
// attempt, so a clustered CA's legitimately rotated cert wins there; the retry can only rescue a message
|
|
761
|
-
// ACTUALLY signed by the cached signer, never a genuinely tampered / mismatched / untrusted one (it re-runs
|
|
762
|
-
// every gate against the cached cert -- a wrong-key message stays valid:false, a wrong sender stays mismatched).
|
|
763
|
-
// A transaction-integrity failure (transactionID / recipNonce) is NOT retried: it is decoy-independent and a
|
|
764
|
-
// real desync, so the cached signer would fail it identically -- retrying would only mask the diagnostic.
|
|
765
|
-
// The fallback signer is the already-authenticated cached signer, or -- before any response has succeeded --
|
|
766
|
-
// the caller's prebound CA certificate (opts.expectedSender). The latter rescues a FIRST response that omits
|
|
767
|
-
// its own extraCerts because the CA assumes the client already holds its certificate (RFC 9483 sec. 3.3): the
|
|
768
|
-
// signer cannot resolve from an empty extraCerts, yet the exact certificate is locally provisioned.
|
|
769
455
|
var usedCachedFallback = false;
|
|
770
456
|
var fallbackSigner = cachedSignerCert != null ? cachedSignerCert : _expectedSenderDer;
|
|
771
457
|
var recoverable = _isRecoverableVerify(verdict.code);
|
|
772
458
|
if ((verdict.valid !== true || verdict.trusted !== true) && isSig && fallbackSigner != null && recoverable) {
|
|
773
|
-
// A CACHED-signer fallback re-verifies against material a PRIOR leg authenticated, so THIS response's
|
|
774
|
-
// extraCerts are decoy-suspect and must NOT overwrite the cache. A PREBORN fallback (no prior cache, the
|
|
775
|
-
// caller's opts.expectedSender) instead verifies this response's OWN extraCerts against the prebound signer,
|
|
776
|
-
// so they ARE authenticated -- cache them, else a later extraCerts-less leg retries with an empty chain.
|
|
777
459
|
usedCachedFallback = cachedSignerCert != null;
|
|
778
460
|
verdict = await cmp.verify(res.responseBytes, _verifyOpts(fresh, fallbackSigner, cachedChain, responseExtra));
|
|
779
461
|
}
|
|
780
462
|
_recordTranscript({ direction: "in", arm: verdict.body ? verdict.body.arm : null, status: res.status, bytes: res.responseBytes, verdict: { valid: verdict.valid, trusted: verdict.trusted, code: verdict.code || null } });
|
|
781
463
|
if (verdict.valid !== true) throw _err(verdict.code || "cmp/protection-failed", "the CMP response protection did not verify (" + (verdict.reason || "invalid") + ") -- the transaction is NOT advanced", null);
|
|
782
|
-
// Cryptographically valid is NOT enough: the signer (signature flavor) must chain to a supplied trust
|
|
783
|
-
// anchor, or the shared secret (MAC flavor) must match -- both surface as verdict.trusted. A valid-but-
|
|
784
|
-
// untrusted response is an unauthenticated signer; fail closed rather than read a certificate off it.
|
|
785
464
|
if (verdict.trusted !== true) throw _err(verdict.code || "cmp/untrusted-signer", "the CMP response protection verified but its signer is not trusted (it did not chain to a supplied trust anchor) -- the transaction is NOT advanced", null);
|
|
786
|
-
// Bind a SIGNED response to the intended CA IDENTITY, not merely to "any trusted signer". A trust anchor that
|
|
787
|
-
// issues to more than one party would otherwise let an on-path holder of ANY chaining digitalSignature cert
|
|
788
|
-
// sign a forged response under its OWN subject and still pass the trust gate. The binding reuses cmp.verify's
|
|
789
|
-
// OWN sender<->certificate rule (RFC 5280 sec. 7 per-type comparison: a directoryName under the sec. 7.1
|
|
790
|
-
// canonical DN comparison, a dNSName / rfc822Name / URI case-folded, every other type by exact DER) so a
|
|
791
|
-
// re-encoded-but-equivalent DN (PrintableString<->UTF8String) and an empty-subject CA named only by a
|
|
792
|
-
// subjectAltName both compare correctly -- neither a raw byte pin nor a subject-string pin can do that.
|
|
793
465
|
if (isSig && verdict.signer && verdict.header && verdict.header.sender) {
|
|
794
|
-
// (1) opts.expectedSender pinned the CA's signer certificate up front: every signed response's authenticated
|
|
795
|
-
// header sender MUST bind to it, enforced from the FIRST response (so a forged first response is caught too).
|
|
796
466
|
if (_expectedSenderCert && !cmp.senderBoundToCert(verdict.header.sender, _expectedSenderCert)) {
|
|
797
467
|
throw _err("cmp/untrusted-signer", "the response signer does not match the expected CA identity (opts.expectedSender) -- refusing a response from a different trusted signer", null);
|
|
798
468
|
}
|
|
799
|
-
|
|
800
|
-
// it under the same rule -- permitting same-identity certificate/key rotation (the sender identity survives a
|
|
801
|
-
// re-encoding) while rejecting a mid-transaction switch to a different trusted signer.
|
|
802
|
-
if (!signerPinned) { pinnedSignerCert = x509.parse(verdict.signer.cert); signerPinned = true; } // a boolean flag: the pin is set once, never re-initialized per leg
|
|
469
|
+
if (!signerPinned) { pinnedSignerCert = x509.parse(verdict.signer.cert); signerPinned = true; }
|
|
803
470
|
else if (!cmp.senderBoundToCert(verdict.header.sender, pinnedSignerCert)) {
|
|
804
471
|
throw _err("cmp/untrusted-signer", "the response signer identity changed mid-transaction -- a different trusted signer, not a same-identity certificate rotation; the transaction is NOT advanced", null);
|
|
805
472
|
}
|
|
806
473
|
}
|
|
807
|
-
lastPeerNonce = verdict.senderNonce;
|
|
474
|
+
lastPeerNonce = verdict.senderNonce;
|
|
808
475
|
haveResponse = true;
|
|
809
|
-
// Refresh the cached signer + its validated chain material from a response that verified ON ITS OWN OR via the
|
|
810
|
-
// PREBORN (opts.expectedSender) fallback -- in both, its extraCerts IS the chain that established trust (the
|
|
811
|
-
// most recently authenticated cert + the intermediates that chained it; a clustered CA may rotate its
|
|
812
|
-
// protection cert across legs), so a later extraCerts-less leg falls back to the current signer AND can rebuild
|
|
813
|
-
// its path. A response that verified only via the CACHED-signer fallback did NOT authenticate its own extraCerts
|
|
814
|
-
// (which may hold a meddler's decoy or omit the real intermediate); keep the existing cached signer + chain --
|
|
815
|
-
// the material that actually established trust -- rather than overwriting it with that response's untrusted pool.
|
|
816
476
|
if (isSig && !usedCachedFallback && verdict.signer && Buffer.isBuffer(verdict.signer.cert)) {
|
|
817
|
-
var extras = _responseExtraCerts(res.responseBytes);
|
|
818
|
-
// Cache the VALIDATED chain cmp.verify assembled (the signer + the intermediates path.build actually used),
|
|
819
|
-
// NOT the raw extraCerts. extraCerts is UNSIGNED: a peer can pad it with the real signer plus unrelated
|
|
820
|
-
// parseable certificates, and caching that raw list (even newest-first, bounded) would evict the issuer a
|
|
821
|
-
// same-identity rotation relied on -- a subsequent bare leg from the rotated signer would then fail to chain.
|
|
822
|
-
// verdict.signer.chain holds ONLY certificates on the trusted path, so the establishing issuer always
|
|
823
|
-
// survives. Fall back to the delivered extraCerts if a chain is somehow absent (defensive; a trusted verdict
|
|
824
|
-
// always carries one). Only refresh when the response delivered its OWN extraCerts (an extraCerts-less leg
|
|
825
|
-
// keeps the prior chain).
|
|
477
|
+
var extras = _responseExtraCerts(res.responseBytes);
|
|
826
478
|
if (extras.length) {
|
|
827
|
-
cachedSignerCert = Buffer.from(verdict.signer.cert);
|
|
479
|
+
cachedSignerCert = Buffer.from(verdict.signer.cert);
|
|
828
480
|
cachedChain = (Array.isArray(verdict.signer.chain) && verdict.signer.chain.length) ? verdict.signer.chain : extras;
|
|
829
481
|
}
|
|
830
482
|
}
|
|
831
483
|
return verdict;
|
|
832
484
|
}
|
|
833
485
|
|
|
834
|
-
// Classify a verified response into a transaction transition. For ip/cp/kup, read the FIRST CertResponse
|
|
835
|
-
// status; error carries a terminal PKIStatusInfo; pollRep drives the loop; any other arm is unexpected.
|
|
836
486
|
function _classify(verdict) {
|
|
837
487
|
var body = verdict.body || {};
|
|
838
488
|
var arm = body.arm;
|
|
839
489
|
if (arm === expectedRespArm && txnKind === "revoke") return _classifyRevRep(body);
|
|
840
490
|
if (arm === expectedRespArm && txnKind === "info") return _classifyGenRep(body);
|
|
841
491
|
if (arm === expectedRespArm) {
|
|
842
|
-
// Select the CertResponse for OUR request by its certReqId (RFC 9810 sec. 5.3.4) -- never blindly
|
|
843
|
-
// response[0], so a batch / misrouted response for a different request id is not read as ours.
|
|
844
492
|
var responses = (body.decoded && body.decoded.response) || [];
|
|
845
493
|
var resp = null;
|
|
846
494
|
for (var ri = 0; ri < responses.length; ri++) { if (_certReqIdEq(responses[ri].certReqId, activeCertReqId)) { resp = responses[ri]; break; } }
|
|
847
495
|
if (!resp) return { state: "unexpected", reason: "a " + arm + " carried no CertResponse for certReqId " + activeCertReqId };
|
|
848
496
|
var code = resp.status && resp.status.status ? resp.status.status.code : null;
|
|
849
497
|
var isGrantLeg = _isGranted(code);
|
|
850
|
-
// RFC 9483 sec. 4.4: a "waiting" CertResponse must not carry failInfo -- a wait is not a failure. Refuse
|
|
851
|
-
// a contradictory failInfo (mirroring the _classifyError waiting check). A granting status carrying
|
|
852
|
-
// failInfo is already refused upstream: the parser rejects failInfo alongside a certifiedKeyPair (RFC 9810
|
|
853
|
-
// sec. 5.3.4), and a grant with no certificate is refused as a missing issued certificate.
|
|
854
498
|
if (code === 3 && resp.status && resp.status.failInfo != null) {
|
|
855
499
|
return { state: "unexpected", reason: "a waiting " + arm + " CertResponse must not carry failInfo (RFC 9483 sec. 4.4)" };
|
|
856
500
|
}
|
|
857
|
-
// Accumulate the AUTHENTICATED caPubs of EVERY ip/cp/kup leg (waiting AND granting): a CA may deliver the
|
|
858
|
-
// issuing chain in a `waiting` response and omit it from the grant, so the leaf's issuer must survive the poll.
|
|
859
|
-
// Bound + validate + dedup the caPubs AS ACCUMULATED, not only when _finish is reached: an authenticated CA
|
|
860
|
-
// can return `waiting` up to the poll budget with large or repeated caPubs, so without a cross-leg cap this
|
|
861
|
-
// array would retain unbounded material before validation. Enforce a count AND a byte budget; a byte-identical
|
|
862
|
-
// repeat is dropped; a non-X.509 entry is REJECTED (fail-closed -- the leaf validation would otherwise defer
|
|
863
|
-
// it to _finish). The GRANT leg's caPubs are the issued leaf's OWN chain (highest priority): they EVICT the
|
|
864
|
-
// oldest WAITING entries to fit ALL of them within the budget, so no fixed reserve can starve the leaf's own
|
|
865
|
-
// required issuer no matter how many waiting caPubs preceded it (a caPubs field carries no MAX_EXTRA_CERTS cap).
|
|
866
501
|
if (body.decoded && Array.isArray(body.decoded.caPubs)) body.decoded.caPubs.forEach(function (c) {
|
|
867
502
|
if (!Buffer.isBuffer(c)) return;
|
|
868
503
|
var k = c.toString("base64");
|
|
869
504
|
if (caPubsSeen[k]) {
|
|
870
|
-
// Already retained (from a waiting leg). On the GRANT leg it is the issued leaf's OWN material -- PROMOTE
|
|
871
|
-
// it to the newest position so a LATER grant entry's eviction cannot drop it while its grant occurrence is
|
|
872
|
-
// skipped as a duplicate. (A waiting-leg duplicate keeps its position; only the grant confers priority.)
|
|
873
505
|
if (isGrantLeg) { for (var pi = 0; pi < caPubsAccum.length; pi++) { if (caPubsAccum[pi].toString("base64") === k) { caPubsAccum.push(caPubsAccum.splice(pi, 1)[0]); if (pi < caPubsWaitingCount) caPubsWaitingCount--; break; } } }
|
|
874
506
|
return;
|
|
875
507
|
}
|
|
876
508
|
try { x509.parse(c); } catch (e) { throw _err("cmp/bad-cert-response", "an authenticated caPubs entry is not a valid X.509 certificate", e); }
|
|
877
509
|
if (isGrantLeg) {
|
|
878
|
-
// Evict ONLY the WAITING entries at the front to fit this grant entry -- never an earlier GRANT entry (the
|
|
879
|
-
// issued leaf's own chain). Once no waiting entries remain, this grant entry is dropped by the cap below.
|
|
880
510
|
while (caPubsWaitingCount > 0 && (caPubsAccum.length >= CAPUBS_MAX || caPubsBytes + c.length > caPubsByteBudget)) {
|
|
881
511
|
var evicted = caPubsAccum.shift();
|
|
882
512
|
caPubsBytes -= evicted.length;
|
|
883
513
|
caPubsWaitingCount--;
|
|
884
|
-
delete caPubsSeen[evicted.toString("base64")];
|
|
514
|
+
delete caPubsSeen[evicted.toString("base64")];
|
|
885
515
|
}
|
|
886
516
|
}
|
|
887
|
-
if (caPubsAccum.length >= CAPUBS_MAX || caPubsBytes + c.length > caPubsByteBudget) return;
|
|
517
|
+
if (caPubsAccum.length >= CAPUBS_MAX || caPubsBytes + c.length > caPubsByteBudget) return;
|
|
888
518
|
caPubsSeen[k] = 1;
|
|
889
519
|
caPubsBytes += c.length;
|
|
890
|
-
caPubsAccum.push(Buffer.from(c));
|
|
891
|
-
if (!isGrantLeg) caPubsWaitingCount++;
|
|
520
|
+
caPubsAccum.push(Buffer.from(c));
|
|
521
|
+
if (!isGrantLeg) caPubsWaitingCount++;
|
|
892
522
|
});
|
|
893
523
|
if (isGrantLeg) return { state: "granted", resp: resp, code: code };
|
|
894
524
|
if (code === 3) return { state: "waiting", resp: resp };
|
|
@@ -901,36 +531,18 @@ function session(opts) {
|
|
|
901
531
|
return { state: "unexpected", reason: "response arm " + JSON.stringify(arm) + " has no transition in a " + txnKind + " transaction" };
|
|
902
532
|
}
|
|
903
533
|
|
|
904
|
-
// An error body. For a revocation or a support message it is ALSO how delayed delivery is announced:
|
|
905
|
-
// RFC 9483 sec. 4.4 puts the "waiting" status in an ip/cp/kup for ir/cr/kur/p10cr "and for responses
|
|
906
|
-
// to other request message types in an error message". An error answering an ENROLLMENT therefore
|
|
907
|
-
// stays a rejection -- that operation announces its own delay in its own cert-response arm, so
|
|
908
|
-
// reading waiting out of an error there would accept a shape the profile does not produce.
|
|
909
534
|
function _classifyError(body) {
|
|
910
535
|
var si = body.decoded && body.decoded.pKIStatusInfo;
|
|
911
536
|
var code = si && si.status ? si.status.code : null;
|
|
912
537
|
if (code === 3) {
|
|
913
|
-
// sec. 4.4 puts waiting in an ip/cp/kup when answering an enrollment and in an error message for
|
|
914
|
-
// anything else, so a waiting error drives the poll loop only for revoke/info. For an enrollment a
|
|
915
|
-
// waiting ERROR is off-profile and refused, not coerced to a rejection whose own status reads
|
|
916
|
-
// "waiting". A wait is not a failure, so sec. 4.4 forbids failInfo on it -- a waiting error carrying
|
|
917
|
-
// one is contradictory and refused rather than polled.
|
|
918
538
|
if (txnKind === "enroll") throw _err("cmp/bad-error", "an error message answering an enrollment must not carry status waiting -- the profile places enrollment waiting in an ip/cp/kup (RFC 9483 sec. 4.4)");
|
|
919
539
|
if (si.failInfo != null) throw _err("cmp/bad-error", "an error message with status waiting must not carry failInfo (RFC 9483 sec. 4.4)");
|
|
920
540
|
return { state: "waiting", resp: { status: si } };
|
|
921
541
|
}
|
|
922
|
-
// sec. 4.2 / 3.6.4: an error message that is not delayed delivery is a rejection, whose status is
|
|
923
|
-
// rejection(2). accepted(0), grantedWithMods(1), a status in 4..6, an unknown value, or an absent
|
|
924
|
-
// status is malformed for an error message, so it is refused rather than surfaced as a definitive
|
|
925
|
-
// rejected verdict a caller would act on -- the same call the rp classifier makes for an unsupported
|
|
926
|
-
// status (a trusted "rejected" whose PKIStatusInfo says "accepted" is a contradiction, not a verdict).
|
|
927
542
|
if (code !== 2) throw _err("cmp/bad-error", "an error message carries status rejection(2), or waiting(3) for a revoke/info delayed delivery (RFC 9483 sec. 4.2); got " + (code == null ? "no status" : code));
|
|
928
543
|
return { state: "rejected", status: si };
|
|
929
544
|
}
|
|
930
545
|
|
|
931
|
-
// RevRepContent. RFC 9483 sec. 4.2: the response to an rr "MUST be an rp message containing a single
|
|
932
|
-
// status field", whose positive value is "accepted" and negative "rejection", with failInfo "MUST be
|
|
933
|
-
// absent if the status is accepted". A status with no transition here is a hard stop, never a verdict.
|
|
934
546
|
function _classifyRevRep(body) {
|
|
935
547
|
var d = body.decoded || {};
|
|
936
548
|
var list = d.status || [];
|
|
@@ -949,9 +561,6 @@ function session(opts) {
|
|
|
949
561
|
return { state: "unexpected", reason: "an rp status code " + code + " has no transition in a revocation (RFC 9483 sec. 4.2 allows accepted or rejection)" };
|
|
950
562
|
}
|
|
951
563
|
|
|
952
|
-
// GenRepContent. RFC 9483 sec. 4.3: a genp "MUST contain a sequence of one element of type
|
|
953
|
-
// InfoTypeAndValue" whose infoType names the operation. The OID is checked against the RESPONSE
|
|
954
|
-
// side of the INFO_OPS row, which for rootCaCert is not the OID the request carried.
|
|
955
564
|
function _classifyGenRep(body) {
|
|
956
565
|
var items = body.decoded || [];
|
|
957
566
|
if (items.length !== 1) {
|
|
@@ -967,8 +576,6 @@ function session(opts) {
|
|
|
967
576
|
|
|
968
577
|
function _statusOf(resp) { return resp && resp.status ? resp.status : null; }
|
|
969
578
|
|
|
970
|
-
// A granted implicitConfirm in the RESPONSE header's generalInfo (RFC 9810 sec. 5.1.1.1) -- classified by
|
|
971
|
-
// the IMMUTABLE id-it OID, never the mutable display name.
|
|
972
579
|
function _implicitConfirmGranted(header) {
|
|
973
580
|
var gi = header && header.generalInfo;
|
|
974
581
|
if (!Array.isArray(gi)) return false;
|
|
@@ -976,12 +583,6 @@ function session(opts) {
|
|
|
976
583
|
return false;
|
|
977
584
|
}
|
|
978
585
|
|
|
979
|
-
// Append a transcript entry, bounding the RETAINED payload bytes transaction-wide. A polling enrollment runs
|
|
980
|
-
// many legs and an on-path party can pad every otherwise-valid signed response with unsigned extraCerts up to
|
|
981
|
-
// the per-response cap; retaining each full payload would multiply that cap across legs into memory exhaustion.
|
|
982
|
-
// Once the running total crosses the ceiling, the entry keeps its metadata + `byteLength` but drops the payload
|
|
983
|
-
// (`bytes: null, truncated: true`) so the internal reference is released to GC and the transcript stays a
|
|
984
|
-
// fixed-size diagnostic. Normal kilobyte messages never reach the ceiling, so the payloads are retained intact.
|
|
985
586
|
function _recordTranscript(entry) {
|
|
986
587
|
var len = Buffer.isBuffer(entry.bytes) ? entry.bytes.length : 0;
|
|
987
588
|
if (transcriptBytes + len > transcriptCap) {
|
|
@@ -992,9 +593,6 @@ function session(opts) {
|
|
|
992
593
|
transcript.push(entry);
|
|
993
594
|
}
|
|
994
595
|
|
|
995
|
-
// A defensive SNAPSHOT of the transcript: a fresh array of shallow-copied entries with copied byte Buffers, so
|
|
996
|
-
// freezing or mutating the returned value cannot break the internal transcript (_send keeps appending to it)
|
|
997
|
-
// nor mutate a Buffer still shared with an in-flight request/response body.
|
|
998
596
|
function _transcriptSnapshot() {
|
|
999
597
|
return transcript.map(function (e) {
|
|
1000
598
|
var c = Object.assign({}, e);
|
|
@@ -1004,17 +602,13 @@ function session(opts) {
|
|
|
1004
602
|
});
|
|
1005
603
|
}
|
|
1006
604
|
function _terminal(outcome, extra) {
|
|
1007
|
-
// Every terminal outcome is reached only through _send, which throws unless the response was both valid
|
|
1008
|
-
// AND trusted -- so a returned verdict is always authenticated (trusted:true), surfaced for the caller.
|
|
1009
605
|
return Object.assign({
|
|
1010
606
|
outcome: outcome, certificate: null, chain: [], status: null, trusted: true,
|
|
1011
|
-
confirmed: false, implicitConfirm: false, transactionID: Buffer.from(transactionID),
|
|
607
|
+
confirmed: false, implicitConfirm: false, transactionID: Buffer.from(transactionID),
|
|
1012
608
|
polls: 0, transcript: _transcriptSnapshot(),
|
|
1013
609
|
}, extra);
|
|
1014
610
|
}
|
|
1015
611
|
|
|
1016
|
-
// The bounded pollReq/pollRep loop (RFC 9810 sec. 5.3.22): sleep >= checkAfter each round, re-classify a
|
|
1017
|
-
// non-pollRep response as an initial response, END as poll-timeout when maxPolls / maxTotalWait is hit.
|
|
1018
612
|
async function _pollLoop(lastWaitingResp) {
|
|
1019
613
|
var polls = 0, waited = 0, lastStatus = _statusOf(lastWaitingResp);
|
|
1020
614
|
for (;;) {
|
|
@@ -1023,9 +617,6 @@ function session(opts) {
|
|
|
1023
617
|
polls += 1;
|
|
1024
618
|
var t = _classify(verdict);
|
|
1025
619
|
if (t.state === "pollRep") {
|
|
1026
|
-
// Select the pollRep entry for OUR request by certReqId (RFC 9810 sec. 5.3.22) -- never entry zero,
|
|
1027
|
-
// so an unrelated entry's checkAfter cannot stall us and a pollRep that never mentions our request
|
|
1028
|
-
// (its entry absent) is a misrouted response, not a silent zero-delay re-poll.
|
|
1029
620
|
var entries = t.entries || [];
|
|
1030
621
|
var entry = null;
|
|
1031
622
|
for (var pe = 0; pe < entries.length; pe++) { if (_certReqIdEq(entries[pe].certReqId, activeCertReqId)) { entry = entries[pe]; break; } }
|
|
@@ -1033,42 +624,24 @@ function session(opts) {
|
|
|
1033
624
|
var checkAfter = typeof entry.checkAfter === "number" ? entry.checkAfter : 0;
|
|
1034
625
|
waited += checkAfter;
|
|
1035
626
|
if (waited > maxTotalWait) return { timeout: true, polls: polls, status: lastStatus };
|
|
1036
|
-
// Do NOT sleep after the FINAL permitted poll: this pollRep answered poll number maxPolls, so the
|
|
1037
|
-
// budget is already spent -- sleeping its checkAfter (up to a year) before the loop-top count check
|
|
1038
|
-
// would defeat the poll-count bound. Detect exhaustion here, before scheduling the delay.
|
|
1039
627
|
if (polls >= maxPolls) return { timeout: true, polls: polls, status: lastStatus };
|
|
1040
628
|
await sleep(checkAfter * constants.TIME.seconds(1));
|
|
1041
629
|
continue;
|
|
1042
630
|
}
|
|
1043
|
-
if (t.state === "waiting") { lastStatus = _statusOf(t.resp); continue; }
|
|
1044
|
-
// granted / rejected / unexpected -> advance out of the loop, carrying the GRANTING response's header
|
|
1045
|
-
// (an implicitConfirm grant / a fresh nonce lives in THIS response, not the initial waiting one).
|
|
631
|
+
if (t.state === "waiting") { lastStatus = _statusOf(t.resp); continue; }
|
|
1046
632
|
return { done: t, polls: polls, header: verdict.header };
|
|
1047
633
|
}
|
|
1048
634
|
}
|
|
1049
635
|
|
|
1050
|
-
// Confirm delivery (RFC 9810 sec. 5.1.1.1 / 5.3.18): if implicitConfirm was granted, END; else send a
|
|
1051
|
-
// certConf (certHash under the cert's signature hash) and require a verified pkiConf, else cmp/bad-confirmation.
|
|
1052
|
-
// `info` carries the grant status so an acceptance policy can veto a grantedWithMods certificate.
|
|
1053
636
|
async function _confirm(certDer, header, info) {
|
|
1054
|
-
// Honor an implicitConfirm grant ONLY when the caller REQUESTED it (opts.implicitConfirm): implicit
|
|
1055
|
-
// confirmation is negotiated (RFC 9810 sec. 5.1.1.1), so an UNSOLICITED implicitConfirm from a nonconforming
|
|
1056
|
-
// server is ignored -- the session performs the explicit certConf the caller expects. No acceptance policy
|
|
1057
|
-
// runs here: acceptCert + implicitConfirm is refused at construction (there is no reject leg to honor a veto).
|
|
1058
637
|
if (opts.implicitConfirm && _implicitConfirmGranted(header)) return { confirmed: true, implicit: true };
|
|
1059
|
-
// Consult the caller's acceptance policy before the explicit confirmation. A grantedWithMods response (RFC
|
|
1060
|
-
// 9810 sec. 5.2.3) carries a certificate the CA CHANGED (subject / validity / extensions), and an accepting
|
|
1061
|
-
// certConf is irrevocable, so the caller may inspect + veto it here. The policy MUST return true to accept;
|
|
1062
|
-
// anything else is a veto (a thrown rejection is NOT swallowed -- it propagates). No policy accepts (default).
|
|
1063
638
|
var accept = true;
|
|
1064
639
|
if (typeof opts.acceptCert === "function") accept = (await opts.acceptCert(Buffer.from(certDer), info)) === true;
|
|
1065
640
|
var h = _certConfHash(certDer);
|
|
1066
641
|
var certHash = Buffer.from(await webcrypto.webcrypto.subtle.digest(h.digest, certDer));
|
|
1067
642
|
var cs = { certHash: certHash, certReqId: activeCertReqId };
|
|
1068
|
-
// A veto sends a REJECTING certConf (CertStatus.statusInfo status rejection, RFC 9810 sec. 5.3.18) so the CA
|
|
1069
|
-
// learns the EE declined and does not treat the certificate as delivered; an acceptance omits statusInfo.
|
|
1070
643
|
if (!accept) cs.statusInfo = { status: 2, statusString: ["the enrolling client rejected the issued certificate"] };
|
|
1071
|
-
if (h.hashAlg) cs.hashAlg = h.hashAlg;
|
|
644
|
+
if (h.hashAlg) cs.hashAlg = h.hashAlg;
|
|
1072
645
|
var verdict = await _send({ certConf: [cs] }, "certConf");
|
|
1073
646
|
if (!verdict.body || verdict.body.arm !== "pkiconf") throw _err("cmp/bad-confirmation", "expected a pkiConf acknowledgement to the certConf but got " + JSON.stringify(verdict.body && verdict.body.arm) + " (RFC 9810 sec. 5.3.18)");
|
|
1074
647
|
return { confirmed: accept, implicit: false, rejected: !accept };
|
|
@@ -1078,28 +651,16 @@ function session(opts) {
|
|
|
1078
651
|
var ckp = resp && resp.certifiedKeyPair;
|
|
1079
652
|
var cert = ckp && ckp.certificate;
|
|
1080
653
|
if (!Buffer.isBuffer(cert)) throw _err("cmp/unexpected-arm", "a granted CertResponse carried no plain issued certificate (an encryptedCert form is out of enrollment v1 scope) (RFC 9810 sec. 5.3.4)");
|
|
1081
|
-
// A central-key-generation privateKey (the CA generated the key, delivered encrypted) is out of a
|
|
1082
|
-
// client-key enrollment session's scope: the caller submitted the key + its proof of possession, so the
|
|
1083
|
-
// session would otherwise confirm a certificate whose private key it discarded -- a result the caller cannot use.
|
|
1084
654
|
if (ckp.privateKey != null) throw _err("cmp/unexpected-arm", "a granted CertResponse carried a server-generated privateKey (central key generation is out of enrollment v1 scope; a session enrolls a client-generated key) (RFC 9810 sec. 5.3.4)");
|
|
1085
|
-
// The CMP parser surfaces certifiedKeyPair.certificate as opaque bytes (any non-empty SEQUENCE); a
|
|
1086
|
-
// non-conformant / hostile CA could deliver bytes that are not a real X.509 certificate. Validate them
|
|
1087
|
-
// BEFORE confirming, so the session never returns outcome:issued with a value pki.schema.x509.parse rejects.
|
|
1088
655
|
var parsed;
|
|
1089
656
|
try { parsed = x509.parse(cert); }
|
|
1090
657
|
catch (e) { throw _err("cmp/bad-cert-response", "the granted CertResponse's certificate is not a valid X.509 certificate", e); }
|
|
1091
|
-
// For a client-generated-key enrollment, the issued cert MUST carry the key the request submitted -- else
|
|
1092
|
-
// it is a misrouted certificate the caller holds no private key for. Compare the KEY identity (alg OID +
|
|
1093
|
-
// key bits), not the raw SPKI bytes, so an equivalent re-encoding (rsaEncryption NULL vs omitted) matches.
|
|
1094
658
|
if (requestedSpki != null && _spkiKeyIdentity(parsed.subjectPublicKeyInfo.bytes) !== _spkiKeyIdentity(requestedSpki)) {
|
|
1095
659
|
throw _err("cmp/bad-cert-response", "the issued certificate's public key does not match the requested key -- a misrouted certificate the caller has no private key for");
|
|
1096
660
|
}
|
|
1097
661
|
return cert;
|
|
1098
662
|
}
|
|
1099
663
|
|
|
1100
|
-
// The SPKI DER the enrollment requested, for the issued-cert key match: an ir/cr/kur certTemplate.publicKey
|
|
1101
|
-
// (a SPKI Buffer), or a p10cr CSR's subjectPublicKeyInfo. null when the form is not a plain SPKI/CSR (the
|
|
1102
|
-
// build path validates the key separately) or the CSR is unparseable (which fails at the build boundary).
|
|
1103
664
|
function _extractRequestedSpki(arm, armSpec) {
|
|
1104
665
|
if (arm === "p10cr") {
|
|
1105
666
|
if (!guard.bytes.isByteSource(armSpec)) return null;
|
|
@@ -1107,41 +668,22 @@ function session(opts) {
|
|
|
1107
668
|
catch (_e) { /* allow:swallow-unverified an unparseable p10cr CSR fails closed at the cmp.build boundary; the key-match is simply not applied to a request that never sends */ return null; }
|
|
1108
669
|
}
|
|
1109
670
|
var pk = armSpec && armSpec.certTemplate ? armSpec.certTemplate.publicKey : null;
|
|
1110
|
-
// SNAPSHOT: this is held in session state across the transport round trip and then compared
|
|
1111
|
-
// against the issued certificate's key, so an alias would let the caller rewrite the key the
|
|
1112
|
-
// response is checked against after the request went out.
|
|
1113
671
|
if (guard.bytes.isByteSource(pk)) return guard.bytes.snapshotSource(pk, CmpError, "cmp/bad-input", "the certTemplate publicKey");
|
|
1114
672
|
return null;
|
|
1115
673
|
}
|
|
1116
674
|
|
|
1117
|
-
// Validate the issued leaf's SIGNATURE + chain (pki.schema.x509.parse is structural only, so a corrupted
|
|
1118
|
-
// signature would otherwise be confirmed) whenever trust anchors are supplied. A signature session always has
|
|
1119
|
-
// them (required); a MAC session validates the issued cert ONLY when the caller opts in with anchors -- the
|
|
1120
|
-
// response MAC authenticates the exchange, NOT the embedded X.509 signature, so without anchors an invalid-sig
|
|
1121
|
-
// MAC-issued cert would otherwise be confirmed. The pool prioritizes the AUTHENTICATED, leaf-relevant caPubs
|
|
1122
|
-
// (the CA's OWN issuer chain for this leaf) as the base -- never evicted by a caller pool that fills the
|
|
1123
|
-
// ceiling -- then the caller's intermediates, then the cached protection-signer chain, deduped + ceiling-bounded.
|
|
1124
675
|
async function _validateLeaf(leaf, caPubs) {
|
|
1125
676
|
if (_engine == null || opts.trustAnchors == null) return;
|
|
1126
677
|
var caPubsList = [];
|
|
1127
678
|
if (Array.isArray(caPubs)) caPubs.forEach(function (c) { if (Buffer.isBuffer(c)) caPubsList.push(c); });
|
|
1128
|
-
var added = _asCertList(opts.intermediates);
|
|
1129
|
-
if (Buffer.isBuffer(cachedSignerCert)) added.push(cachedSignerCert);
|
|
1130
|
-
// cachedChain holds the VALIDATED signer chain from cmp.verify as independent DER copies (or delivered
|
|
1131
|
-
// extraCerts DER on the defensive fallback) -- path.build's coerceCert parses each, so add them directly.
|
|
679
|
+
var added = _asCertList(opts.intermediates);
|
|
680
|
+
if (Buffer.isBuffer(cachedSignerCert)) added.push(cachedSignerCert);
|
|
1132
681
|
cachedChain.forEach(function (c) { if (Buffer.isBuffer(c)) added.push(c); });
|
|
1133
|
-
// ONE candidate pool holds everything: the AUTHENTICATED caPubs (the CA's OWN issuer chain for this leaf) as the
|
|
1134
|
-
// priority base -- never evicted -- then the whole caller pool + cached signer chain. All fit under the ceiling
|
|
1135
|
-
// (caPubs <= CAPUBS_MAX, cached chain <= MAX_EXTRA_CERTS, caller <= SESSION_MAX_INTERMEDIATES = ceiling minus
|
|
1136
|
-
// both), so a path assembled from the caPubs AND a late caller intermediate builds in this single attempt.
|
|
1137
682
|
var pool = _boundedPool(caPubsList, added);
|
|
1138
683
|
var res;
|
|
1139
684
|
try { res = await _engine.build(leaf, { trustAnchors: _asCertList(opts.trustAnchors), intermediates: pool, validate: true, time: opts.time != null ? opts.time : new Date() }); }
|
|
1140
685
|
catch (e) {
|
|
1141
686
|
if (e.code === "path/bad-input") throw _err("cmp/bad-input", "invalid trust / validation options for issued-certificate validation: " + (e.message || e), e);
|
|
1142
|
-
// path.build THROWS path/no-path when the issued leaf cannot chain to a supplied anchor -- a CA that grants a
|
|
1143
|
-
// certificate whose issuer it never delivered. Re-type it to the domain error so the transaction fails closed
|
|
1144
|
-
// before confirmation, never leaking a path/* code.
|
|
1145
687
|
throw _err("cmp/bad-cert-response", "the issued certificate could not be validated to a supplied trust anchor: " + (e.message || e), e);
|
|
1146
688
|
}
|
|
1147
689
|
if (!res || res.valid !== true) throw _err("cmp/bad-cert-response", "the issued certificate did not validate to a supplied trust anchor (its signature or chain is invalid) (RFC 5280 sec. 6.1)");
|
|
@@ -1149,12 +691,6 @@ function session(opts) {
|
|
|
1149
691
|
|
|
1150
692
|
async function _finish(granted, header) {
|
|
1151
693
|
var leaf = _leafOf(granted.resp);
|
|
1152
|
-
// The returned chain is the issued leaf followed by the authenticated caPubs accumulated across the WHOLE
|
|
1153
|
-
// transaction (the issuer certs the CA delivered on any ip/cp/kup leg, RFC 9810 sec. 5.3.4) -- surfaced so a
|
|
1154
|
-
// caller whose intermediate arrived only on a `waiting` leg can still assemble the full chain. They are chain
|
|
1155
|
-
// material, NOT trust anchors. The CMP parser surfaces each caPubs entry as a raw sequence, so validate every
|
|
1156
|
-
// one as X.509 BEFORE the certConf acknowledges the grant -- the returned chain must never carry bytes
|
|
1157
|
-
// pki.schema.x509.parse rejects -- and deduplicate a chain re-delivered across the waiting + granting legs.
|
|
1158
694
|
var chain = [leaf], seenChain = Object.create(null);
|
|
1159
695
|
caPubsAccum.forEach(function (c) {
|
|
1160
696
|
if (!Buffer.isBuffer(c)) return;
|
|
@@ -1165,16 +701,10 @@ function session(opts) {
|
|
|
1165
701
|
if (k != null) seenChain[k] = 1;
|
|
1166
702
|
chain.push(c);
|
|
1167
703
|
});
|
|
1168
|
-
await _validateLeaf(leaf, caPubsAccum);
|
|
704
|
+
await _validateLeaf(leaf, caPubsAccum);
|
|
1169
705
|
var info = { status: PKI_STATUS_NAMES[granted.code] || granted.code, grantedWithMods: granted.code === 1 };
|
|
1170
706
|
var conf = await _confirm(leaf, header, info);
|
|
1171
|
-
// Surface COPIES of the issued certificate + chain, not the internal Buffers (slices of the response bytes),
|
|
1172
|
-
// so a caller mutating the returned value cannot reach back into session state -- consistent with the
|
|
1173
|
-
// transactionID + transcript defensive copies.
|
|
1174
707
|
var certOut = Buffer.from(leaf), chainOut = chain.map(function (c) { return Buffer.from(c); });
|
|
1175
|
-
// A caller acceptance-policy veto (typically of a grantedWithMods certificate) is a terminal `rejected`
|
|
1176
|
-
// outcome that still surfaces the certificate the caller inspected -- the accepting certConf was replaced by
|
|
1177
|
-
// a rejecting one (explicit path) so the CA does not treat it as delivered.
|
|
1178
708
|
if (conf.rejected) {
|
|
1179
709
|
return _terminal("rejected", { certificate: certOut, chain: chainOut, status: _statusOf(granted.resp), polls: granted.polls || 0 });
|
|
1180
710
|
}
|
|
@@ -1184,23 +714,15 @@ function session(opts) {
|
|
|
1184
714
|
});
|
|
1185
715
|
}
|
|
1186
716
|
|
|
1187
|
-
// One transaction per session (RFC 9810 sec. 5.1.1: one transactionID per transaction). A second or
|
|
1188
|
-
// concurrent call on any verb would reuse this transaction's transactionID / nonce chain / certReqId --
|
|
1189
|
-
// a replay or a corrupted interleave -- so it is refused. Every verb checks this BEFORE marking the
|
|
1190
|
-
// transaction in flight, so a malformed-request call does not consume the session.
|
|
1191
717
|
function _assertFresh(what) {
|
|
1192
718
|
if (completed || inFlight) {
|
|
1193
719
|
throw _err("cmp/bad-input", "this pki.cmp.session transaction is already " + (completed ? "completed" : "in flight") + "; create a new session per " + what + " (RFC 9810 sec. 5.1.1: one transactionID per transaction)");
|
|
1194
720
|
}
|
|
1195
721
|
}
|
|
1196
722
|
|
|
1197
|
-
// The shared tail of a non-enrollment transaction: send the one request, poll through a delayed
|
|
1198
|
-
// delivery, and hand back the terminal classification. Both verbs reach a verdict the same way, so
|
|
1199
|
-
// the poll budget, the nonce chaining and the verify-before-read ordering cannot differ between them.
|
|
1200
723
|
async function _runOneShot(bodySpec, arm, kind) {
|
|
1201
724
|
txnKind = kind;
|
|
1202
725
|
expectedRespArm = RESPONSE_ARM[arm];
|
|
1203
|
-
// A pollReq raised by this transaction refers to the whole message (RFC 9483 sec. 4.4).
|
|
1204
726
|
activeCertReqId = WHOLE_MESSAGE_CERT_REQ_ID;
|
|
1205
727
|
var verdict = await _send(bodySpec, arm);
|
|
1206
728
|
var t = _classify(verdict);
|
|
@@ -1214,69 +736,44 @@ function session(opts) {
|
|
|
1214
736
|
return { done: t, polls: polls };
|
|
1215
737
|
}
|
|
1216
738
|
|
|
1217
|
-
// The transaction driver: build the initial request arm, send it, then poll / confirm / terminate.
|
|
1218
739
|
async function enroll(request) {
|
|
1219
740
|
_assertFresh("enrollment");
|
|
1220
741
|
guard.identifier.assertPlainRecord(request, _err, "cmp/bad-input", "enroll(request), a body spec { ir | cr | kur | p10cr },");
|
|
1221
|
-
// A key here that is not an arm needs no door of its own: this whole object becomes the message
|
|
1222
|
-
// body, and pki.cmp.build counts the body's keys without filtering out the null-valued ones, so
|
|
1223
|
-
// a misspelling set to null is already refused there ("must have exactly one arm, got 2"). The
|
|
1224
|
-
// info() request is the opposite case and does carry a door, because that object is read for a
|
|
1225
|
-
// name and never handed to the builder.
|
|
1226
742
|
var arms = Object.keys(request).filter(function (k) { return request[k] != null; });
|
|
1227
743
|
if (arms.length !== 1 || !ENROLL_ARMS[arms[0]]) {
|
|
1228
744
|
throw _err("cmp/bad-input", "enroll(request) must carry EXACTLY ONE enrollment arm (ir / cr / kur / p10cr)");
|
|
1229
745
|
}
|
|
1230
|
-
// A batched CRMF request ({ messages: [...] }) asks for several certificates in one message; this session
|
|
1231
|
-
// drives ONE request (single transactionID, one certReqId, one certConf), so a batch is refused rather than
|
|
1232
|
-
// silently confirming only the first CertResponse and leaving the rest unprocessed. Submit one per session.
|
|
1233
746
|
var armSpec = request[arms[0]];
|
|
1234
747
|
if (armSpec && typeof armSpec === "object" && !Buffer.isBuffer(armSpec) && armSpec.messages != null) {
|
|
1235
748
|
throw _err("cmp/bad-input", "a batched CRMF request ({ messages: [...] }) is not supported by a session -- submit one certificate request per pki.cmp.session");
|
|
1236
749
|
}
|
|
1237
|
-
// The session enrolls a CLIENT-submitted key: an ir / cr / kur MUST carry certTemplate.publicKey (a p10cr
|
|
1238
|
-
// carries it in the CSR). A keyless request (e.g. raVerified POP with no publicKey, or central key
|
|
1239
|
-
// generation) is refused -- else the session would confirm an arbitrary issued key it can neither match
|
|
1240
|
-
// to a submitted key nor return a private key for. Checked before the transaction engages the transport.
|
|
1241
750
|
var reqSpki = _extractRequestedSpki(arms[0], armSpec);
|
|
1242
751
|
if (arms[0] !== "p10cr" && reqSpki == null) {
|
|
1243
752
|
throw _err("cmp/bad-input", "an ir / cr / kur enrollment must submit certTemplate.publicKey -- a session enrolls a client-generated key (a raVerified keyless request or central key generation is not supported)");
|
|
1244
753
|
}
|
|
1245
|
-
// The session proves possession by SIGNING the CRMF proof of possession with the requested key (opts.key for
|
|
1246
|
-
// a signature session, the arm-local `key` for a MAC session). A raVerified / other non-signature POP override
|
|
1247
|
-
// would make crmf.build emit THAT mode and skip the signature entirely -- no requester proof of possession --
|
|
1248
|
-
// so refuse it: it would also bypass the MAC key requirement below (a present key is ignored under raVerified).
|
|
1249
754
|
if (arms[0] !== "p10cr" && armSpec != null && armSpec.pop != null && armSpec.pop.type != null && armSpec.pop.type !== "signature") {
|
|
1250
755
|
throw _err("cmp/bad-input", "a session ir / cr / kur proves possession by signing the CRMF proof of possession; a non-signature POP mode (" + JSON.stringify(armSpec.pop.type) + ") is not supported (RFC 4211 sec. 4)");
|
|
1251
756
|
}
|
|
1252
|
-
// A signature session signs the CRMF proof of possession with opts.key; a PBMAC1 session has NO signing key,
|
|
1253
|
-
// so an ir / cr / kur under MAC protection MUST carry the requested key's private half as the arm-local
|
|
1254
|
-
// `key`. Without it crmf.build emits NO proof of possession (RFC 4211 sec. 4) and a POP-enforcing CA rejects
|
|
1255
|
-
// the request. crmf.build then verifies the POP signature against certTemplate.publicKey, so a WRONG key
|
|
1256
|
-
// fails at build -- here we only require the key is present. (A p10cr proves possession via the CSR signature.)
|
|
1257
757
|
if (isMac && arms[0] !== "p10cr" && (armSpec == null || armSpec.key == null)) {
|
|
1258
758
|
throw _err("cmp/bad-input", "a MAC-protected ir / cr / kur must carry the requested key's private half as `key` for the CRMF proof of possession -- a signature session reuses opts.key, but a PBMAC1 session has no signing key, so the request would carry no proof of possession (RFC 4211 sec. 4)");
|
|
1259
759
|
}
|
|
1260
760
|
inFlight = true;
|
|
1261
761
|
try {
|
|
1262
762
|
txnKind = "enroll";
|
|
1263
|
-
expectedRespArm = RESPONSE_ARM[arms[0]];
|
|
1264
|
-
// The certReqId the session echoes in pollReq / certConf and matches in every CertResponse: the caller's
|
|
1265
|
-
// CRMF request id (ir / cr / kur) when supplied, else the arm default -- a p10cr has no CRMF id, so a
|
|
1266
|
-
// conforming cp identifies it with the -1 sentinel (RFC 9483), while ir / cr / kur default to 0.
|
|
763
|
+
expectedRespArm = RESPONSE_ARM[arms[0]];
|
|
1267
764
|
var cid = (typeof armSpec === "object" && !Buffer.isBuffer(armSpec)) ? armSpec.certReqId : undefined;
|
|
1268
765
|
activeCertReqId = _normalizeCertReqId(cid, arms[0] === "p10cr" ? P10CR_CERT_REQ_ID : DEFAULT_CERT_REQ_ID);
|
|
1269
|
-
requestedSpki = reqSpki;
|
|
766
|
+
requestedSpki = reqSpki;
|
|
1270
767
|
var initial = await _send(request, arms[0]);
|
|
1271
768
|
var t = _classify(initial);
|
|
1272
|
-
var grantHeader = initial.header;
|
|
769
|
+
var grantHeader = initial.header;
|
|
1273
770
|
var pollCount = 0;
|
|
1274
771
|
if (t.state === "waiting") {
|
|
1275
772
|
var polled = await _pollLoop(t.resp);
|
|
1276
773
|
pollCount = polled.polls;
|
|
1277
774
|
if (polled.timeout) return _terminal("poll-timeout", { status: polled.status, polls: pollCount });
|
|
1278
775
|
t = polled.done;
|
|
1279
|
-
grantHeader = polled.header;
|
|
776
|
+
grantHeader = polled.header;
|
|
1280
777
|
t.polls = pollCount;
|
|
1281
778
|
}
|
|
1282
779
|
if (t.state === "granted") { var r = await _finish({ resp: t.resp, polls: pollCount, code: t.code }, grantHeader); r.polls = pollCount; return r; }
|
|
@@ -1284,19 +781,10 @@ function session(opts) {
|
|
|
1284
781
|
throw _err("cmp/unexpected-arm", t.reason || "the enrollment transaction reached an unexpected state");
|
|
1285
782
|
} finally {
|
|
1286
783
|
inFlight = false;
|
|
1287
|
-
// Consume the session only if a request may have reached the transport. A purely LOCAL failure (a build
|
|
1288
|
-
// error before the first transfer -- e.g. a missing CRMF template) leaves the session retryable.
|
|
1289
784
|
completed = started;
|
|
1290
785
|
}
|
|
1291
786
|
}
|
|
1292
787
|
|
|
1293
|
-
// RFC 9483 sec. 4.2: "The revocation request message MUST be signed using the certificate that is to
|
|
1294
|
-
// be revoked to prove the authorization to revoke." The signature over this message IS the
|
|
1295
|
-
// authorization, so the certificate named in certDetails has to be the one this session protects with.
|
|
1296
|
-
// Both request shapes are compared here, through the same encode-then-read path, so the certificate
|
|
1297
|
-
// form and the explicit certDetails form cannot diverge on what counts as the same certificate.
|
|
1298
|
-
// The issuer comparison is the RFC 5280 sec. 7.1 canonical one, since two spellings of a
|
|
1299
|
-
// distinguished name name the same issuer and a byte compare would refuse a caller who wrote it out.
|
|
1300
788
|
function _assertRevokingOwnCertificate(certTemplateDer) {
|
|
1301
789
|
var own;
|
|
1302
790
|
try {
|
|
@@ -1311,16 +799,9 @@ function session(opts) {
|
|
|
1311
799
|
if (!sameSerial || !sameIssuer) {
|
|
1312
800
|
throw _err("cmp/bad-input", "a session revokes ITS OWN certificate: the issuer and serialNumber in certDetails must name opts.cert, because the signature over the request is the proof of authorization to revoke (RFC 9483 sec. 4.2). Revocation on behalf of another entity is an RA operation -- drive it with pki.cmp.build + pki.cmp.transfer.");
|
|
1313
801
|
}
|
|
1314
|
-
return own;
|
|
802
|
+
return own;
|
|
1315
803
|
}
|
|
1316
804
|
|
|
1317
|
-
// An rp MAY name the certificates it revoked in revCerts (RFC 4210 RevRepContent). This session
|
|
1318
|
-
// revokes exactly one -- its own -- so a conforming rp names exactly that one, and a response that
|
|
1319
|
-
// names another issuer/serial, or several, is not a verdict about this request. Left unbound, a
|
|
1320
|
-
// verified verdict would report an unrelated certificate as revoked. Compared to the session
|
|
1321
|
-
// certificate `own` the request was already held to, by serialNumber and the RFC 5280 sec. 7.1
|
|
1322
|
-
// canonical issuer rule. A CertId issuer is a GeneralName; a certificate's issuer is a directoryName,
|
|
1323
|
-
// so any other arm cannot name the revoked certificate.
|
|
1324
805
|
function _bindRevCerts(revCerts, own) {
|
|
1325
806
|
if (revCerts == null) return null;
|
|
1326
807
|
if (revCerts.length !== 1) {
|
|
@@ -1336,21 +817,12 @@ function session(opts) {
|
|
|
1336
817
|
return revCerts;
|
|
1337
818
|
}
|
|
1338
819
|
|
|
1339
|
-
|
|
1340
|
-
* Request revocation of this session's own certificate (RFC 9483 sec. 4.2).
|
|
1341
|
-
* `request` is `{ certificate }` or `{ certDetails: { issuer, serialNumber } }`, plus an optional
|
|
1342
|
-
* `reason` (a CRLReason name). Returns a terminal verdict: `revoked`, `rejected`, or `poll-timeout`.
|
|
1343
|
-
*/
|
|
820
|
+
|
|
1344
821
|
async function revoke(request) {
|
|
1345
822
|
_assertFresh("revocation");
|
|
1346
823
|
guard.identifier.assertPlainRecord(request, _err, "cmp/bad-input", "revoke(request), an object { certificate | certDetails, reason? },");
|
|
1347
824
|
guard.identifier.assertKnownKeys(request, KNOWN_REVOKE_KEYS, _err, "cmp/bad-input", "unknown revoke request field ");
|
|
1348
|
-
// A PBMAC1 session shares a secret with the CA; it holds no certificate, so it has nothing to sign
|
|
1349
|
-
// the request with and no way to demonstrate which certificate it is entitled to revoke.
|
|
1350
825
|
if (isMac) throw _err("cmp/bad-input", "a PBMAC1 session cannot revoke: the request must be signature-protected with the certificate being revoked (RFC 9483 sec. 4.2). Use a signature session ({ key, cert }).");
|
|
1351
|
-
// Each field is read ONCE, here, and every use below is of the value that was read. A caller
|
|
1352
|
-
// field can be an accessor, so a second read can answer with a different certificate or reason
|
|
1353
|
-
// from the one that was checked, and the message would carry what the checks never saw.
|
|
1354
826
|
var certificateArg = request.certificate, reasonArg = request.reason;
|
|
1355
827
|
var certDetails = request.certDetails;
|
|
1356
828
|
if ((certificateArg == null) === (certDetails == null)) {
|
|
@@ -1369,8 +841,6 @@ function session(opts) {
|
|
|
1369
841
|
}
|
|
1370
842
|
certDetails = { issuer: target.issuer.bytes, serialNumber: target.serialNumber };
|
|
1371
843
|
}
|
|
1372
|
-
// Encode the CertTemplate HERE rather than letting cmp.build do it, so the identity check below
|
|
1373
|
-
// reads the same bytes the message will carry instead of a second interpretation of the spec.
|
|
1374
844
|
var certTemplateDer;
|
|
1375
845
|
try {
|
|
1376
846
|
certTemplateDer = crmfSign.buildCertTemplate(certDetails);
|
|
@@ -1379,8 +849,6 @@ function session(opts) {
|
|
|
1379
849
|
throw _err("cmp/bad-rev-req", "revoke request.certDetails must be a CertTemplate naming issuer and serialNumber -- " + ((e && e.message) || e), e);
|
|
1380
850
|
}
|
|
1381
851
|
var own = _assertRevokingOwnCertificate(certTemplateDer);
|
|
1382
|
-
// sec. 4.2: exactly one RevDetails, and crlEntryDetails REQUIRED carrying one reasonCode. The
|
|
1383
|
-
// builder emits the reasonCode even at unspecified(0), which the same clause requires.
|
|
1384
852
|
var body = { rr: [{ certDetails: certTemplateDer, crlEntryDetails: { reason: reasonArg == null ? undefined : reasonArg } }] };
|
|
1385
853
|
inFlight = true;
|
|
1386
854
|
try {
|
|
@@ -1390,10 +858,7 @@ function session(opts) {
|
|
|
1390
858
|
if (t.state === "granted") {
|
|
1391
859
|
return _terminal("revoked", {
|
|
1392
860
|
status: t.status, polls: out.polls,
|
|
1393
|
-
revokedCerts: _bindRevCerts(t.revCerts, own),
|
|
1394
|
-
// An rp MAY deliver CRLs, and the schema reads those slots as raw SEQUENCEs for the same
|
|
1395
|
-
// reason it does in a genp. They are surfaced on a verdict an operator acts on, so they
|
|
1396
|
-
// are held to being CertificateLists here, exactly as the support-message values are.
|
|
861
|
+
revokedCerts: _bindRevCerts(t.revCerts, own),
|
|
1397
862
|
crls: (t.crls || []).map(_asCrl),
|
|
1398
863
|
});
|
|
1399
864
|
}
|
|
@@ -1405,22 +870,11 @@ function session(opts) {
|
|
|
1405
870
|
}
|
|
1406
871
|
}
|
|
1407
872
|
|
|
1408
|
-
|
|
1409
|
-
* Ask the PKI management entity for one of the RFC 9483 sec. 4.3 support values.
|
|
1410
|
-
* `request` names exactly one operation: `{ caCerts: true }`, `{ rootCaCert: <cert> }`,
|
|
1411
|
-
* `{ certReqTemplate: true }`, or `{ crlUpdate: { issuer, dpn?, thisUpdate? } }`.
|
|
1412
|
-
* Returns a terminal verdict: `answered` (with `value` and `present`), `rejected`, or `poll-timeout`.
|
|
1413
|
-
*/
|
|
873
|
+
|
|
1414
874
|
async function info(request) {
|
|
1415
875
|
_assertFresh("support message");
|
|
1416
876
|
guard.identifier.assertPlainRecord(request, _err, "cmp/bad-input", "info(request), an object naming one support operation (caCerts / rootCaCert / certReqTemplate / crlUpdate),");
|
|
1417
|
-
// The name door runs BEFORE the arm count, so a key that is not an operation is refused whatever
|
|
1418
|
-
// its value is. Counting non-null keys alone would let a misspelled one set to null pass as
|
|
1419
|
-
// absent, and the caller would watch a different operation go out than the one they wrote.
|
|
1420
877
|
guard.identifier.assertKnownKeys(request, INFO_OPS, _err, "cmp/bad-input", "unknown info request field ");
|
|
1421
|
-
// Each field is read ONCE, here, and everything below uses the value that was read. A caller
|
|
1422
|
-
// field can be an accessor, so a second read can answer with a different request from the one
|
|
1423
|
-
// the arm count was taken over, and what goes on the wire would not be what was checked.
|
|
1424
878
|
var keys = Object.keys(request), names = [], values = [];
|
|
1425
879
|
for (var ki = 0; ki < keys.length; ki++) {
|
|
1426
880
|
var kv = request[keys[ki]];
|
|
@@ -1433,36 +887,15 @@ function session(opts) {
|
|
|
1433
887
|
}
|
|
1434
888
|
var op = INFO_OPS[names[0]], asked = values[0];
|
|
1435
889
|
var itav = { infoType: op.requestOid };
|
|
1436
|
-
// sec. 4.3.1 and sec. 4.3.3 both make the request infoValue absent; the other two carry one.
|
|
1437
|
-
// For the two that carry none, the only accepted value is `true` -- a caller who supplies a
|
|
1438
|
-
// certificate or a template to one of them means something this operation cannot send, and
|
|
1439
|
-
// taking it silently would have the request go out without the constraint they wrote.
|
|
1440
890
|
if (op.value === false) {
|
|
1441
891
|
if (asked !== true) throw _err("cmp/bad-input", "info request." + op.name + " takes `true`: this operation's genm carries no infoValue (RFC 9483 sec. 4.3.1 and sec. 4.3.3)");
|
|
1442
892
|
} else if (op.value === "cert") {
|
|
1443
|
-
// sec. 4.3.2: the request SHOULD carry the root CA certificate the update is requested for,
|
|
1444
|
-
// and the field is a CMPCertificate. It goes through the SAME door revoke() gives a
|
|
1445
|
-
// certificate -- DER, PEM, or an already-parsed one -- because it is the same kind of value
|
|
1446
|
-
// and a caller should not have to remember which of the two verbs takes which form. Holding
|
|
1447
|
-
// it to being a certificate here keeps a mistake a local refusal; sending it unchecked would
|
|
1448
|
-
// put a malformed request on the wire and consume this one-shot transaction to learn what the
|
|
1449
|
-
// caller could have been told before it started.
|
|
1450
893
|
var rootCaCertDer = _certificateArgument(asked, "info request rootCaCert");
|
|
1451
|
-
// sec. 4.3.2: rootCaCert names the CURRENT root the update chains from, which is a CA. An
|
|
1452
|
-
// end-entity certificate here would let a cross-certificate signed by that non-CA key read as a
|
|
1453
|
-
// rollover (the response path verifies newWithOld / oldWithNew against this certificate's key), so
|
|
1454
|
-
// it is held to being a CA before the one-shot transaction engages rather than after.
|
|
1455
894
|
_assertCaCapable(x509.parse(rootCaCertDer), "info request rootCaCert", "cmp/bad-input");
|
|
1456
895
|
itav.infoValue = rootCaCertDer;
|
|
1457
896
|
} else if (op.value === "crlStatus") {
|
|
1458
897
|
var built = cmpBuild.buildCrlStatusList(asked);
|
|
1459
898
|
itav.infoValue = built.der;
|
|
1460
|
-
// What the request asked for, kept so the answer can be held to it. sec. 4.3.4 says the
|
|
1461
|
-
// responder returns the latest CRL FROM THE REFERENCED SOURCE, and only when it is more
|
|
1462
|
-
// recent than a supplied thisUpdate; in every other case the response value is absent.
|
|
1463
|
-
// The cutoff comes back FROM the builder, which read it once while encoding. Reading the
|
|
1464
|
-
// caller's field a second time here would let an accessor, or a Date moved during the
|
|
1465
|
-
// transport round-trip, judge the response against a query the request never carried.
|
|
1466
899
|
crlQuery = { issuerName: built.issuerName, thisUpdate: built.thisUpdate, dpn: built.dpn };
|
|
1467
900
|
}
|
|
1468
901
|
expectedInfoOp = op;
|
|
@@ -1472,9 +905,6 @@ function session(opts) {
|
|
|
1472
905
|
if (out.timeout) return _terminal("poll-timeout", { operation: op.name, status: out.status, polls: out.polls });
|
|
1473
906
|
var t = out.done;
|
|
1474
907
|
if (t.state === "granted") {
|
|
1475
|
-
// sec. 4.3: the infoValue is OPTIONAL in every one of the four responses, and its absence is
|
|
1476
|
-
// the answer "nothing is available" rather than a malformed message -- so `present` reports
|
|
1477
|
-
// which of the two a caller received, and never conflates them into an empty value.
|
|
1478
908
|
var raw = t.itav.value;
|
|
1479
909
|
var value = null;
|
|
1480
910
|
if (raw != null) {
|
|
@@ -1496,25 +926,13 @@ function session(opts) {
|
|
|
1496
926
|
}
|
|
1497
927
|
}
|
|
1498
928
|
|
|
1499
|
-
// The RFC 9483 profile rules that sit ON TOP of the RFC 9480 syntax the decoders enforce. Each is a
|
|
1500
|
-
// requirement the ASN.1 cannot express, so a structurally valid response can still be non-conforming.
|
|
1501
|
-
// `requestValue` is the infoValue this operation SENT, which the rootCaCert rules compare against:
|
|
1502
|
-
// the old root the caller named is what the update has to chain from. It is read here rather than
|
|
1503
|
-
// off `request`, so the comparison uses the bytes that went on the wire.
|
|
1504
929
|
function _checkInfoValue(op, value, requestValue) {
|
|
1505
930
|
if (op.name === "rootCaCert") {
|
|
1506
|
-
// sec. 4.3.2 makes newWithOld REQUIRED of a response ("it is needed for the receiving entity
|
|
1507
|
-
// trusting the old root CA certificate to gain trust in the new one"), where RFC 9480's ASN.1
|
|
1508
|
-
// marks it OPTIONAL. Without it the update cannot be trusted by the entity that asked for it.
|
|
1509
931
|
if (value.newWithOld == null) throw _err("cmp/bad-info-value", "a rootCaKeyUpdate response must carry newWithOld -- the certificate that lets an entity trusting the OLD root gain trust in the new one (RFC 9483 sec. 4.3.2)");
|
|
1510
932
|
return _checkRootCaKeyUpdate(value, requestValue);
|
|
1511
933
|
}
|
|
1512
934
|
if (op.name === "certReqTemplate") {
|
|
1513
935
|
var t = value.certTemplate;
|
|
1514
|
-
// sec. 4.3.3: "The publicKey field of type SubjectPublicKeyInfo in the CertTemplate of the
|
|
1515
|
-
// CertReqTemplateValue MUST be omitted", and "the serialNumber, signingAlg, issuerUID, and
|
|
1516
|
-
// subjectUID fields MUST be omitted". A template stating a key would tell an entity to request
|
|
1517
|
-
// a certificate over a key the responder chose, which is what the keySpec field exists to avoid.
|
|
1518
936
|
["publicKey", "serialNumber", "signingAlg", "issuerUID", "subjectUID"].forEach(function (f) {
|
|
1519
937
|
if (t[f] != null) throw _err("cmp/bad-info-value", "a certReqTemplate certTemplate must omit " + f + " (RFC 9483 sec. 4.3.3)");
|
|
1520
938
|
});
|
|
@@ -1522,44 +940,17 @@ function session(opts) {
|
|
|
1522
940
|
return value;
|
|
1523
941
|
}
|
|
1524
942
|
if (op.name === "caCerts") return value.map(function (d) {
|
|
1525
|
-
// sec. 4.3.1 returns CA certificates for chain construction, so each is held to being one: an
|
|
1526
|
-
// authenticated responder placing an end-entity certificate here would hand back a list nothing
|
|
1527
|
-
// can chain through. The same CA-capability rule the rootCaKeyUpdate rollover certificates get.
|
|
1528
943
|
var der = _asCertificate(d, "a caCerts entry");
|
|
1529
944
|
_assertCaCapable(x509.parse(der), "caCerts entry");
|
|
1530
945
|
return der;
|
|
1531
946
|
});
|
|
1532
947
|
if (op.name === "crlUpdate") {
|
|
1533
|
-
// sec. 4.3.4: the request carries exactly one CRLStatus (one source), and the response "represents
|
|
1534
|
-
// the latest CRL available from the referenced CRL source" -- one CRL. The ASN.1 list is SIZE
|
|
1535
|
-
// (1..MAX), so the responder could return several; accepting them all would surface an older CRL
|
|
1536
|
-
// beside the latest and let a caller act on a stale value[0]. Exactly one is required, and it is
|
|
1537
|
-
// then bound to the query that asked for it.
|
|
1538
948
|
if (value.length !== 1) throw _err("cmp/bad-info-value", "a crlUpdate response answers a single-source query with exactly one CRL -- the latest from the named source (RFC 9483 sec. 4.3.4); got " + value.length);
|
|
1539
949
|
return value.map(_bindCrlToQuery);
|
|
1540
950
|
}
|
|
1541
|
-
// Unreachable while INFO_OPS holds exactly the four operations above, each with a branch here.
|
|
1542
|
-
// It refuses rather than passing the value through, so adding a fifth operation without its
|
|
1543
|
-
// profile rules fails at once instead of surfacing a value nothing checked.
|
|
1544
951
|
throw _err("cmp/bad-info-value", "no response rules are defined for the " + op.name + " support message");
|
|
1545
952
|
}
|
|
1546
953
|
|
|
1547
|
-
// A support-message response is surfaced as an ANSWER an operator acts on, so what it carries is
|
|
1548
|
-
// held to being the structure it claims. The CMP schema reads these slots as raw SEQUENCEs on
|
|
1549
|
-
// purpose (the parser confers no trust and a CMPCertificate slot is opaque to it), which leaves
|
|
1550
|
-
// "is this actually a certificate" to the verb that hands the bytes back -- the same division
|
|
1551
|
-
// this module already applies to an authenticated caPubs entry. Without it a responder could
|
|
1552
|
-
// answer a chain-construction request with any well-formed SEQUENCE and have it read as a
|
|
1553
|
-
// certificate, and a CRL request with a certificate. Returns a COPY, since the decoded value is
|
|
1554
|
-
// a slice of the response buffer.
|
|
1555
|
-
// A root CA key update is three certificates in NAMED relationships, and the relationships are the
|
|
1556
|
-
// whole mechanism (RFC 9483 sec. 4.3.2): newWithOld carries "the new public root CA key signed
|
|
1557
|
-
// with the old private root CA key", which is what lets an entity that trusts the old root reach
|
|
1558
|
-
// the new one, and oldWithNew carries "the old public root CA key signed with the new private root
|
|
1559
|
-
// CA key" for the reverse. Three certificates that merely parse establish neither, so a responder
|
|
1560
|
-
// could answer with any three it holds and the update would be reported as usable. Each signature
|
|
1561
|
-
// is checked by the path engine, so the EdDSA low-order-point and algorithm-confusion gates apply
|
|
1562
|
-
// here as they do to any other untrusted signature.
|
|
1563
954
|
async function _checkRootCaKeyUpdate(value, oldRootDer) {
|
|
1564
955
|
var newWithNew = _asCertificate(value.newWithNew, "the rootCaKeyUpdate newWithNew");
|
|
1565
956
|
var newWithOld = _asCertificate(value.newWithOld, "the rootCaKeyUpdate newWithOld");
|
|
@@ -1570,22 +961,8 @@ function session(opts) {
|
|
|
1570
961
|
if (!cmpBuild.samePublicKey(pNewOld.subjectPublicKeyInfo.bytes, pNewNew.subjectPublicKeyInfo.bytes)) {
|
|
1571
962
|
throw _err("cmp/bad-info-value", "the rootCaKeyUpdate newWithOld must certify the NEW root key, the one newWithNew carries (RFC 9483 sec. 4.3.2)");
|
|
1572
963
|
}
|
|
1573
|
-
// A key alone does not make a trust transition. If the old CA has ever signed ANY certificate
|
|
1574
|
-
// for the new key -- an ordinary end-entity certificate its holder legitimately obtained --
|
|
1575
|
-
// then key equality plus a valid signature is satisfied by that certificate, and its holder
|
|
1576
|
-
// could pair it with a self-signed newWithNew of their own choosing and have the result read as
|
|
1577
|
-
// the authority's own rollover. What sec. 4.3.2 describes is a certificate FOR the new root
|
|
1578
|
-
// issued BY the old one, so both names are bound too, under the RFC 5280 sec. 7.1 comparison.
|
|
1579
964
|
_assertSameName(pNewOld.issuer, pOldRoot.subject, "newWithOld", "be issued by the OLD root named in the request");
|
|
1580
965
|
_assertSameName(pNewOld.subject, pNewNew.subject, "newWithOld", "name the same subject as newWithNew, the certificate it vouches for");
|
|
1581
|
-
// newWithNew is the replacement certificate, and for a root it is self-issued, so its own key is
|
|
1582
|
-
// the only thing that can vouch for the name, validity and extensions the caller would install.
|
|
1583
|
-
// sec. 4.3.2's note extends this operation to "trusted non-root certificates, e.g., directly
|
|
1584
|
-
// trusted intermediate or issuing CA certificates", and such a certificate is signed by an
|
|
1585
|
-
// issuer this message does not carry -- there is nothing here to check it against, and claiming
|
|
1586
|
-
// otherwise would refuse a use the profile allows. So the signature is verified exactly when it
|
|
1587
|
-
// is verifiable: when the certificate is self-issued. Either way the new KEY is authenticated by
|
|
1588
|
-
// newWithOld below, which the old root signed and which must carry this same key.
|
|
1589
966
|
if (guard.name.dnEqual(pNewNew.issuer.rdns, pNewNew.subject.rdns, _err, "cmp/bad-info-value", "the rootCaKeyUpdate newWithNew name")) {
|
|
1590
967
|
await _assertSignedBy(pNewNew, pNewNew, "newWithNew", "its own key, which a self-issued root certificate must be");
|
|
1591
968
|
}
|
|
@@ -1602,18 +979,8 @@ function session(opts) {
|
|
|
1602
979
|
}
|
|
1603
980
|
return { newWithNew: newWithNew, newWithOld: newWithOld, oldWithNew: oldWithNew };
|
|
1604
981
|
}
|
|
1605
|
-
// A certificate a caller will build a chain from -- a rootCaKeyUpdate rollover certificate or a
|
|
1606
|
-
// caCerts entry -- has to actually be a CA. Names, a key and a signature are satisfied by an
|
|
1607
|
-
// ordinary end-entity certificate that certifies nothing, so surfacing one as an issuer reports the
|
|
1608
|
-
// answer as usable while handing back a list nothing can chain through. The rule is RFC 5280 sec.
|
|
1609
|
-
// 6.1.4's, the one pki.path.validate applies to any intermediate: basicConstraints cA TRUE, marked
|
|
1610
|
-
// critical (sec. 4.2.1.9), and keyUsage, where present, allowing keyCertSign. Criticality matters
|
|
1611
|
-
// because a relying party that skips extensions it does not recognize would not see the cA bit at
|
|
1612
|
-
// all, which is why the path validator refuses a non-critical cA:TRUE; this check holds to the same
|
|
1613
|
-
// line so the two never disagree on what a CA certificate is. `which` names the surface for the
|
|
1614
|
-
// error. Extensions are read through the shared certificate-extension decoders rather than walked here.
|
|
1615
982
|
function _assertCaCapable(cert, which, code) {
|
|
1616
|
-
code = code || "cmp/bad-info-value";
|
|
983
|
+
code = code || "cmp/bad-info-value";
|
|
1617
984
|
var bc = null, ku = null, bcCritical = false;
|
|
1618
985
|
cert.extensions.forEach(function (e) {
|
|
1619
986
|
if (e.oid === OID_BASIC_CONSTRAINTS) { bc = cmpBuild.decodeCertExtension(e.oid, e.value); bcCritical = e.critical === true; }
|
|
@@ -1630,9 +997,6 @@ function session(opts) {
|
|
|
1630
997
|
}
|
|
1631
998
|
}
|
|
1632
999
|
|
|
1633
|
-
// Names are compared through guard-name's RFC 5280 sec. 7.1 canonical rule, the one place a
|
|
1634
|
-
// distinguished-name identity is decided here: two spellings of the same name are the same
|
|
1635
|
-
// authority, and a byte compare would refuse a re-encoded but equal one.
|
|
1636
1000
|
function _assertSameName(a, b, which, must) {
|
|
1637
1001
|
if (!guard.name.dnEqual(a.rdns, b.rdns, _err, "cmp/bad-info-value", "a rootCaKeyUpdate name")) {
|
|
1638
1002
|
throw _err("cmp/bad-info-value", "the rootCaKeyUpdate " + which + " must " + must + " (RFC 9483 sec. 4.3.2); got " + JSON.stringify(a.dn));
|
|
@@ -1642,10 +1006,6 @@ function session(opts) {
|
|
|
1642
1006
|
if (!_engine || !_engine.verifyWithSpki) {
|
|
1643
1007
|
throw _err("cmp/bad-info-value", "the signature engine is unavailable, so a rootCaKeyUpdate cannot be checked; require pki.path to install it");
|
|
1644
1008
|
}
|
|
1645
|
-
// The signature is a BIT STRING and a valid one is octet-aligned (no unused bits). Passing only its
|
|
1646
|
-
// octets to the engine would drop the unusedBits metadata, so a signature declaring unused bits would
|
|
1647
|
-
// verify on its octets while being a malformed shape the path verifier refuses (path-validate) before
|
|
1648
|
-
// the engine. Hold a rollover signature to the same rule.
|
|
1649
1009
|
if (!guard.crypto.isOctetAligned(cert.signatureValue)) {
|
|
1650
1010
|
throw _err("cmp/bad-info-value", "the rootCaKeyUpdate " + which + " signature is not octet-aligned (a BIT STRING with unused bits), which no valid signature is (RFC 9483 sec. 4.3.2)");
|
|
1651
1011
|
}
|
|
@@ -1655,11 +1015,6 @@ function session(opts) {
|
|
|
1655
1015
|
}
|
|
1656
1016
|
}
|
|
1657
1017
|
|
|
1658
|
-
// A certificate this verb has to put BACK on the wire, so it takes the two forms that carry their
|
|
1659
|
-
// own bytes: DER, and PEM which decodes to them. An already-parsed certificate is refused rather
|
|
1660
|
-
// than re-encoded -- the parsed form retains no source DER, and rebuilding one would emit bytes
|
|
1661
|
-
// that are not the ones the issuer signed. revoke() takes the parsed form because it reads an
|
|
1662
|
-
// issuer and a serial out of it and never re-emits the certificate itself.
|
|
1663
1018
|
function _certificateArgument(value, what) {
|
|
1664
1019
|
var der;
|
|
1665
1020
|
if (typeof value === "string") {
|
|
@@ -1673,9 +1028,6 @@ function session(opts) {
|
|
|
1673
1028
|
return _asCertificate(der, what, "cmp/bad-input");
|
|
1674
1029
|
}
|
|
1675
1030
|
|
|
1676
|
-
// `code` names the boundary: a response value is malformed protocol (cmp/bad-info-value), while a
|
|
1677
|
-
// value the CALLER supplied for a request is an authoring error (cmp/bad-input), and the two must
|
|
1678
|
-
// not read alike to whoever catches them.
|
|
1679
1031
|
function _asCertificate(der, what, code) {
|
|
1680
1032
|
try { x509.parse(der); }
|
|
1681
1033
|
catch (e) { throw _err(code || "cmp/bad-info-value", what + " is not a valid X.509 certificate (RFC 9483 sec. 4.3)", e); }
|
|
@@ -1686,17 +1038,6 @@ function session(opts) {
|
|
|
1686
1038
|
catch (e) { throw _err("cmp/bad-info-value", "a crlUpdate entry is not a valid CertificateList (RFC 9483 sec. 4.3.4)", e); }
|
|
1687
1039
|
return Buffer.from(der);
|
|
1688
1040
|
}
|
|
1689
|
-
// sec. 4.3.4 does not say "a CRL", it says the latest one FROM THE REFERENCED SOURCE, and only
|
|
1690
|
-
// when it is more recent than a supplied thisUpdate; in every other case the value is absent.
|
|
1691
|
-
// A CRL that answers some other query is not the answer to this one, and reporting it as one
|
|
1692
|
-
// hands the caller a list whose scope they did not ask about.
|
|
1693
|
-
//
|
|
1694
|
-
// The issuer arm is bound by name. The dpn arm is bound where a CRL says which point it speaks
|
|
1695
|
-
// for: RFC 5280 sec. 5.2.5 puts that in the issuingDistributionPoint extension, and sec. 4.3.4's
|
|
1696
|
-
// own note points there for where a distribution point name comes from. A CRL carrying no such
|
|
1697
|
-
// scope is claiming to be its issuer's complete list, which sec. 5.2.5 permits and which answers
|
|
1698
|
-
// a request for any point of that issuer, so there is nothing to refuse -- and nothing to bind,
|
|
1699
|
-
// since the request named no issuer. The freshness rule applies to both arms.
|
|
1700
1041
|
function _bindCrlToQuery(der) {
|
|
1701
1042
|
var parsed;
|
|
1702
1043
|
try { parsed = schemaCrl.parse(der); }
|
|
@@ -1715,21 +1056,12 @@ function session(opts) {
|
|
|
1715
1056
|
idpCritical = crlExts[xi].critical === true;
|
|
1716
1057
|
break;
|
|
1717
1058
|
}
|
|
1718
|
-
// The comparison is the one the path validator applies to a shard CRL, so a CRL either speaks
|
|
1719
|
-
// for the point this request named or it does not, and the answer does not depend on which verb
|
|
1720
|
-
// is asking. Only a CRITICAL issuingDistributionPoint establishes that scope (sec. 5.2.5): a
|
|
1721
|
-
// relying party may ignore a non-critical one, so a scope it could ignore cannot bind the answer
|
|
1722
|
-
// to the named point -- the path validator makes the same fail-closed decision for a shard CRL.
|
|
1723
|
-
// A CRL naming no point is a complete list and is left to the freshness rule.
|
|
1724
1059
|
if (idpDpn !== null && (!idpCritical || !guard.name.dpnCorresponds(crlQuery.dpn, idpDpn, _err, "cmp/bad-info-value", "the crlUpdate distribution point"))) {
|
|
1725
1060
|
throw _err("cmp/bad-info-value", "the returned CRL is scoped to a distribution point other than the one this request named, or marks that scope non-critical where a relying party may ignore it (RFC 9483 sec. 4.3.4, RFC 5280 sec. 5.2.5)");
|
|
1726
1061
|
}
|
|
1727
1062
|
}
|
|
1728
1063
|
if (crlQuery && crlQuery.thisUpdate != null) {
|
|
1729
|
-
var asked = crlQuery.thisUpdate;
|
|
1730
|
-
// The CRL's own thisUpdate goes through the same door: it arrived from a responder, so it is
|
|
1731
|
-
// the side more likely to be missing or unusable, and reading its epoch value directly would
|
|
1732
|
-
// compare NaN and quietly answer false.
|
|
1064
|
+
var asked = crlQuery.thisUpdate;
|
|
1733
1065
|
var got = guard.time.instantOf(parsed.thisUpdate, _err, "cmp/bad-info-value", "the returned CRL's thisUpdate");
|
|
1734
1066
|
if (got <= asked) {
|
|
1735
1067
|
throw _err("cmp/bad-info-value", "the returned CRL is no more recent than the thisUpdate this request supplied, so sec. 4.3.4 requires the response to carry no value at all");
|
|
@@ -1740,28 +1072,19 @@ function session(opts) {
|
|
|
1740
1072
|
|
|
1741
1073
|
var OID_REG_CTRL_ALG_ID = oid.byName("algId");
|
|
1742
1074
|
var OID_REG_CTRL_RSA_KEY_LEN = oid.byName("rsaKeyLen");
|
|
1743
|
-
// True for an RSA algorithm, which sec. 4.3.3 excludes from a keySpec algId (RSA uses rsaKeyLen).
|
|
1744
|
-
// Membership is decided by OID family: any OID under the PKCS#1 arc, plus the two RSA identifiers
|
|
1745
|
-
// that sit off it. A standardized RSA OID the registry has not named (sha1WithRSAEncryption, the
|
|
1746
|
-
// md*/sha224 variants) is still under the arc, so it is caught without being listed. An OID that is
|
|
1747
|
-
// not an encodable dotted string, or one outside every RSA arc, is NOT RSA here -- it is surfaced.
|
|
1748
1075
|
function _isRsaAlgorithm(dotted) {
|
|
1749
|
-
if (RSA_ARC_EXCLUDE[dotted] === 1) return false;
|
|
1750
|
-
var arcs = oid.toArcs(dotted);
|
|
1076
|
+
if (RSA_ARC_EXCLUDE[dotted] === 1) return false;
|
|
1077
|
+
var arcs = oid.toArcs(dotted);
|
|
1751
1078
|
var underAny = false;
|
|
1752
1079
|
for (var a = 0; !underAny && a < RSA_ARCS.length; a++) {
|
|
1753
1080
|
var arc = RSA_ARCS[a];
|
|
1754
|
-
if (arcs.length > arc.length) {
|
|
1081
|
+
if (arcs.length > arc.length) {
|
|
1755
1082
|
underAny = true;
|
|
1756
1083
|
for (var i = 0; underAny && i < arc.length; i++) { underAny = arcs[i] === arc[i]; }
|
|
1757
1084
|
}
|
|
1758
1085
|
}
|
|
1759
1086
|
return underAny || RSA_OFF_ARC[dotted] === 1;
|
|
1760
1087
|
}
|
|
1761
|
-
// sec. 4.3.3: a keySpec element carries "attribute id-regCtrl-algId or id-regCtrl-rsaKeyLen", the
|
|
1762
|
-
// first "MUST be of type AlgorithmIdentifier and give an algorithm other than RSA", the second
|
|
1763
|
-
// "MUST be a positive integer value". A control outside the pair states a requirement this
|
|
1764
|
-
// operation has no meaning for, so it is refused rather than surfaced as an understood one.
|
|
1765
1088
|
function _checkKeySpec(ctrl) {
|
|
1766
1089
|
if (ctrl.type === OID_REG_CTRL_RSA_KEY_LEN) {
|
|
1767
1090
|
var len;
|
|
@@ -1772,27 +1095,17 @@ function session(opts) {
|
|
|
1772
1095
|
return;
|
|
1773
1096
|
}
|
|
1774
1097
|
if (ctrl.type === OID_REG_CTRL_ALG_ID) {
|
|
1775
|
-
// Read through the shared AlgorithmIdentifier sub-schema rather than probing the node, so the
|
|
1776
|
-
// structure is held to its whole definition: a SEQUENCE of an OID and at most one parameters
|
|
1777
|
-
// field. A hand-rolled tag test says only that the first child is an OID, which a SEQUENCE of
|
|
1778
|
-
// three fields also satisfies -- and that value would then reach the caller as an algorithm
|
|
1779
|
-
// requirement no conforming responder can have meant.
|
|
1780
1098
|
var alg;
|
|
1781
1099
|
try { alg = schemaCmp.readAlgorithmIdentifier(ctrl.value); }
|
|
1782
1100
|
catch (e) {
|
|
1783
1101
|
if (e && e.isCmpError) throw e;
|
|
1784
1102
|
throw _err("cmp/bad-info-value", "a keySpec algId must be an AlgorithmIdentifier (RFC 9483 sec. 4.3.3)", e);
|
|
1785
1103
|
}
|
|
1786
|
-
// sec. 4.3.3 / RFC 9480 sec. 2.16 hold the algId to ONE rule: not RSA. A known RSA algorithm is
|
|
1787
|
-
// refused (its requirement is stated with rsaKeyLen); every other value -- a known non-RSA
|
|
1788
|
-
// algorithm OR an OID the registry does not name -- is surfaced. The responder offers one control
|
|
1789
|
-
// per algorithm it supports and the entity picks one, so an unrecognized offer must not fail the
|
|
1790
|
-
// whole exchange.
|
|
1791
1104
|
if (_isRsaAlgorithm(alg.oid)) {
|
|
1792
1105
|
throw _err("cmp/bad-info-value", "a keySpec algId must give an algorithm other than RSA, whose requirement is stated with rsaKeyLen instead (RFC 9483 sec. 4.3.3); got " + (alg.name || alg.oid));
|
|
1793
1106
|
}
|
|
1794
1107
|
ctrl.algorithm = alg.oid;
|
|
1795
|
-
ctrl.algorithmName = alg.name;
|
|
1108
|
+
ctrl.algorithmName = alg.name;
|
|
1796
1109
|
ctrl.algorithmParameters = alg.parameters;
|
|
1797
1110
|
return;
|
|
1798
1111
|
}
|
|
@@ -1803,8 +1116,8 @@ function session(opts) {
|
|
|
1803
1116
|
enroll: enroll,
|
|
1804
1117
|
revoke: revoke,
|
|
1805
1118
|
info: info,
|
|
1806
|
-
get transactionID() { return Buffer.from(transactionID); },
|
|
1807
|
-
get transcript() { return _transcriptSnapshot(); },
|
|
1119
|
+
get transactionID() { return Buffer.from(transactionID); },
|
|
1120
|
+
get transcript() { return _transcriptSnapshot(); },
|
|
1808
1121
|
};
|
|
1809
1122
|
}
|
|
1810
1123
|
|
|
@@ -1814,5 +1127,5 @@ module.exports = {
|
|
|
1814
1127
|
wellKnownUrl: cmp.wellKnownUrl,
|
|
1815
1128
|
verify: cmp.verify,
|
|
1816
1129
|
session: session,
|
|
1817
|
-
setEngine: setEngine, // @internal
|
|
1130
|
+
setEngine: setEngine, // @internal
|
|
1818
1131
|
};
|