@blamejs/pki 0.5.25 → 0.5.26
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 +30 -0
- package/README.md +1 -1
- package/lib/cmp-build.js +185 -5
- package/lib/cmp-session.js +753 -11
- package/lib/guard-identifier.js +36 -0
- package/lib/guard-name.js +90 -1
- package/lib/oid.js +55 -2
- package/lib/path-validate.js +15 -27
- package/lib/schema-cmp.js +100 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/cmp-session.js
CHANGED
|
@@ -21,6 +21,10 @@ var asn1 = require("./asn1-der");
|
|
|
21
21
|
var oid = require("./oid");
|
|
22
22
|
var x509 = require("./schema-x509");
|
|
23
23
|
var schemaCmp = require("./schema-cmp");
|
|
24
|
+
var schemaCrmf = require("./schema-crmf");
|
|
25
|
+
var schemaCrl = require("./schema-crl");
|
|
26
|
+
var crmfSign = require("./crmf-sign");
|
|
27
|
+
var cmpBuild = require("./cmp-build");
|
|
24
28
|
var csr = require("./schema-csr");
|
|
25
29
|
var guard = require("./guard-all");
|
|
26
30
|
var compositeSig = require("./composite-sig");
|
|
@@ -58,6 +62,15 @@ var SIG_OID_TO_HASH = {};
|
|
|
58
62
|
});
|
|
59
63
|
var OID_RSASSA_PSS = oid.byName("rsassaPss");
|
|
60
64
|
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
|
+
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
|
+
var OID_BASIC_CONSTRAINTS = oid.byName("basicConstraints");
|
|
73
|
+
var OID_KEY_USAGE = oid.byName("keyUsage");
|
|
61
74
|
// Message-digest OID -> the WebCrypto digest name, for the RSASSA-PSS hashAlgorithm parameter, dispatched by
|
|
62
75
|
// the immutable OID (never oid.name, which pki.oid.register can rename). Resolved to a dotted-OID map at load.
|
|
63
76
|
var HASH_OID_TO_DIGEST = {};
|
|
@@ -84,7 +97,84 @@ var P10CR_CERT_REQ_ID = -1; // a PKCS#10 request has no CRMF id; a conforming
|
|
|
84
97
|
var ENROLL_ARMS = { ir: 1, cr: 1, kur: 1, p10cr: 1 }; // the initial request arms enroll() accepts
|
|
85
98
|
// The response body arm each enrollment request is answered by (RFC 9810 sec. 5.3.2/5.3.4): ir->ip, cr/p10cr
|
|
86
99
|
// ->cp, kur->kup. A response on a different cert-response arm than the request is misrouted, never confirmed.
|
|
87
|
-
var RESPONSE_ARM = { ir: "ip", cr: "cp", kur: "kup", p10cr: "cp" };
|
|
100
|
+
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
|
+
var WHOLE_MESSAGE_CERT_REQ_ID = -1;
|
|
108
|
+
|
|
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
|
+
var INFO_OPS = Object.assign(Object.create(null), {
|
|
117
|
+
caCerts: { name: "caCerts", requestOid: "caCerts", responseOid: "caCerts", value: false, read: "readCaCerts" },
|
|
118
|
+
rootCaCert: { name: "rootCaCert", requestOid: "rootCaCert", responseOid: "rootCaKeyUpdate", value: "cert", read: "readRootCaKeyUpdate" },
|
|
119
|
+
certReqTemplate: { name: "certReqTemplate", requestOid: "certReqTemplate", responseOid: "certReqTemplate", value: false, read: "readCertReqTemplate" },
|
|
120
|
+
crlUpdate: { name: "crlUpdate", requestOid: "crlStatusList", responseOid: "crls", value: "crlStatus", read: "readCrls" },
|
|
121
|
+
});
|
|
122
|
+
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), and the BSI TR-03110 id-TA-RSA arc (EAC Terminal
|
|
139
|
+
// Authentication, RSA v1.5/PSS over SHA-1/256/512). Each prefix is the arc a registered member sits
|
|
140
|
+
// directly under, read from the registry, so no dotted-decimal literal appears here and pki.oid.register
|
|
141
|
+
// cannot move it. The PKCS#1 arc also carries two auxiliary identifiers that are NOT RSA algorithms --
|
|
142
|
+
// mgf1 and pSpecified, the mask-generation and OAEP-label parameters -- so those are excluded from the
|
|
143
|
+
// match by name (RSA_ARC_EXCLUDE) and surfaced. RSA also sits off those arcs on ones it SHARES with non-RSA
|
|
144
|
+
// algorithms, where no prefix can isolate it, so those are listed by name: id-rsa-kem (S/MIME) and
|
|
145
|
+
// id-kem-rsa (ISO 18033-2); the RSASSA-PKCS1-v1_5-with-SHA-3 set on the NIST signature arc, shared with
|
|
146
|
+
// ML-DSA and SLH-DSA; RSASSA-PSS-with-SHAKE (RFC 8692) on the PKIX arc; every RSA identifier the OIW SIG
|
|
147
|
+
// assigned on the OIW Secsig arc (the md4/md5-WithRSA, rsaSignature, mdc2/sha-WithRSA, rsaKeyTransport,
|
|
148
|
+
// and md2/md5/sha1-WithRSASignature identifiers), which is shared with DES, DSA, DH, and the hashes so no
|
|
149
|
+
// prefix isolates it; and the X.500 directory RSA OIDs on the 2.5.8 arc -- rsa (2.5.8.1.1), distinct from
|
|
150
|
+
// PKCS#1 rsaEncryption, and sqMod-nWithRSA / mdc2WithRSA on the signatureAlgorithm sub-arc; and the
|
|
151
|
+
// Chinese GM/T sm3WithRSAEncryption (1.2.156.10197.1.504), an RSA-over-SM3 signature on the SM arc it
|
|
152
|
+
// shares with SM2 and SM3. A composite signature naming RSA as one half is a composite key, not an RSA
|
|
153
|
+
// one, and lives on the PKIX arc, so it is outside all of these.
|
|
154
|
+
var RSA_ARCS = ["rsaEncryption", "rsaSignatureWithripemd160", "id-TA-RSA-v1-5-SHA-256"].map(function (n) {
|
|
155
|
+
return oid.toArcs(oid.byName(n)).slice(0, -1); // the RSA-dedicated arc a registered member sits directly under
|
|
156
|
+
});
|
|
157
|
+
var RSA_OFF_ARC = Object.create(null);
|
|
158
|
+
["id-rsa-kem", "id-kem-rsa",
|
|
159
|
+
"id-rsassa-pkcs1-v1_5-with-sha3-224", "id-rsassa-pkcs1-v1_5-with-sha3-256",
|
|
160
|
+
"id-rsassa-pkcs1-v1_5-with-sha3-384", "id-rsassa-pkcs1-v1_5-with-sha3-512",
|
|
161
|
+
"id-RSASSA-PSS-SHAKE128", "id-RSASSA-PSS-SHAKE256",
|
|
162
|
+
"md4WithRSA", "md5WithRSA", "md4WithRSAEncryption", "rsaSignature", "mdc2WithRSASignature",
|
|
163
|
+
"shaWithRSAEncryption", "rsaKeyTransport",
|
|
164
|
+
"md2WithRSASignature", "md5WithRSASignature", "sha1WithRSASignature",
|
|
165
|
+
"rsa", // the X.509 directory rsa public-key OID (2.5.8.1.1), distinct from PKCS#1 rsaEncryption
|
|
166
|
+
"sqMod-nWithRSA", "mdc2WithRSA", // the X.500 directory signatureAlgorithm RSA OIDs (2.5.8.3.1, 2.5.8.3.100)
|
|
167
|
+
"sm3WithRSAEncryption", // the Chinese GM/T RSA-with-SM3 signature (1.2.156.10197.1.504)
|
|
168
|
+
].forEach(function (n) { var d = oid.byName(n); if (d) RSA_OFF_ARC[d] = 1; });
|
|
169
|
+
// The PKCS#1 arc carries two identifiers that are NOT RSA algorithms: mgf1 (the mask-generation function
|
|
170
|
+
// RSA-PSS and RSA-OAEP parametrize) and pSpecified (the OAEP label source). The prefix match would sweep
|
|
171
|
+
// them in, but a keySpec algId naming one states no RSA key requirement -- the entity does not generate an
|
|
172
|
+
// "mgf1 key" -- so they are surfaced, not refused, and are excluded from the arc match by name.
|
|
173
|
+
var RSA_ARC_EXCLUDE = Object.create(null);
|
|
174
|
+
["mgf1", "pSpecified"].forEach(function (n) { var d = oid.byName(n); if (d) RSA_ARC_EXCLUDE[d] = 1; });
|
|
175
|
+
|
|
176
|
+
var CRL_REASON_NAMES = Object.create(null);
|
|
177
|
+
Object.keys(constants.NAMES.CRL_REASON).forEach(function (v) { CRL_REASON_NAMES[constants.NAMES.CRL_REASON[v]] = 1; });
|
|
88
178
|
|
|
89
179
|
// PKIStatus codes (RFC 9810 sec. 5.2.3): 0 accepted, 1 grantedWithMods, 2 rejection, 3 waiting.
|
|
90
180
|
function _isGranted(code) { return code === 0 || code === 1; }
|
|
@@ -311,6 +401,7 @@ function _certConfHash(certDer) {
|
|
|
311
401
|
* (PBMAC1). A crypto-valid response is not enough: the signer must chain to a supplied trust anchor
|
|
312
402
|
* (signature) or the shared secret must match (MAC); a valid-but-untrusted response is a hard stop, so the
|
|
313
403
|
* signature flavor REQUIRES `opts.trustAnchors` at construction. Returns a session with `enroll(request)`,
|
|
404
|
+
* `revoke(request)`, `info(request)`,
|
|
314
405
|
* and read-only `transactionID` + `transcript` (each leg's request/response bytes, retained up to a
|
|
315
406
|
* transaction-wide cap; a later leg beyond the cap keeps its metadata + `byteLength` but drops the payload as
|
|
316
407
|
* `bytes: null, truncated: true`, so a padded-response flood across polls cannot exhaust memory). The granted certificate must be valid X.509, carry the key
|
|
@@ -326,6 +417,33 @@ function _certConfHash(certDer) {
|
|
|
326
417
|
* issued leaf plus any authenticated `caPubs` the CA delivered (chain material, never trust anchors); a
|
|
327
418
|
* server-generated (central key generation) private key is out of scope and the grant is refused.
|
|
328
419
|
*
|
|
420
|
+
* `revoke(request)` drives the RFC 9483 sec. 4.2 revocation exchange (`rr` -> `rp`) through the same shell.
|
|
421
|
+
* The request names the certificate, either as `{ certificate }` or as `{ certDetails: { issuer,
|
|
422
|
+
* serialNumber } }`, plus an optional `reason` (a CRLReason name). A session revokes ITS OWN certificate:
|
|
423
|
+
* the signature over the request is the proof of authorization to revoke, so the named certificate must be
|
|
424
|
+
* `opts.cert`, and a PBMAC1 session, holding no certificate, is refused. The `crlEntryDetails` reasonCode
|
|
425
|
+
* is always emitted, at `unspecified(0)` when no reason is given, which sec. 4.2 requires and is the
|
|
426
|
+
* opposite of the RFC 5280 sec. 5.3.1 rule for a CRL entry. Terminal `outcome`: `revoked` (the response's
|
|
427
|
+
* single accepted status), `rejected`, or `poll-timeout`.
|
|
428
|
+
*
|
|
429
|
+
* `info(request)` drives the sec. 4.3 support messages (`genm` -> `genp`), one operation per call:
|
|
430
|
+
* `{ caCerts: true }` for the CA certificates available for chain construction; `{ rootCaCert: <cert> }`
|
|
431
|
+
* for a root CA key update, whose response carries a DIFFERENT infoType and must include the `newWithOld`
|
|
432
|
+
* certificate an entity trusting the old root needs; `{ certReqTemplate: true }` for the certificate-request
|
|
433
|
+
* requirements, whose template must omit `publicKey`, `serialNumber`, `signingAlg`, `issuerUID` and
|
|
434
|
+
* `subjectUID`, and whose `keySpec` states key requirements as `algId` (a non-RSA AlgorithmIdentifier) or
|
|
435
|
+
* `rsaKeyLen` (a positive integer); and `{ crlUpdate: { issuer, dpn?, thisUpdate? } }` for a CRL from a
|
|
436
|
+
* named source, held to that source -- the issuer under the RFC 5280 sec. 7.1 rule, a distribution point
|
|
437
|
+
* against the CRL's own `issuingDistributionPoint` where it states one -- and to the supplied `thisUpdate`.
|
|
438
|
+
* A request naming both sends the dpn (sec. 4.3.4: the dpn choice when a distribution point name is
|
|
439
|
+
* available) and holds the answer to the issuer as well. The issuer is REQUIRED, since a distribution
|
|
440
|
+
* point name alone leaves the answer unbound: a complete CRL states no scope to compare it against.
|
|
441
|
+
* Terminal `outcome`: `answered` (with `operation`, `present`, and the decoded `value`),
|
|
442
|
+
* `rejected`, or `poll-timeout`. An absent response value is `present: false` with a null `value`, which is
|
|
443
|
+
* how each of the four says "nothing available" -- never conflated with an empty result. Delayed delivery
|
|
444
|
+
* for both verbs arrives as an error message carrying the `waiting` status (sec. 4.4) and drives the same
|
|
445
|
+
* bounded poll loop, with the `pollReq` referring to the whole message.
|
|
446
|
+
*
|
|
329
447
|
* @opts
|
|
330
448
|
* - `url` -- REQUIRED: the CMP endpoint URL.
|
|
331
449
|
* - `key` + `cert` -- signature protection (the enrolling key pair + its cert), XOR `mac: { secret, ... }` for PBMAC1 protection.
|
|
@@ -343,10 +461,18 @@ function _certConfHash(certDer) {
|
|
|
343
461
|
* var session = pki.cmp.session({ url: "https://ca.example/cmp", key: signerKeyPkcs8, cert: signerCertDer, trustAnchors: [cmpCaCert], transport: cmpTransport });
|
|
344
462
|
* var result = await session.enroll({ ir: { certTemplate: { subject: [{ commonName: "device-42" }], publicKey: signerSpki } } });
|
|
345
463
|
* if (result.outcome === "issued") { var leaf = result.certificate; }
|
|
464
|
+
*
|
|
465
|
+
* var revoking = pki.cmp.session({ url: "https://ca.example/cmp", key: signerKeyPkcs8, cert: signerCertDer, trustAnchors: [cmpCaCert], transport: cmpTransport });
|
|
466
|
+
* var gone = await revoking.revoke({ certificate: signerCertDer, reason: "keyCompromise" });
|
|
467
|
+
* if (gone.outcome === "revoked") { var when = gone.status; }
|
|
468
|
+
*
|
|
469
|
+
* var asking = pki.cmp.session({ url: "https://ca.example/cmp", key: signerKeyPkcs8, cert: signerCertDer, trustAnchors: [cmpCaCert], transport: cmpTransport });
|
|
470
|
+
* var answer = await asking.info({ caCerts: true });
|
|
471
|
+
* if (answer.outcome === "answered" && answer.present) { var caCerts = answer.value; }
|
|
346
472
|
*/
|
|
347
473
|
function session(opts) {
|
|
348
474
|
if (opts == null) opts = {};
|
|
349
|
-
|
|
475
|
+
guard.identifier.assertPlainRecord(opts, _err, "cmp/bad-input", "opts");
|
|
350
476
|
guard.identifier.assertKnownKeys(opts, KNOWN_SESSION_OPTS, _err, "cmp/bad-input", "unknown session opts field ");
|
|
351
477
|
// Work on a SHALLOW COPY so a later normalization (e.g. an empty MAC trustAnchors list) never mutates the
|
|
352
478
|
// caller's object -- which may be frozen or reused across sessions -- and normalizing a frozen input does not
|
|
@@ -462,7 +588,14 @@ function session(opts) {
|
|
|
462
588
|
var caPubsBytes = 0; // running total of retained caPubs bytes -- a transaction-wide byte budget (below) bounds it independent of the count cap, since a single certificate can approach the DER limit
|
|
463
589
|
var caPubsWaitingCount = 0; // WAITING-leg entries retained at the FRONT of caPubsAccum -- only these are evictable to fit a grant entry; a grant entry (at the back) is the issued leaf's own chain and is never evicted
|
|
464
590
|
var activeCertReqId = DEFAULT_CERT_REQ_ID; // the certReqId of the in-flight enrollment; echoed in pollReq / certConf
|
|
465
|
-
var expectedRespArm = "ip"; // the
|
|
591
|
+
var expectedRespArm = "ip"; // the response arm the in-flight transaction must be answered by (RESPONSE_ARM)
|
|
592
|
+
// Which operation is in flight: it decides how the expected arm's body is read, and, for an error
|
|
593
|
+
// body, whether a `waiting` status announces delayed delivery. RFC 9483 sec. 4.4 puts that status
|
|
594
|
+
// in the ip/cp/kup of an enrollment and in an error message for every other operation, so the two
|
|
595
|
+
// readings are exclusive rather than a leniency applied to both.
|
|
596
|
+
var txnKind = "enroll";
|
|
597
|
+
var expectedInfoOp = null; // the INFO_OPS row of an in-flight support message
|
|
598
|
+
var crlQuery = null; // what a crlUpdate asked for: the encoded issuer Name and any thisUpdate
|
|
466
599
|
var requestedSpki = null; // the SPKI DER of the key the enrollment requested; the issued cert MUST carry it
|
|
467
600
|
var inFlight = false, completed = false, started = false; // one transaction per session; `started` = a request may have hit the transport
|
|
468
601
|
var transcript = [];
|
|
@@ -694,6 +827,8 @@ function session(opts) {
|
|
|
694
827
|
function _classify(verdict) {
|
|
695
828
|
var body = verdict.body || {};
|
|
696
829
|
var arm = body.arm;
|
|
830
|
+
if (arm === expectedRespArm && txnKind === "revoke") return _classifyRevRep(body);
|
|
831
|
+
if (arm === expectedRespArm && txnKind === "info") return _classifyGenRep(body);
|
|
697
832
|
if (arm === expectedRespArm) {
|
|
698
833
|
// Select the CertResponse for OUR request by its certReqId (RFC 9810 sec. 5.3.4) -- never blindly
|
|
699
834
|
// response[0], so a batch / misrouted response for a different request id is not read as ours.
|
|
@@ -744,10 +879,74 @@ function session(opts) {
|
|
|
744
879
|
if (code === 2) return { state: "rejected", resp: resp };
|
|
745
880
|
return { state: "unexpected", reason: "a " + arm + " CertResponse status code " + code + " has no transition" };
|
|
746
881
|
}
|
|
747
|
-
if (arm === "error") return
|
|
882
|
+
if (arm === "error") return _classifyError(body);
|
|
748
883
|
if (arm === "pollRep") return { state: "pollRep", entries: body.decoded };
|
|
749
884
|
if (arm === "pkiconf") return { state: "pkiconf" };
|
|
750
|
-
return { state: "unexpected", reason: "response arm " + JSON.stringify(arm) + " has no transition in
|
|
885
|
+
return { state: "unexpected", reason: "response arm " + JSON.stringify(arm) + " has no transition in a " + txnKind + " transaction" };
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// An error body. For a revocation or a support message it is ALSO how delayed delivery is announced:
|
|
889
|
+
// RFC 9483 sec. 4.4 puts the "waiting" status in an ip/cp/kup for ir/cr/kur/p10cr "and for responses
|
|
890
|
+
// to other request message types in an error message". An error answering an ENROLLMENT therefore
|
|
891
|
+
// stays a rejection -- that operation announces its own delay in its own cert-response arm, so
|
|
892
|
+
// reading waiting out of an error there would accept a shape the profile does not produce.
|
|
893
|
+
function _classifyError(body) {
|
|
894
|
+
var si = body.decoded && body.decoded.pKIStatusInfo;
|
|
895
|
+
var code = si && si.status ? si.status.code : null;
|
|
896
|
+
if (code === 3) {
|
|
897
|
+
// sec. 4.4 puts waiting in an ip/cp/kup when answering an enrollment and in an error message for
|
|
898
|
+
// anything else, so a waiting error drives the poll loop only for revoke/info. For an enrollment a
|
|
899
|
+
// waiting ERROR is off-profile and refused, not coerced to a rejection whose own status reads
|
|
900
|
+
// "waiting". A wait is not a failure, so sec. 4.4 forbids failInfo on it -- a waiting error carrying
|
|
901
|
+
// one is contradictory and refused rather than polled.
|
|
902
|
+
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)");
|
|
903
|
+
if (si.failInfo != null) throw _err("cmp/bad-error", "an error message with status waiting must not carry failInfo (RFC 9483 sec. 4.4)");
|
|
904
|
+
return { state: "waiting", resp: { status: si } };
|
|
905
|
+
}
|
|
906
|
+
// sec. 4.2 / 3.6.4: an error message that is not delayed delivery is a rejection, whose status is
|
|
907
|
+
// rejection(2). accepted(0), grantedWithMods(1), a status in 4..6, an unknown value, or an absent
|
|
908
|
+
// status is malformed for an error message, so it is refused rather than surfaced as a definitive
|
|
909
|
+
// rejected verdict a caller would act on -- the same call the rp classifier makes for an unsupported
|
|
910
|
+
// status (a trusted "rejected" whose PKIStatusInfo says "accepted" is a contradiction, not a verdict).
|
|
911
|
+
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));
|
|
912
|
+
return { state: "rejected", status: si };
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// RevRepContent. RFC 9483 sec. 4.2: the response to an rr "MUST be an rp message containing a single
|
|
916
|
+
// status field", whose positive value is "accepted" and negative "rejection", with failInfo "MUST be
|
|
917
|
+
// absent if the status is accepted". A status with no transition here is a hard stop, never a verdict.
|
|
918
|
+
function _classifyRevRep(body) {
|
|
919
|
+
var d = body.decoded || {};
|
|
920
|
+
var list = d.status || [];
|
|
921
|
+
if (list.length !== 1) {
|
|
922
|
+
return { state: "unexpected", reason: "an rp answering a revocation must carry exactly one status (RFC 9483 sec. 4.2); got " + list.length };
|
|
923
|
+
}
|
|
924
|
+
var si = list[0];
|
|
925
|
+
var code = si && si.status ? si.status.code : null;
|
|
926
|
+
if (code === 0) {
|
|
927
|
+
if (si.failInfo != null) {
|
|
928
|
+
return { state: "unexpected", reason: "an accepted rp must not carry failInfo (RFC 9483 sec. 4.2)" };
|
|
929
|
+
}
|
|
930
|
+
return { state: "granted", status: si, revCerts: d.revCerts, crls: d.crls };
|
|
931
|
+
}
|
|
932
|
+
if (code === 2) return { state: "rejected", status: si };
|
|
933
|
+
return { state: "unexpected", reason: "an rp status code " + code + " has no transition in a revocation (RFC 9483 sec. 4.2 allows accepted or rejection)" };
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// GenRepContent. RFC 9483 sec. 4.3: a genp "MUST contain a sequence of one element of type
|
|
937
|
+
// InfoTypeAndValue" whose infoType names the operation. The OID is checked against the RESPONSE
|
|
938
|
+
// side of the INFO_OPS row, which for rootCaCert is not the OID the request carried.
|
|
939
|
+
function _classifyGenRep(body) {
|
|
940
|
+
var items = body.decoded || [];
|
|
941
|
+
if (items.length !== 1) {
|
|
942
|
+
return { state: "unexpected", reason: "a genp must carry exactly one InfoTypeAndValue (RFC 9483 sec. 4.3); got " + items.length };
|
|
943
|
+
}
|
|
944
|
+
var itav = items[0];
|
|
945
|
+
var want = oid.byName(expectedInfoOp.responseOid);
|
|
946
|
+
if (itav.type !== want) {
|
|
947
|
+
return { state: "unexpected", reason: "a genp answering " + expectedInfoOp.name + " must carry infoType " + expectedInfoOp.responseOid + " (" + want + "); got " + itav.type };
|
|
948
|
+
}
|
|
949
|
+
return { state: "granted", itav: itav };
|
|
751
950
|
}
|
|
752
951
|
|
|
753
952
|
function _statusOf(resp) { return resp && resp.status ? resp.status : null; }
|
|
@@ -969,14 +1168,45 @@ function session(opts) {
|
|
|
969
1168
|
});
|
|
970
1169
|
}
|
|
971
1170
|
|
|
1171
|
+
// One transaction per session (RFC 9810 sec. 5.1.1: one transactionID per transaction). A second or
|
|
1172
|
+
// concurrent call on any verb would reuse this transaction's transactionID / nonce chain / certReqId --
|
|
1173
|
+
// a replay or a corrupted interleave -- so it is refused. Every verb checks this BEFORE marking the
|
|
1174
|
+
// transaction in flight, so a malformed-request call does not consume the session.
|
|
1175
|
+
function _assertFresh(what) {
|
|
1176
|
+
if (completed || inFlight) {
|
|
1177
|
+
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)");
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// The shared tail of a non-enrollment transaction: send the one request, poll through a delayed
|
|
1182
|
+
// delivery, and hand back the terminal classification. Both verbs reach a verdict the same way, so
|
|
1183
|
+
// the poll budget, the nonce chaining and the verify-before-read ordering cannot differ between them.
|
|
1184
|
+
async function _runOneShot(bodySpec, arm, kind) {
|
|
1185
|
+
txnKind = kind;
|
|
1186
|
+
expectedRespArm = RESPONSE_ARM[arm];
|
|
1187
|
+
// A pollReq raised by this transaction refers to the whole message (RFC 9483 sec. 4.4).
|
|
1188
|
+
activeCertReqId = WHOLE_MESSAGE_CERT_REQ_ID;
|
|
1189
|
+
var verdict = await _send(bodySpec, arm);
|
|
1190
|
+
var t = _classify(verdict);
|
|
1191
|
+
var polls = 0;
|
|
1192
|
+
if (t.state === "waiting") {
|
|
1193
|
+
var polled = await _pollLoop(t.resp);
|
|
1194
|
+
polls = polled.polls;
|
|
1195
|
+
if (polled.timeout) return { timeout: true, polls: polls, status: polled.status };
|
|
1196
|
+
t = polled.done;
|
|
1197
|
+
}
|
|
1198
|
+
return { done: t, polls: polls };
|
|
1199
|
+
}
|
|
1200
|
+
|
|
972
1201
|
// The transaction driver: build the initial request arm, send it, then poll / confirm / terminate.
|
|
973
1202
|
async function enroll(request) {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
//
|
|
977
|
-
//
|
|
978
|
-
|
|
979
|
-
|
|
1203
|
+
_assertFresh("enrollment");
|
|
1204
|
+
guard.identifier.assertPlainRecord(request, _err, "cmp/bad-input", "enroll(request), a body spec { ir | cr | kur | p10cr },");
|
|
1205
|
+
// A key here that is not an arm needs no door of its own: this whole object becomes the message
|
|
1206
|
+
// body, and pki.cmp.build counts the body's keys without filtering out the null-valued ones, so
|
|
1207
|
+
// a misspelling set to null is already refused there ("must have exactly one arm, got 2"). The
|
|
1208
|
+
// info() request is the opposite case and does carry a door, because that object is read for a
|
|
1209
|
+
// name and never handed to the builder.
|
|
980
1210
|
var arms = Object.keys(request).filter(function (k) { return request[k] != null; });
|
|
981
1211
|
if (arms.length !== 1 || !ENROLL_ARMS[arms[0]]) {
|
|
982
1212
|
throw _err("cmp/bad-input", "enroll(request) must carry EXACTLY ONE enrollment arm (ir / cr / kur / p10cr)");
|
|
@@ -1013,6 +1243,7 @@ function session(opts) {
|
|
|
1013
1243
|
}
|
|
1014
1244
|
inFlight = true;
|
|
1015
1245
|
try {
|
|
1246
|
+
txnKind = "enroll";
|
|
1016
1247
|
expectedRespArm = RESPONSE_ARM[arms[0]]; // ir->ip, cr/p10cr->cp, kur->kup: the arm this request must be answered by
|
|
1017
1248
|
// The certReqId the session echoes in pollReq / certConf and matches in every CertResponse: the caller's
|
|
1018
1249
|
// CRMF request id (ir / cr / kur) when supplied, else the arm default -- a p10cr has no CRMF id, so a
|
|
@@ -1043,8 +1274,519 @@ function session(opts) {
|
|
|
1043
1274
|
}
|
|
1044
1275
|
}
|
|
1045
1276
|
|
|
1277
|
+
// RFC 9483 sec. 4.2: "The revocation request message MUST be signed using the certificate that is to
|
|
1278
|
+
// be revoked to prove the authorization to revoke." The signature over this message IS the
|
|
1279
|
+
// authorization, so the certificate named in certDetails has to be the one this session protects with.
|
|
1280
|
+
// Both request shapes are compared here, through the same encode-then-read path, so the certificate
|
|
1281
|
+
// form and the explicit certDetails form cannot diverge on what counts as the same certificate.
|
|
1282
|
+
// The issuer comparison is the RFC 5280 sec. 7.1 canonical one, since two spellings of a
|
|
1283
|
+
// distinguished name name the same issuer and a byte compare would refuse a caller who wrote it out.
|
|
1284
|
+
function _assertRevokingOwnCertificate(certTemplateDer) {
|
|
1285
|
+
var own;
|
|
1286
|
+
try {
|
|
1287
|
+
own = guard.parsed.acceptDerived(opts.cert, "certificate", x509.parse, _err, "cmp/bad-input", "opts.cert");
|
|
1288
|
+
} catch (e) {
|
|
1289
|
+
if (e && e.isCmpError) throw e;
|
|
1290
|
+
throw _err("cmp/bad-input", "opts.cert must be a certificate to revoke one (RFC 9483 sec. 4.2)", e);
|
|
1291
|
+
}
|
|
1292
|
+
var tmpl = schemaCrmf.walkCertTemplate(asn1.decode(certTemplateDer));
|
|
1293
|
+
var sameSerial = tmpl.serialNumber != null && own.serialNumber != null && BigInt(tmpl.serialNumber) === BigInt(own.serialNumber);
|
|
1294
|
+
var sameIssuer = tmpl.issuer != null && guard.name.dnEqual(tmpl.issuer.rdns, own.issuer.rdns, _err, "cmp/bad-input", "the revoked certificate issuer");
|
|
1295
|
+
if (!sameSerial || !sameIssuer) {
|
|
1296
|
+
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.");
|
|
1297
|
+
}
|
|
1298
|
+
return own; // the parsed session certificate, so the response's revCerts can be bound to it
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// An rp MAY name the certificates it revoked in revCerts (RFC 4210 RevRepContent). This session
|
|
1302
|
+
// revokes exactly one -- its own -- so a conforming rp names exactly that one, and a response that
|
|
1303
|
+
// names another issuer/serial, or several, is not a verdict about this request. Left unbound, a
|
|
1304
|
+
// verified verdict would report an unrelated certificate as revoked. Compared to the session
|
|
1305
|
+
// certificate `own` the request was already held to, by serialNumber and the RFC 5280 sec. 7.1
|
|
1306
|
+
// canonical issuer rule. A CertId issuer is a GeneralName; a certificate's issuer is a directoryName,
|
|
1307
|
+
// so any other arm cannot name the revoked certificate.
|
|
1308
|
+
function _bindRevCerts(revCerts, own) {
|
|
1309
|
+
if (revCerts == null) return null;
|
|
1310
|
+
if (revCerts.length !== 1) {
|
|
1311
|
+
throw _err("cmp/bad-rev-rep", "an rp for a single revocation must name exactly one certificate in revCerts (RFC 9483 sec. 4.2); got " + revCerts.length);
|
|
1312
|
+
}
|
|
1313
|
+
var cid = revCerts[0];
|
|
1314
|
+
var dn = cid.issuer && cid.issuer.tagClass === "context" && cid.issuer.tagNumber === 4 ? cid.issuer.value : null;
|
|
1315
|
+
var sameSerial = cid.serialNumber != null && own.serialNumber != null && BigInt(cid.serialNumber) === BigInt(own.serialNumber);
|
|
1316
|
+
var sameIssuer = dn != null && guard.name.dnEqual(dn.rdns, own.issuer.rdns, _err, "cmp/bad-rev-rep", "the revCerts issuer");
|
|
1317
|
+
if (!sameSerial || !sameIssuer) {
|
|
1318
|
+
throw _err("cmp/bad-rev-rep", "the rp revCerts names a certificate other than the one this session revoked (RFC 9483 sec. 4.2), so a verified verdict must not report it as revoked");
|
|
1319
|
+
}
|
|
1320
|
+
return revCerts;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Request revocation of this session's own certificate (RFC 9483 sec. 4.2).
|
|
1325
|
+
* `request` is `{ certificate }` or `{ certDetails: { issuer, serialNumber } }`, plus an optional
|
|
1326
|
+
* `reason` (a CRLReason name). Returns a terminal verdict: `revoked`, `rejected`, or `poll-timeout`.
|
|
1327
|
+
*/
|
|
1328
|
+
async function revoke(request) {
|
|
1329
|
+
_assertFresh("revocation");
|
|
1330
|
+
guard.identifier.assertPlainRecord(request, _err, "cmp/bad-input", "revoke(request), an object { certificate | certDetails, reason? },");
|
|
1331
|
+
guard.identifier.assertKnownKeys(request, KNOWN_REVOKE_KEYS, _err, "cmp/bad-input", "unknown revoke request field ");
|
|
1332
|
+
// A PBMAC1 session shares a secret with the CA; it holds no certificate, so it has nothing to sign
|
|
1333
|
+
// the request with and no way to demonstrate which certificate it is entitled to revoke.
|
|
1334
|
+
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 }).");
|
|
1335
|
+
// Each field is read ONCE, here, and every use below is of the value that was read. A caller
|
|
1336
|
+
// field can be an accessor, so a second read can answer with a different certificate or reason
|
|
1337
|
+
// from the one that was checked, and the message would carry what the checks never saw.
|
|
1338
|
+
var certificateArg = request.certificate, reasonArg = request.reason;
|
|
1339
|
+
var certDetails = request.certDetails;
|
|
1340
|
+
if ((certificateArg == null) === (certDetails == null)) {
|
|
1341
|
+
throw _err("cmp/bad-input", "revoke(request) names the certificate by EXACTLY ONE of certificate (the certificate itself) or certDetails ({ issuer, serialNumber })");
|
|
1342
|
+
}
|
|
1343
|
+
if (reasonArg != null && (typeof reasonArg !== "string" || !CRL_REASON_NAMES[reasonArg])) {
|
|
1344
|
+
throw _err("cmp/bad-input", "revoke request.reason must be a CRLReason name (RFC 5280 sec. 5.3.1); got " + JSON.stringify(reasonArg));
|
|
1345
|
+
}
|
|
1346
|
+
if (certificateArg != null) {
|
|
1347
|
+
var target;
|
|
1348
|
+
try {
|
|
1349
|
+
target = guard.parsed.acceptDerived(certificateArg, "certificate", x509.parse, _err, "cmp/bad-input", "revoke request.certificate");
|
|
1350
|
+
} catch (e) {
|
|
1351
|
+
if (e && e.isCmpError) throw e;
|
|
1352
|
+
throw _err("cmp/bad-input", "revoke request.certificate must be a certificate (DER / PEM / parsed)", e);
|
|
1353
|
+
}
|
|
1354
|
+
certDetails = { issuer: target.issuer.bytes, serialNumber: target.serialNumber };
|
|
1355
|
+
}
|
|
1356
|
+
// Encode the CertTemplate HERE rather than letting cmp.build do it, so the identity check below
|
|
1357
|
+
// reads the same bytes the message will carry instead of a second interpretation of the spec.
|
|
1358
|
+
var certTemplateDer;
|
|
1359
|
+
try {
|
|
1360
|
+
certTemplateDer = crmfSign.buildCertTemplate(certDetails);
|
|
1361
|
+
} catch (e) {
|
|
1362
|
+
if (e && e.isCmpError) throw e;
|
|
1363
|
+
throw _err("cmp/bad-rev-req", "revoke request.certDetails must be a CertTemplate naming issuer and serialNumber -- " + ((e && e.message) || e), e);
|
|
1364
|
+
}
|
|
1365
|
+
var own = _assertRevokingOwnCertificate(certTemplateDer);
|
|
1366
|
+
// sec. 4.2: exactly one RevDetails, and crlEntryDetails REQUIRED carrying one reasonCode. The
|
|
1367
|
+
// builder emits the reasonCode even at unspecified(0), which the same clause requires.
|
|
1368
|
+
var body = { rr: [{ certDetails: certTemplateDer, crlEntryDetails: { reason: reasonArg == null ? undefined : reasonArg } }] };
|
|
1369
|
+
inFlight = true;
|
|
1370
|
+
try {
|
|
1371
|
+
var out = await _runOneShot(body, "rr", "revoke");
|
|
1372
|
+
if (out.timeout) return _terminal("poll-timeout", { status: out.status, polls: out.polls });
|
|
1373
|
+
var t = out.done;
|
|
1374
|
+
if (t.state === "granted") {
|
|
1375
|
+
return _terminal("revoked", {
|
|
1376
|
+
status: t.status, polls: out.polls,
|
|
1377
|
+
revokedCerts: _bindRevCerts(t.revCerts, own), // the CertIds the responder confirms it revoked, bound to the certificate this session revoked
|
|
1378
|
+
// An rp MAY deliver CRLs, and the schema reads those slots as raw SEQUENCEs for the same
|
|
1379
|
+
// reason it does in a genp. They are surfaced on a verdict an operator acts on, so they
|
|
1380
|
+
// are held to being CertificateLists here, exactly as the support-message values are.
|
|
1381
|
+
crls: (t.crls || []).map(_asCrl),
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
if (t.state === "rejected") return _terminal("rejected", { status: t.status || null, polls: out.polls });
|
|
1385
|
+
throw _err("cmp/unexpected-arm", t.reason || "the revocation transaction reached an unexpected state");
|
|
1386
|
+
} finally {
|
|
1387
|
+
inFlight = false;
|
|
1388
|
+
completed = started;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Ask the PKI management entity for one of the RFC 9483 sec. 4.3 support values.
|
|
1394
|
+
* `request` names exactly one operation: `{ caCerts: true }`, `{ rootCaCert: <cert> }`,
|
|
1395
|
+
* `{ certReqTemplate: true }`, or `{ crlUpdate: { issuer, dpn?, thisUpdate? } }`.
|
|
1396
|
+
* Returns a terminal verdict: `answered` (with `value` and `present`), `rejected`, or `poll-timeout`.
|
|
1397
|
+
*/
|
|
1398
|
+
async function info(request) {
|
|
1399
|
+
_assertFresh("support message");
|
|
1400
|
+
guard.identifier.assertPlainRecord(request, _err, "cmp/bad-input", "info(request), an object naming one support operation (caCerts / rootCaCert / certReqTemplate / crlUpdate),");
|
|
1401
|
+
// The name door runs BEFORE the arm count, so a key that is not an operation is refused whatever
|
|
1402
|
+
// its value is. Counting non-null keys alone would let a misspelled one set to null pass as
|
|
1403
|
+
// absent, and the caller would watch a different operation go out than the one they wrote.
|
|
1404
|
+
guard.identifier.assertKnownKeys(request, INFO_OPS, _err, "cmp/bad-input", "unknown info request field ");
|
|
1405
|
+
// Each field is read ONCE, here, and everything below uses the value that was read. A caller
|
|
1406
|
+
// field can be an accessor, so a second read can answer with a different request from the one
|
|
1407
|
+
// the arm count was taken over, and what goes on the wire would not be what was checked.
|
|
1408
|
+
var keys = Object.keys(request), names = [], values = [];
|
|
1409
|
+
for (var ki = 0; ki < keys.length; ki++) {
|
|
1410
|
+
var kv = request[keys[ki]];
|
|
1411
|
+
if (kv == null) continue;
|
|
1412
|
+
names[names.length] = keys[ki];
|
|
1413
|
+
values[values.length] = kv;
|
|
1414
|
+
}
|
|
1415
|
+
if (names.length !== 1) {
|
|
1416
|
+
throw _err("cmp/bad-input", "info(request) must name EXACTLY ONE support operation (caCerts / rootCaCert / certReqTemplate / crlUpdate); a genm carries a sequence of one InfoTypeAndValue (RFC 9483 sec. 4.3)");
|
|
1417
|
+
}
|
|
1418
|
+
var op = INFO_OPS[names[0]], asked = values[0];
|
|
1419
|
+
var itav = { infoType: op.requestOid };
|
|
1420
|
+
// sec. 4.3.1 and sec. 4.3.3 both make the request infoValue absent; the other two carry one.
|
|
1421
|
+
// For the two that carry none, the only accepted value is `true` -- a caller who supplies a
|
|
1422
|
+
// certificate or a template to one of them means something this operation cannot send, and
|
|
1423
|
+
// taking it silently would have the request go out without the constraint they wrote.
|
|
1424
|
+
if (op.value === false) {
|
|
1425
|
+
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)");
|
|
1426
|
+
} else if (op.value === "cert") {
|
|
1427
|
+
// sec. 4.3.2: the request SHOULD carry the root CA certificate the update is requested for,
|
|
1428
|
+
// and the field is a CMPCertificate. It goes through the SAME door revoke() gives a
|
|
1429
|
+
// certificate -- DER, PEM, or an already-parsed one -- because it is the same kind of value
|
|
1430
|
+
// and a caller should not have to remember which of the two verbs takes which form. Holding
|
|
1431
|
+
// it to being a certificate here keeps a mistake a local refusal; sending it unchecked would
|
|
1432
|
+
// put a malformed request on the wire and consume this one-shot transaction to learn what the
|
|
1433
|
+
// caller could have been told before it started.
|
|
1434
|
+
var rootCaCertDer = _certificateArgument(asked, "info request rootCaCert");
|
|
1435
|
+
// sec. 4.3.2: rootCaCert names the CURRENT root the update chains from, which is a CA. An
|
|
1436
|
+
// end-entity certificate here would let a cross-certificate signed by that non-CA key read as a
|
|
1437
|
+
// rollover (the response path verifies newWithOld / oldWithNew against this certificate's key), so
|
|
1438
|
+
// it is held to being a CA before the one-shot transaction engages rather than after.
|
|
1439
|
+
_assertCaCapable(x509.parse(rootCaCertDer), "info request rootCaCert", "cmp/bad-input");
|
|
1440
|
+
itav.infoValue = rootCaCertDer;
|
|
1441
|
+
} else if (op.value === "crlStatus") {
|
|
1442
|
+
var built = cmpBuild.buildCrlStatusList(asked);
|
|
1443
|
+
itav.infoValue = built.der;
|
|
1444
|
+
// What the request asked for, kept so the answer can be held to it. sec. 4.3.4 says the
|
|
1445
|
+
// responder returns the latest CRL FROM THE REFERENCED SOURCE, and only when it is more
|
|
1446
|
+
// recent than a supplied thisUpdate; in every other case the response value is absent.
|
|
1447
|
+
// The cutoff comes back FROM the builder, which read it once while encoding. Reading the
|
|
1448
|
+
// caller's field a second time here would let an accessor, or a Date moved during the
|
|
1449
|
+
// transport round-trip, judge the response against a query the request never carried.
|
|
1450
|
+
crlQuery = { issuerName: built.issuerName, thisUpdate: built.thisUpdate, dpn: built.dpn };
|
|
1451
|
+
}
|
|
1452
|
+
expectedInfoOp = op;
|
|
1453
|
+
inFlight = true;
|
|
1454
|
+
try {
|
|
1455
|
+
var out = await _runOneShot({ genm: [itav] }, "genm", "info");
|
|
1456
|
+
if (out.timeout) return _terminal("poll-timeout", { operation: op.name, status: out.status, polls: out.polls });
|
|
1457
|
+
var t = out.done;
|
|
1458
|
+
if (t.state === "granted") {
|
|
1459
|
+
// sec. 4.3: the infoValue is OPTIONAL in every one of the four responses, and its absence is
|
|
1460
|
+
// the answer "nothing is available" rather than a malformed message -- so `present` reports
|
|
1461
|
+
// which of the two a caller received, and never conflates them into an empty value.
|
|
1462
|
+
var raw = t.itav.value;
|
|
1463
|
+
var value = null;
|
|
1464
|
+
if (raw != null) {
|
|
1465
|
+
try {
|
|
1466
|
+
value = schemaCmp[op.read](raw);
|
|
1467
|
+
} catch (e) {
|
|
1468
|
+
if (e && e.isCmpError) throw e;
|
|
1469
|
+
throw _err("cmp/bad-info-value", "the " + op.name + " response value is malformed -- " + ((e && e.message) || e), e);
|
|
1470
|
+
}
|
|
1471
|
+
value = await _checkInfoValue(op, value, itav.infoValue);
|
|
1472
|
+
}
|
|
1473
|
+
return _terminal("answered", { operation: op.name, present: raw != null, value: value, status: null, polls: out.polls });
|
|
1474
|
+
}
|
|
1475
|
+
if (t.state === "rejected") return _terminal("rejected", { operation: op.name, status: t.status || null, polls: out.polls });
|
|
1476
|
+
throw _err("cmp/unexpected-arm", t.reason || "the support-message transaction reached an unexpected state");
|
|
1477
|
+
} finally {
|
|
1478
|
+
inFlight = false;
|
|
1479
|
+
completed = started;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// The RFC 9483 profile rules that sit ON TOP of the RFC 9480 syntax the decoders enforce. Each is a
|
|
1484
|
+
// requirement the ASN.1 cannot express, so a structurally valid response can still be non-conforming.
|
|
1485
|
+
// `requestValue` is the infoValue this operation SENT, which the rootCaCert rules compare against:
|
|
1486
|
+
// the old root the caller named is what the update has to chain from. It is read here rather than
|
|
1487
|
+
// off `request`, so the comparison uses the bytes that went on the wire.
|
|
1488
|
+
function _checkInfoValue(op, value, requestValue) {
|
|
1489
|
+
if (op.name === "rootCaCert") {
|
|
1490
|
+
// sec. 4.3.2 makes newWithOld REQUIRED of a response ("it is needed for the receiving entity
|
|
1491
|
+
// trusting the old root CA certificate to gain trust in the new one"), where RFC 9480's ASN.1
|
|
1492
|
+
// marks it OPTIONAL. Without it the update cannot be trusted by the entity that asked for it.
|
|
1493
|
+
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)");
|
|
1494
|
+
return _checkRootCaKeyUpdate(value, requestValue);
|
|
1495
|
+
}
|
|
1496
|
+
if (op.name === "certReqTemplate") {
|
|
1497
|
+
var t = value.certTemplate;
|
|
1498
|
+
// sec. 4.3.3: "The publicKey field of type SubjectPublicKeyInfo in the CertTemplate of the
|
|
1499
|
+
// CertReqTemplateValue MUST be omitted", and "the serialNumber, signingAlg, issuerUID, and
|
|
1500
|
+
// subjectUID fields MUST be omitted". A template stating a key would tell an entity to request
|
|
1501
|
+
// a certificate over a key the responder chose, which is what the keySpec field exists to avoid.
|
|
1502
|
+
["publicKey", "serialNumber", "signingAlg", "issuerUID", "subjectUID"].forEach(function (f) {
|
|
1503
|
+
if (t[f] != null) throw _err("cmp/bad-info-value", "a certReqTemplate certTemplate must omit " + f + " (RFC 9483 sec. 4.3.3)");
|
|
1504
|
+
});
|
|
1505
|
+
if (value.keySpec != null) value.keySpec.forEach(_checkKeySpec);
|
|
1506
|
+
return value;
|
|
1507
|
+
}
|
|
1508
|
+
if (op.name === "caCerts") return value.map(function (d) {
|
|
1509
|
+
// sec. 4.3.1 returns CA certificates for chain construction, so each is held to being one: an
|
|
1510
|
+
// authenticated responder placing an end-entity certificate here would hand back a list nothing
|
|
1511
|
+
// can chain through. The same CA-capability rule the rootCaKeyUpdate rollover certificates get.
|
|
1512
|
+
var der = _asCertificate(d, "a caCerts entry");
|
|
1513
|
+
_assertCaCapable(x509.parse(der), "caCerts entry");
|
|
1514
|
+
return der;
|
|
1515
|
+
});
|
|
1516
|
+
if (op.name === "crlUpdate") {
|
|
1517
|
+
// sec. 4.3.4: the request carries exactly one CRLStatus (one source), and the response "represents
|
|
1518
|
+
// the latest CRL available from the referenced CRL source" -- one CRL. The ASN.1 list is SIZE
|
|
1519
|
+
// (1..MAX), so the responder could return several; accepting them all would surface an older CRL
|
|
1520
|
+
// beside the latest and let a caller act on a stale value[0]. Exactly one is required, and it is
|
|
1521
|
+
// then bound to the query that asked for it.
|
|
1522
|
+
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);
|
|
1523
|
+
return value.map(_bindCrlToQuery);
|
|
1524
|
+
}
|
|
1525
|
+
// Unreachable while INFO_OPS holds exactly the four operations above, each with a branch here.
|
|
1526
|
+
// It refuses rather than passing the value through, so adding a fifth operation without its
|
|
1527
|
+
// profile rules fails at once instead of surfacing a value nothing checked.
|
|
1528
|
+
throw _err("cmp/bad-info-value", "no response rules are defined for the " + op.name + " support message");
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// A support-message response is surfaced as an ANSWER an operator acts on, so what it carries is
|
|
1532
|
+
// held to being the structure it claims. The CMP schema reads these slots as raw SEQUENCEs on
|
|
1533
|
+
// purpose (the parser confers no trust and a CMPCertificate slot is opaque to it), which leaves
|
|
1534
|
+
// "is this actually a certificate" to the verb that hands the bytes back -- the same division
|
|
1535
|
+
// this module already applies to an authenticated caPubs entry. Without it a responder could
|
|
1536
|
+
// answer a chain-construction request with any well-formed SEQUENCE and have it read as a
|
|
1537
|
+
// certificate, and a CRL request with a certificate. Returns a COPY, since the decoded value is
|
|
1538
|
+
// a slice of the response buffer.
|
|
1539
|
+
// A root CA key update is three certificates in NAMED relationships, and the relationships are the
|
|
1540
|
+
// whole mechanism (RFC 9483 sec. 4.3.2): newWithOld carries "the new public root CA key signed
|
|
1541
|
+
// with the old private root CA key", which is what lets an entity that trusts the old root reach
|
|
1542
|
+
// the new one, and oldWithNew carries "the old public root CA key signed with the new private root
|
|
1543
|
+
// CA key" for the reverse. Three certificates that merely parse establish neither, so a responder
|
|
1544
|
+
// could answer with any three it holds and the update would be reported as usable. Each signature
|
|
1545
|
+
// is checked by the path engine, so the EdDSA low-order-point and algorithm-confusion gates apply
|
|
1546
|
+
// here as they do to any other untrusted signature.
|
|
1547
|
+
async function _checkRootCaKeyUpdate(value, oldRootDer) {
|
|
1548
|
+
var newWithNew = _asCertificate(value.newWithNew, "the rootCaKeyUpdate newWithNew");
|
|
1549
|
+
var newWithOld = _asCertificate(value.newWithOld, "the rootCaKeyUpdate newWithOld");
|
|
1550
|
+
var oldWithNew = value.oldWithNew == null ? null : _asCertificate(value.oldWithNew, "the rootCaKeyUpdate oldWithNew");
|
|
1551
|
+
var pNewNew = x509.parse(newWithNew), pNewOld = x509.parse(newWithOld), pOldRoot = x509.parse(oldRootDer);
|
|
1552
|
+
_assertCaCapable(pNewNew, "rootCaKeyUpdate newWithNew");
|
|
1553
|
+
_assertCaCapable(pNewOld, "rootCaKeyUpdate newWithOld");
|
|
1554
|
+
if (!cmpBuild.samePublicKey(pNewOld.subjectPublicKeyInfo.bytes, pNewNew.subjectPublicKeyInfo.bytes)) {
|
|
1555
|
+
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)");
|
|
1556
|
+
}
|
|
1557
|
+
// A key alone does not make a trust transition. If the old CA has ever signed ANY certificate
|
|
1558
|
+
// for the new key -- an ordinary end-entity certificate its holder legitimately obtained --
|
|
1559
|
+
// then key equality plus a valid signature is satisfied by that certificate, and its holder
|
|
1560
|
+
// could pair it with a self-signed newWithNew of their own choosing and have the result read as
|
|
1561
|
+
// the authority's own rollover. What sec. 4.3.2 describes is a certificate FOR the new root
|
|
1562
|
+
// issued BY the old one, so both names are bound too, under the RFC 5280 sec. 7.1 comparison.
|
|
1563
|
+
_assertSameName(pNewOld.issuer, pOldRoot.subject, "newWithOld", "be issued by the OLD root named in the request");
|
|
1564
|
+
_assertSameName(pNewOld.subject, pNewNew.subject, "newWithOld", "name the same subject as newWithNew, the certificate it vouches for");
|
|
1565
|
+
// newWithNew is the replacement certificate, and for a root it is self-issued, so its own key is
|
|
1566
|
+
// the only thing that can vouch for the name, validity and extensions the caller would install.
|
|
1567
|
+
// sec. 4.3.2's note extends this operation to "trusted non-root certificates, e.g., directly
|
|
1568
|
+
// trusted intermediate or issuing CA certificates", and such a certificate is signed by an
|
|
1569
|
+
// issuer this message does not carry -- there is nothing here to check it against, and claiming
|
|
1570
|
+
// otherwise would refuse a use the profile allows. So the signature is verified exactly when it
|
|
1571
|
+
// is verifiable: when the certificate is self-issued. Either way the new KEY is authenticated by
|
|
1572
|
+
// newWithOld below, which the old root signed and which must carry this same key.
|
|
1573
|
+
if (guard.name.dnEqual(pNewNew.issuer.rdns, pNewNew.subject.rdns, _err, "cmp/bad-info-value", "the rootCaKeyUpdate newWithNew name")) {
|
|
1574
|
+
await _assertSignedBy(pNewNew, pNewNew, "newWithNew", "its own key, which a self-issued root certificate must be");
|
|
1575
|
+
}
|
|
1576
|
+
await _assertSignedBy(pNewOld, pOldRoot, "newWithOld", "the OLD root CA key");
|
|
1577
|
+
if (oldWithNew !== null) {
|
|
1578
|
+
var pOldNew = x509.parse(oldWithNew);
|
|
1579
|
+
_assertCaCapable(pOldNew, "rootCaKeyUpdate oldWithNew");
|
|
1580
|
+
if (!cmpBuild.samePublicKey(pOldNew.subjectPublicKeyInfo.bytes, pOldRoot.subjectPublicKeyInfo.bytes)) {
|
|
1581
|
+
throw _err("cmp/bad-info-value", "the rootCaKeyUpdate oldWithNew must certify the OLD root key (RFC 9483 sec. 4.3.2)");
|
|
1582
|
+
}
|
|
1583
|
+
_assertSameName(pOldNew.issuer, pNewNew.subject, "oldWithNew", "be issued by the NEW root");
|
|
1584
|
+
_assertSameName(pOldNew.subject, pOldRoot.subject, "oldWithNew", "name the OLD root it vouches for");
|
|
1585
|
+
await _assertSignedBy(pOldNew, pNewNew, "oldWithNew", "the NEW root CA key");
|
|
1586
|
+
}
|
|
1587
|
+
return { newWithNew: newWithNew, newWithOld: newWithOld, oldWithNew: oldWithNew };
|
|
1588
|
+
}
|
|
1589
|
+
// A certificate a caller will build a chain from -- a rootCaKeyUpdate rollover certificate or a
|
|
1590
|
+
// caCerts entry -- has to actually be a CA. Names, a key and a signature are satisfied by an
|
|
1591
|
+
// ordinary end-entity certificate that certifies nothing, so surfacing one as an issuer reports the
|
|
1592
|
+
// answer as usable while handing back a list nothing can chain through. The rule is RFC 5280 sec.
|
|
1593
|
+
// 6.1.4's, the one pki.path.validate applies to any intermediate: basicConstraints cA TRUE, marked
|
|
1594
|
+
// critical (sec. 4.2.1.9), and keyUsage, where present, allowing keyCertSign. Criticality matters
|
|
1595
|
+
// because a relying party that skips extensions it does not recognize would not see the cA bit at
|
|
1596
|
+
// all, which is why the path validator refuses a non-critical cA:TRUE; this check holds to the same
|
|
1597
|
+
// line so the two never disagree on what a CA certificate is. `which` names the surface for the
|
|
1598
|
+
// error. Extensions are read through the shared certificate-extension decoders rather than walked here.
|
|
1599
|
+
function _assertCaCapable(cert, which, code) {
|
|
1600
|
+
code = code || "cmp/bad-info-value"; // a response cert is cmp/bad-info-value; a caller-supplied one (rootCaCert request) is cmp/bad-input
|
|
1601
|
+
var bc = null, ku = null, bcCritical = false;
|
|
1602
|
+
cert.extensions.forEach(function (e) {
|
|
1603
|
+
if (e.oid === OID_BASIC_CONSTRAINTS) { bc = cmpBuild.decodeCertExtension(e.oid, e.value); bcCritical = e.critical === true; }
|
|
1604
|
+
if (e.oid === OID_KEY_USAGE) ku = cmpBuild.decodeCertExtension(e.oid, e.value);
|
|
1605
|
+
});
|
|
1606
|
+
if (!bc || bc.cA !== true) {
|
|
1607
|
+
throw _err(code, "the " + which + " must be a CA certificate (basicConstraints cA TRUE): an end-entity certificate certifies nothing and cannot serve as an issuer for chain construction (RFC 5280 sec. 6.1.4)");
|
|
1608
|
+
}
|
|
1609
|
+
if (!bcCritical) {
|
|
1610
|
+
throw _err(code, "the " + which + " marks basicConstraints non-critical, so a relying party that skips unrecognized extensions would not see the cA bit it must act on (RFC 5280 sec. 4.2.1.9)");
|
|
1611
|
+
}
|
|
1612
|
+
if (ku && ku.keyCertSign !== true) {
|
|
1613
|
+
throw _err(code, "the " + which + " carries a keyUsage that withholds keyCertSign, so it cannot sign certificates as a CA must (RFC 5280 sec. 6.1.4)");
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// Names are compared through guard-name's RFC 5280 sec. 7.1 canonical rule, the one place a
|
|
1618
|
+
// distinguished-name identity is decided here: two spellings of the same name are the same
|
|
1619
|
+
// authority, and a byte compare would refuse a re-encoded but equal one.
|
|
1620
|
+
function _assertSameName(a, b, which, must) {
|
|
1621
|
+
if (!guard.name.dnEqual(a.rdns, b.rdns, _err, "cmp/bad-info-value", "a rootCaKeyUpdate name")) {
|
|
1622
|
+
throw _err("cmp/bad-info-value", "the rootCaKeyUpdate " + which + " must " + must + " (RFC 9483 sec. 4.3.2); got " + JSON.stringify(a.dn));
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
async function _assertSignedBy(cert, signer, which, whose) {
|
|
1626
|
+
if (!_engine || !_engine.verifyWithSpki) {
|
|
1627
|
+
throw _err("cmp/bad-info-value", "the signature engine is unavailable, so a rootCaKeyUpdate cannot be checked; require pki.path to install it");
|
|
1628
|
+
}
|
|
1629
|
+
// The signature is a BIT STRING and a valid one is octet-aligned (no unused bits). Passing only its
|
|
1630
|
+
// octets to the engine would drop the unusedBits metadata, so a signature declaring unused bits would
|
|
1631
|
+
// verify on its octets while being a malformed shape the path verifier refuses (path-validate) before
|
|
1632
|
+
// the engine. Hold a rollover signature to the same rule.
|
|
1633
|
+
if (!guard.crypto.isOctetAligned(cert.signatureValue)) {
|
|
1634
|
+
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)");
|
|
1635
|
+
}
|
|
1636
|
+
var ok = await _engine.verifyWithSpki(cert.signatureAlgorithm, cert.signatureValue.bytes, signer.subjectPublicKeyInfo.bytes, cert.tbsBytes);
|
|
1637
|
+
if (ok !== true) {
|
|
1638
|
+
throw _err("cmp/bad-info-value", "the rootCaKeyUpdate " + which + " is not signed by " + whose + ", so it cannot carry the trust transition it exists for (RFC 9483 sec. 4.3.2)");
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// A certificate this verb has to put BACK on the wire, so it takes the two forms that carry their
|
|
1643
|
+
// own bytes: DER, and PEM which decodes to them. An already-parsed certificate is refused rather
|
|
1644
|
+
// than re-encoded -- the parsed form retains no source DER, and rebuilding one would emit bytes
|
|
1645
|
+
// that are not the ones the issuer signed. revoke() takes the parsed form because it reads an
|
|
1646
|
+
// issuer and a serial out of it and never re-emits the certificate itself.
|
|
1647
|
+
function _certificateArgument(value, what) {
|
|
1648
|
+
var der;
|
|
1649
|
+
if (typeof value === "string") {
|
|
1650
|
+
try { der = x509.pemDecode(value); }
|
|
1651
|
+
catch (e) { throw _err("cmp/bad-input", what + " is a string but not a PEM certificate", e); }
|
|
1652
|
+
} else if (value != null && Buffer.isBuffer(value.tbsBytes)) {
|
|
1653
|
+
throw _err("cmp/bad-input", what + " must carry the certificate's own bytes (DER or PEM): a parsed certificate keeps no source DER, and this request has to send the bytes the issuer signed");
|
|
1654
|
+
} else {
|
|
1655
|
+
der = guard.bytes.snapshot(value, CmpError, "cmp/bad-input", what);
|
|
1656
|
+
}
|
|
1657
|
+
return _asCertificate(der, what, "cmp/bad-input");
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// `code` names the boundary: a response value is malformed protocol (cmp/bad-info-value), while a
|
|
1661
|
+
// value the CALLER supplied for a request is an authoring error (cmp/bad-input), and the two must
|
|
1662
|
+
// not read alike to whoever catches them.
|
|
1663
|
+
function _asCertificate(der, what, code) {
|
|
1664
|
+
try { x509.parse(der); }
|
|
1665
|
+
catch (e) { throw _err(code || "cmp/bad-info-value", what + " is not a valid X.509 certificate (RFC 9483 sec. 4.3)", e); }
|
|
1666
|
+
return Buffer.from(der);
|
|
1667
|
+
}
|
|
1668
|
+
function _asCrl(der) {
|
|
1669
|
+
try { schemaCrl.parse(der); }
|
|
1670
|
+
catch (e) { throw _err("cmp/bad-info-value", "a crlUpdate entry is not a valid CertificateList (RFC 9483 sec. 4.3.4)", e); }
|
|
1671
|
+
return Buffer.from(der);
|
|
1672
|
+
}
|
|
1673
|
+
// sec. 4.3.4 does not say "a CRL", it says the latest one FROM THE REFERENCED SOURCE, and only
|
|
1674
|
+
// when it is more recent than a supplied thisUpdate; in every other case the value is absent.
|
|
1675
|
+
// A CRL that answers some other query is not the answer to this one, and reporting it as one
|
|
1676
|
+
// hands the caller a list whose scope they did not ask about.
|
|
1677
|
+
//
|
|
1678
|
+
// The issuer arm is bound by name. The dpn arm is bound where a CRL says which point it speaks
|
|
1679
|
+
// for: RFC 5280 sec. 5.2.5 puts that in the issuingDistributionPoint extension, and sec. 4.3.4's
|
|
1680
|
+
// own note points there for where a distribution point name comes from. A CRL carrying no such
|
|
1681
|
+
// scope is claiming to be its issuer's complete list, which sec. 5.2.5 permits and which answers
|
|
1682
|
+
// a request for any point of that issuer, so there is nothing to refuse -- and nothing to bind,
|
|
1683
|
+
// since the request named no issuer. The freshness rule applies to both arms.
|
|
1684
|
+
function _bindCrlToQuery(der) {
|
|
1685
|
+
var parsed;
|
|
1686
|
+
try { parsed = schemaCrl.parse(der); }
|
|
1687
|
+
catch (e) { throw _err("cmp/bad-info-value", "a crlUpdate entry is not a valid CertificateList (RFC 9483 sec. 4.3.4)", e); }
|
|
1688
|
+
if (crlQuery && crlQuery.issuerName != null) {
|
|
1689
|
+
var want = schemaCmp.readName(crlQuery.issuerName);
|
|
1690
|
+
if (!guard.name.dnEqual(parsed.issuer.rdns, want.rdns, _err, "cmp/bad-info-value", "the crlUpdate issuer")) {
|
|
1691
|
+
throw _err("cmp/bad-info-value", "the returned CRL was issued by " + JSON.stringify(parsed.issuer.dn) + ", not by the source this request named (RFC 9483 sec. 4.3.4)");
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
if (crlQuery && crlQuery.dpn != null) {
|
|
1695
|
+
var idpDpn = null, idpCritical = false, crlExts = parsed.crlExtensions || [];
|
|
1696
|
+
for (var xi = 0; xi < crlExts.length; xi++) {
|
|
1697
|
+
if (crlExts[xi].oid !== OID_IDP) continue;
|
|
1698
|
+
idpDpn = cmpBuild.decodeIdpDistributionPoint(crlExts[xi].value);
|
|
1699
|
+
idpCritical = crlExts[xi].critical === true;
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
// The comparison is the one the path validator applies to a shard CRL, so a CRL either speaks
|
|
1703
|
+
// for the point this request named or it does not, and the answer does not depend on which verb
|
|
1704
|
+
// is asking. Only a CRITICAL issuingDistributionPoint establishes that scope (sec. 5.2.5): a
|
|
1705
|
+
// relying party may ignore a non-critical one, so a scope it could ignore cannot bind the answer
|
|
1706
|
+
// to the named point -- the path validator makes the same fail-closed decision for a shard CRL.
|
|
1707
|
+
// A CRL naming no point is a complete list and is left to the freshness rule.
|
|
1708
|
+
if (idpDpn !== null && (!idpCritical || !guard.name.dpnCorresponds(crlQuery.dpn, idpDpn, _err, "cmp/bad-info-value", "the crlUpdate distribution point"))) {
|
|
1709
|
+
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)");
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
if (crlQuery && crlQuery.thisUpdate != null) {
|
|
1713
|
+
var asked = crlQuery.thisUpdate; // already an instant, reduced when the request was built
|
|
1714
|
+
// The CRL's own thisUpdate goes through the same door: it arrived from a responder, so it is
|
|
1715
|
+
// the side more likely to be missing or unusable, and reading its epoch value directly would
|
|
1716
|
+
// compare NaN and quietly answer false.
|
|
1717
|
+
var got = guard.time.instantOf(parsed.thisUpdate, _err, "cmp/bad-info-value", "the returned CRL's thisUpdate");
|
|
1718
|
+
if (got <= asked) {
|
|
1719
|
+
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");
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
return Buffer.from(der);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
var OID_REG_CTRL_ALG_ID = oid.byName("algId");
|
|
1726
|
+
var OID_REG_CTRL_RSA_KEY_LEN = oid.byName("rsaKeyLen");
|
|
1727
|
+
// True for an RSA algorithm, which sec. 4.3.3 excludes from a keySpec algId (RSA uses rsaKeyLen).
|
|
1728
|
+
// Membership is decided by OID family: any OID under the PKCS#1 arc, plus the two RSA identifiers
|
|
1729
|
+
// that sit off it. A standardized RSA OID the registry has not named (sha1WithRSAEncryption, the
|
|
1730
|
+
// md*/sha224 variants) is still under the arc, so it is caught without being listed. An OID that is
|
|
1731
|
+
// not an encodable dotted string, or one outside every RSA arc, is NOT RSA here -- it is surfaced.
|
|
1732
|
+
function _isRsaAlgorithm(dotted) {
|
|
1733
|
+
if (RSA_ARC_EXCLUDE[dotted] === 1) return false; // mgf1 / pSpecified: auxiliary OIDs on the PKCS#1 arc, not RSA algorithms
|
|
1734
|
+
var arcs = oid.toArcs(dotted); // dotted is a decoded OID string (readAlgorithmIdentifier), always encodable
|
|
1735
|
+
var underAny = false;
|
|
1736
|
+
for (var a = 0; !underAny && a < RSA_ARCS.length; a++) {
|
|
1737
|
+
var arc = RSA_ARCS[a];
|
|
1738
|
+
if (arcs.length > arc.length) { // the arc node itself is not an algorithm
|
|
1739
|
+
underAny = true;
|
|
1740
|
+
for (var i = 0; underAny && i < arc.length; i++) { underAny = arcs[i] === arc[i]; }
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
return underAny || RSA_OFF_ARC[dotted] === 1;
|
|
1744
|
+
}
|
|
1745
|
+
// sec. 4.3.3: a keySpec element carries "attribute id-regCtrl-algId or id-regCtrl-rsaKeyLen", the
|
|
1746
|
+
// first "MUST be of type AlgorithmIdentifier and give an algorithm other than RSA", the second
|
|
1747
|
+
// "MUST be a positive integer value". A control outside the pair states a requirement this
|
|
1748
|
+
// operation has no meaning for, so it is refused rather than surfaced as an understood one.
|
|
1749
|
+
function _checkKeySpec(ctrl) {
|
|
1750
|
+
if (ctrl.type === OID_REG_CTRL_RSA_KEY_LEN) {
|
|
1751
|
+
var len;
|
|
1752
|
+
try { len = asn1.read.integer(asn1.decode(ctrl.value)); }
|
|
1753
|
+
catch (e) { throw _err("cmp/bad-info-value", "a keySpec rsaKeyLen must be an INTEGER (RFC 9483 sec. 4.3.3)", e); }
|
|
1754
|
+
if (len <= 0n) throw _err("cmp/bad-info-value", "a keySpec rsaKeyLen must be a positive integer (RFC 9483 sec. 4.3.3); got " + len);
|
|
1755
|
+
ctrl.rsaKeyLen = len;
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
if (ctrl.type === OID_REG_CTRL_ALG_ID) {
|
|
1759
|
+
// Read through the shared AlgorithmIdentifier sub-schema rather than probing the node, so the
|
|
1760
|
+
// structure is held to its whole definition: a SEQUENCE of an OID and at most one parameters
|
|
1761
|
+
// field. A hand-rolled tag test says only that the first child is an OID, which a SEQUENCE of
|
|
1762
|
+
// three fields also satisfies -- and that value would then reach the caller as an algorithm
|
|
1763
|
+
// requirement no conforming responder can have meant.
|
|
1764
|
+
var alg;
|
|
1765
|
+
try { alg = schemaCmp.readAlgorithmIdentifier(ctrl.value); }
|
|
1766
|
+
catch (e) {
|
|
1767
|
+
if (e && e.isCmpError) throw e;
|
|
1768
|
+
throw _err("cmp/bad-info-value", "a keySpec algId must be an AlgorithmIdentifier (RFC 9483 sec. 4.3.3)", e);
|
|
1769
|
+
}
|
|
1770
|
+
// sec. 4.3.3 / RFC 9480 sec. 2.16 hold the algId to ONE rule: not RSA. A known RSA algorithm is
|
|
1771
|
+
// refused (its requirement is stated with rsaKeyLen); every other value -- a known non-RSA
|
|
1772
|
+
// algorithm OR an OID the registry does not name -- is surfaced. The responder offers one control
|
|
1773
|
+
// per algorithm it supports and the entity picks one, so an unrecognized offer must not fail the
|
|
1774
|
+
// whole exchange.
|
|
1775
|
+
if (_isRsaAlgorithm(alg.oid)) {
|
|
1776
|
+
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));
|
|
1777
|
+
}
|
|
1778
|
+
ctrl.algorithm = alg.oid;
|
|
1779
|
+
ctrl.algorithmName = alg.name; // null for an OID the registry does not name -- surfaced as-is for the caller to weigh
|
|
1780
|
+
ctrl.algorithmParameters = alg.parameters;
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
throw _err("cmp/bad-info-value", "a keySpec control must be id-regCtrl-algId or id-regCtrl-rsaKeyLen (RFC 9483 sec. 4.3.3); got " + ctrl.type);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1046
1786
|
return {
|
|
1047
1787
|
enroll: enroll,
|
|
1788
|
+
revoke: revoke,
|
|
1789
|
+
info: info,
|
|
1048
1790
|
get transactionID() { return Buffer.from(transactionID); }, // a copy: a caller mutating it must not desync the transaction
|
|
1049
1791
|
get transcript() { return _transcriptSnapshot(); }, // a defensive snapshot; never the mutable internal array
|
|
1050
1792
|
};
|