@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.
@@ -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
- if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cmp/bad-input", "opts must be an object");
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 cert-response arm the in-flight enrollment must be answered by (RESPONSE_ARM)
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 { state: "rejected", status: body.decoded && body.decoded.pKIStatusInfo };
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 an enrollment transaction" };
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
- // One transaction per session (RFC 9810: one transactionID per transaction). A second or concurrent
975
- // enroll would reuse this transaction's transactionID / nonce chain / certReqId -- a replay or a corrupted
976
- // interleave -- so it is refused. The request-shape checks run BEFORE the transaction is marked in-flight,
977
- // so a malformed-request call does not consume the session and the caller may retry with a valid request.
978
- if (completed || inFlight) throw _err("cmp/bad-input", "this pki.cmp.session transaction is already " + (completed ? "completed" : "in flight") + "; create a new session per enrollment (RFC 9810 sec. 5.1.1: one transactionID per transaction)");
979
- if (!request || typeof request !== "object" || Buffer.isBuffer(request)) throw _err("cmp/bad-input", "enroll(request) requires a body spec object { ir | cr | kur | p10cr }");
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
  };