@blamejs/pki 0.5.39 → 0.6.1

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/lib/ct.js CHANGED
@@ -154,7 +154,7 @@ function _parseSct(r, sctLen) {
154
154
  * @primitive pki.ct.parseSctList
155
155
  * @signature pki.ct.parseSctList(extValue) -> { scts, unknownScts, all }
156
156
  * @since 0.1.20
157
- * @status experimental
157
+ * @status stable
158
158
  * @spec RFC 6962, RFC 5246, RFC 8446
159
159
  * @related pki.ct.reconstructSignedData, pki.ct.encodeSctList, pki.schema.x509.parse
160
160
  *
@@ -252,7 +252,7 @@ function _u24Bytes(n) {
252
252
  * @primitive pki.ct.reconstructSignedData
253
253
  * @signature pki.ct.reconstructSignedData(entry, sct) -> Buffer
254
254
  * @since 0.1.20
255
- * @status experimental
255
+ * @status stable
256
256
  * @spec RFC 6962
257
257
  * @related pki.ct.parseSctList
258
258
  *
@@ -378,7 +378,7 @@ function _spkiAlg(spki) {
378
378
  * @primitive pki.ct.verifySct
379
379
  * @signature pki.ct.verifySct(entry, sct, logPublicKey) -> Promise<boolean>
380
380
  * @since 0.2.12
381
- * @status experimental
381
+ * @status stable
382
382
  * @spec RFC 6962
383
383
  * @defends sct-signature-forgery (CWE-347)
384
384
  * @related pki.ct.parseSctList, pki.ct.reconstructSignedData
@@ -498,7 +498,7 @@ function _encodeSctBody(sct) {
498
498
  * @primitive pki.ct.encodeSctList
499
499
  * @signature pki.ct.encodeSctList(scts) -> Buffer
500
500
  * @since 0.2.24
501
- * @status experimental
501
+ * @status stable
502
502
  * @spec RFC 6962, RFC 5246
503
503
  * @related pki.ct.parseSctList, pki.ct.signSct
504
504
  *
@@ -573,7 +573,7 @@ function _logKeyMaterial(logKey) {
573
573
  * @primitive pki.ct.signSct
574
574
  * @signature pki.ct.signSct(entry, logKey, opts?) -> Promise<sct>
575
575
  * @since 0.2.24
576
- * @status experimental
576
+ * @status stable
577
577
  * @spec RFC 6962
578
578
  * @related pki.ct.verifySct, pki.ct.reconstructSignedData, pki.ct.encodeSctList
579
579
  *
@@ -723,7 +723,7 @@ function _logsAgree(a, b) {
723
723
  * @primitive pki.ct.parseLogList
724
724
  * @signature pki.ct.parseLogList(json, opts?) -> { logs, byLogId, version, timestamp }
725
725
  * @since 0.2.28
726
- * @status experimental
726
+ * @status stable
727
727
  * @spec RFC 6962
728
728
  * @related pki.ct.verifySctWithLogList, pki.ct.verifySct
729
729
  *
@@ -815,7 +815,7 @@ function _resolveNotAfter(entry, opts) {
815
815
  * @primitive pki.ct.verifySctWithLogList
816
816
  * @signature pki.ct.verifySctWithLogList(entry, sct, logList, opts?) -> Promise<boolean>
817
817
  * @since 0.2.28
818
- * @status experimental
818
+ * @status stable
819
819
  * @spec RFC 6962
820
820
  * @related pki.ct.parseLogList, pki.ct.verifySct
821
821
  *
@@ -848,37 +848,292 @@ function _resolveNotAfter(entry, opts) {
848
848
  * state: { usable: { timestamp: "2026-01-01T00:00:00Z" } } }] }] }), "utf8"));
849
849
  * var ok = await pki.ct.verifySctWithLogList(sctEntry, embeddedSct, logList);
850
850
  */
851
- async function verifySctWithLogList(entry, sct, logList, opts) {
852
- opts = opts || {};
853
- if (logList == null || typeof logList !== "object" || logList.byLogId == null) throw _ctErr("ct/bad-input", "logList must be a pki.ct.parseLogList result");
854
- if (sct == null || typeof sct !== "object" || typeof sct.logIdHex !== "string") throw _ctErr("ct/bad-input", "the SCT is missing its logIdHex");
851
+ // The pure, NON-THROWING log resolution + state + temporal classifier shared by verifySctWithLogList
852
+ // (which throws the mapped code on a non-ok status) and verifySctList (which records the code into a
853
+ // result row and continues). Returns { status, log?, code?, message? } where status is one of
854
+ // ok | bad-input | not-found | untrusted | temporal. A mis-shaped sct.timestamp (in the retired-log
855
+ // gate) or a malformed opts.certNotAfter (in the temporal gate, via within) still THROWS -- those are
856
+ // caller mis-shapes, not per-SCT trust outcomes (the three-tier rule: bad input throws, a per-SCT
857
+ // verdict is recorded). The state and temporal gate semantics are verbatim from the original
858
+ // verifySctWithLogList body, so its shipped RED vectors are the behavior guard for this refactor.
859
+ function _classifyLog(sct, logList, entry, opts) {
860
+ if (logList == null || typeof logList !== "object" || logList.byLogId == null) return { status: "bad-input", code: "ct/bad-input", message: "logList must be a pki.ct.parseLogList result" };
861
+ if (sct == null || typeof sct !== "object" || typeof sct.logIdHex !== "string") return { status: "bad-input", code: "ct/bad-input", message: "the SCT is missing its logIdHex" };
855
862
  var log = logList.byLogId[sct.logIdHex];
856
- if (log == null) throw _ctErr("ct/log-not-found", "no trusted CT log matches the SCT's logId " + sct.logIdHex);
863
+ if (log == null) return { status: "not-found", code: "ct/log-not-found", message: "no trusted CT log matches the SCT's logId " + sct.logIdHex };
857
864
  // State gate (fail-closed): trusted proceed; retired only before its retirement; else untrusted.
858
865
  if (!log.state.trusted) {
859
- if (!log.state.conditional) throw _ctErr("ct/log-untrusted", "the CT log state '" + log.state.name + "' is not trusted");
866
+ if (!log.state.conditional) return { status: "untrusted", log: log, code: "ct/log-untrusted", message: "the CT log state '" + log.state.name + "' is not trusted" };
860
867
  var ts = guard.range.uint64(sct.timestamp, _ctErr, "ct/bad-input", "sct.timestamp");
861
- if (ts >= BigInt(guard.time.instantOf(log.state.since))) throw _ctErr("ct/log-untrusted", "the CT log is retired and the SCT is not timestamped before its retirement (" + log.state.since.toISOString() + ")");
868
+ if (ts >= BigInt(guard.time.instantOf(log.state.since))) return { status: "untrusted", log: log, code: "ct/log-untrusted", message: "the CT log is retired and the SCT is not timestamped before its retirement (" + log.state.since.toISOString() + ")" };
862
869
  }
863
870
  // Temporal gate (fail-closed): the covered cert's notAfter must be in [start_inclusive, end_exclusive).
864
871
  if (log.temporalInterval) {
865
872
  var notAfter = _resolveNotAfter(entry, opts);
866
- // within() validates notAfter (and the interval bounds) fail-closed before comparing: an Invalid
867
- // Date is instanceof Date yet NaN, and NaN < x / NaN >= x are both false, so an unvalidated notAfter
868
- // would silently BYPASS the [startInclusive, endExclusive) containment. A malformed notAfter throws
869
- // inside within (pass a valid opts.certNotAfter); an out-of-window notAfter returns false here.
873
+ // An unresolvable notAfter is a failure for this windowed SCT, never a silent skip. within() then
874
+ // validates notAfter fail-closed before comparing: an Invalid Date is instanceof Date yet NaN, and
875
+ // NaN < x / NaN >= x are both false, so an unvalidated notAfter would BYPASS the containment. A
876
+ // malformed opts.certNotAfter throws inside within (a caller mis-shape); an out-of-window one is
877
+ // this recorded temporal status.
878
+ if (notAfter == null) return { status: "temporal", log: log, code: "ct/temporal-interval", message: "the windowed CT log has no resolvable covered-certificate notAfter (pass a valid opts.certNotAfter)" };
870
879
  if (!guard.time.within(notAfter, log.temporalInterval.startInclusive, log.temporalInterval.endExclusive, _ctErr, "ct/temporal-interval", "the covered certificate notAfter (pass a valid opts.certNotAfter)")) {
871
- throw _ctErr("ct/temporal-interval", "the covered certificate's notAfter is outside the CT log's temporal_interval");
880
+ return { status: "temporal", log: log, code: "ct/temporal-interval", message: "the covered certificate's notAfter is outside the CT log's temporal_interval" };
872
881
  }
873
882
  }
874
- return verifySct(entry, sct, log.key); // the shipped crypto verdict (re-checks the logId binding)
883
+ return { status: "ok", log: log };
884
+ }
885
+
886
+ async function verifySctWithLogList(entry, sct, logList, opts) {
887
+ opts = opts || {};
888
+ var c = _classifyLog(sct, logList, entry, opts);
889
+ if (c.status !== "ok") throw _ctErr(c.code, c.message);
890
+ return verifySct(entry, sct, c.log.key); // the shipped crypto verdict (re-checks the logId binding)
891
+ }
892
+
893
+ // Options verifySctList reads. All optional; the defaults are the RFC 6962 sec. 3.3 floor (>= 1).
894
+ var _VERIFY_SCT_LIST_OPTS = { minScts: 1, minOperators: 1, certNotAfter: 1, at: 1 };
895
+
896
+ // A caller-supplied policy threshold: a positive integer, or null when omitted (defaulted by the caller).
897
+ function _policyCount(v, name) {
898
+ if (v == null) return null;
899
+ if (typeof v !== "number" || !intrinsic.isInteger(v) || v < 1) throw _ctErr("ct/bad-input", name + " must be a positive integer");
900
+ return v;
901
+ }
902
+
903
+ /**
904
+ * @primitive pki.ct.verifySctList
905
+ * @signature pki.ct.verifySctList(entry, list, logList, opts?) -> Promise<verdict>
906
+ * @since 0.6.1
907
+ * @status stable
908
+ * @spec RFC 6962
909
+ * @related pki.ct.verifySct, pki.ct.verifySctWithLogList, pki.ct.parseLogList, pki.ct.parseSctList
910
+ *
911
+ * Render a certificate-level Certificate Transparency verdict over the SET of SCTs a certificate carries
912
+ * (RFC 6962 sec. 3.3), rather than one SCT at a time. Each SCT is resolved to a trusted log through
913
+ * `logList` and verified with the shipped `verifySct`; the verdict reports how many distinct trusted
914
+ * logs verified an SCT (`validScts`, so a duplicated SCT cannot inflate the count), from how many
915
+ * distinct trusted operators, and whether that meets the caller's CT policy. The RFC floor is one valid
916
+ * SCT (sec. 3.3), so `minScts` and `minOperators` both default to 1; the distinct-operator axis is a
917
+ * browser CT policy layered on top of the RFC, so the caller sets the threshold and the verdict surfaces
918
+ * the counts rather than hardcoding a browser's numbers.
919
+ *
920
+ * `entry` is the shared log-entry context for every SCT (`{ entryType: 0, leafCert }` for SCTs delivered
921
+ * over TLS/OCSP, or `{ entryType: 1, tbsCertificate, issuerKeyHash }` for embedded SCTs; the shape
922
+ * `pki.ct.x509CertEntry` produces). `list` is a `parseSctList` result, its `.scts` array, or an array of
923
+ * decoded v1 SCTs; unknown-version entries are counted `unknownScts` and never as valid. `logList` is a
924
+ * `parseLogList` result. A policy shortfall is a verdict (`policyOk: false`), not a throw; a per-SCT
925
+ * trust or crypto failure is recorded into its result row (`valid: false`, with a `code` for a
926
+ * structural or trust exclusion, without one for a cryptographic mismatch) and the loop continues; only
927
+ * a mis-shaped `entry` / `list` / `logList` / `opts` throws a typed `CtError`.
928
+ *
929
+ * @opts
930
+ * - `minScts` (int, default 1) -- required count of valid SCTs from trusted, in-window logs.
931
+ * - `minOperators` (int, default 1) -- required count of distinct trusted operators among them.
932
+ * - `certNotAfter` (Date) -- the covered certificate's notAfter for each windowed log's temporal gate;
933
+ * auto-derived from `entry.leafCert` for entryType 0, required for a precert entry against a windowed log.
934
+ * - `at` (Date) -- the validation time; when supplied, an SCT whose timestamp is later than it is
935
+ * rejected and not counted (RFC 6962 sec. 5.2, a client rejects a future-dated SCT). The
936
+ * pki.path.validate CT gate passes its opts.time here.
937
+ * @example
938
+ * // requires: logListJsonBytes -- the CT log-list JSON a distributor publishes (the log_list.json browsers consume)
939
+ * // requires: sctExtValue -- a certificate's SCT-list extension value (a signedCertificateTimestampList extension)
940
+ * // requires: certDer -- the DER of the certificate the SCTs cover
941
+ * var logList = pki.ct.parseLogList(logListJsonBytes);
942
+ * var list = pki.ct.parseSctList(sctExtValue);
943
+ * var verdict = await pki.ct.verifySctList({ entryType: 0, leafCert: certDer }, list, logList,
944
+ * { minScts: 2, minOperators: 2 });
945
+ * verdict.policyOk; // did the certificate meet the CT policy?
946
+ * verdict.operators; // the distinct trusted operators whose logs verified an SCT
947
+ */
948
+ async function verifySctList(entry, list, logList, opts) {
949
+ opts = guard.identifier.optionsObject(opts, _ctErr, "ct/bad-input", "pki.ct.verifySctList options");
950
+ guard.identifier.assertKnownKeys(opts, _VERIFY_SCT_LIST_OPTS, _ctErr, "ct/bad-input", "pki.ct.verifySctList has an unknown option: ");
951
+ var minScts = _policyCount(opts.minScts, "opts.minScts"); if (minScts == null) minScts = 1;
952
+ var minOperators = _policyCount(opts.minOperators, "opts.minOperators"); if (minOperators == null) minOperators = 1;
953
+ if (logList == null || typeof logList !== "object" || logList.byLogId == null) throw _ctErr("ct/bad-input", "logList must be a pki.ct.parseLogList result");
954
+ if (entry == null || (entry.entryType !== 0 && entry.entryType !== 1)) throw _ctErr("ct/bad-entry-type", "entryType must be x509_entry(0) or precert_entry(1) (RFC 6962 sec. 3.1)");
955
+ // The validation time, when supplied, arms the RFC 6962 sec. 5.2 rule that a future-dated SCT is
956
+ // rejected: a log cannot have issued an SCT after the moment we verify it, so its timestamp must not
957
+ // be later than `at`. Held as a millisecond Number and compared against the SCT's BigInt uint64
958
+ // timestamp (a BigInt-vs-Number relational comparison is exact).
959
+ var atMs = null;
960
+ if (opts.at != null) { guard.time.assertValid(opts.at, _ctErr, "ct/bad-input", "opts.at (the validation time)"); atMs = guard.time.instantOf(opts.at); }
961
+ // A present certNotAfter must be a valid Date: a non-Date is a caller mis-shape that throws, never a
962
+ // value silently ignored (entryType 0 would fall back to the certificate's own notAfter) or demoted to
963
+ // a per-SCT temporal failure (entryType 1) -- the malformed-options contract, matching opts.at.
964
+ if (opts.certNotAfter != null) guard.time.assertValid(opts.certNotAfter, _ctErr, "ct/bad-input", "opts.certNotAfter (the covered certificate's notAfter)");
965
+ // Validate the shared entry's required fields up front: a mis-shaped entry is a caller error that
966
+ // throws (not a per-SCT row or a verdict rendered without examining it), even when the SCT list is
967
+ // empty. The same fields reconstructSignedData reads for the preimage.
968
+ if (entry.entryType === 0) { _toBuffer(entry.leafCert, "entry.leafCert"); }
969
+ else {
970
+ if (_toBuffer(entry.issuerKeyHash, "entry.issuerKeyHash").length !== 32) throw _ctErr("ct/bad-issuer-key-hash", "entry.issuerKeyHash must be exactly 32 bytes (SHA-256 of the issuer SPKI, RFC 6962 sec. 3.2)");
971
+ _toBuffer(entry.tbsCertificate, "entry.tbsCertificate");
972
+ }
973
+ // Normalize the list into the known v1 SCTs plus the count of forward-compat unknown-version entries
974
+ // (M7 -- their body layout is undefined, so they are unverifiable, never a valid SCT, never a throw).
975
+ var scts, unknownScts;
976
+ if (list != null && intrinsic.isArray(list.scts)) { scts = list.scts; unknownScts = intrinsic.isArray(list.unknownScts) ? list.unknownScts.length : 0; }
977
+ else if (intrinsic.isArray(list)) { scts = list; unknownScts = 0; }
978
+ else throw _ctErr("ct/bad-input", "list must be a pki.ct.parseSctList result, its .scts array, or an array of decoded v1 SCTs");
979
+ // Bound the per-SCT verification work a caller-supplied array can demand to the same cap parseSctList
980
+ // enforces on wire input, so a bare oversized array cannot force an unbounded run of signature checks.
981
+ if (scts.length > C.LIMITS.SCT_MAX_COUNT) throw _ctErr("ct/too-many-scts", "the SCT list has " + scts.length + " entries, exceeding the SCT_MAX_COUNT cap of " + C.LIMITS.SCT_MAX_COUNT + " (RFC 6962 sec. 3.3)");
982
+
983
+ var results = [], validScts = 0, operators = [], seenOps = intrinsic.create(null), seenLogs = intrinsic.create(null);
984
+ for (var i = 0; i < scts.length; i++) {
985
+ var sct = scts[i];
986
+ var row = { logIdHex: (sct && typeof sct.logIdHex === "string") ? sct.logIdHex : null, valid: false,
987
+ operator: null, logState: null,
988
+ timestamp: (sct && typeof sct.timestamp === "bigint") ? sct.timestamp : null, code: undefined, reason: undefined };
989
+ // Classification can THROW for a malformed per-SCT field (a bad timestamp on a retired-log match):
990
+ // that is a recorded per-SCT outcome, never a thrown aggregate abort, so it matches the usable-log
991
+ // malformed-SCT path below and one bad SCT cannot sink the verdict or a valid sibling.
992
+ var c;
993
+ try { c = _classifyLog(sct, logList, entry, opts); }
994
+ catch (e) {
995
+ if (!(e && e.isPkiError)) throw e;
996
+ row.code = e.code; row.reason = e.message; intrinsic.push(results, row); continue;
997
+ }
998
+ if (c.log) { row.operator = c.log.operator; row.logState = c.log.state.name; }
999
+ if (c.status !== "ok") { row.code = c.code; row.reason = c.message; intrinsic.push(results, row); continue; }
1000
+ // A trust exclusion is a recorded code above; the crypto check below is false-is-a-verdict. But a
1001
+ // structurally-defective SCT from a trusted log (a malformed signature encoding, an unsupported hash
1002
+ // or curve) makes verifySct THROW a typed CtError -- for the aggregate that is a recorded per-SCT
1003
+ // outcome (M10, the verdict-collector tier), never a thrown call that would sink the verdict or a
1004
+ // valid sibling. A non-PkiError (an unexpected bug) is re-thrown, never swallowed into a verdict.
1005
+ var ok;
1006
+ try { ok = await verifySct(entry, sct, c.log.key); }
1007
+ catch (e) {
1008
+ if (!(e && e.isPkiError)) throw e;
1009
+ row.code = e.code; row.reason = e.message; intrinsic.push(results, row); continue;
1010
+ }
1011
+ // RFC 6962 sec. 5.2: reject a future-dated SCT even when its signature verifies -- a log cannot have
1012
+ // issued it after the validation time, so it is recorded as a rejection and never counted. An SCT
1013
+ // dated after the certificate's OWN notAfter is deliberately NOT rejected: RFC 6962 sec. 5.1 permits
1014
+ // a log to accept an already-expired certificate, so such an SCT is legitimate.
1015
+ if (ok === true && atMs != null && sct.timestamp > atMs) {
1016
+ row.code = "ct/future-timestamp"; row.reason = "the SCT timestamp is later than the validation time (RFC 6962 sec. 5.2)";
1017
+ intrinsic.push(results, row); continue;
1018
+ }
1019
+ row.valid = ok === true;
1020
+ // Count each distinct trusted log once: a duplicated SCT, or a second SCT from the same log, is one
1021
+ // attestation, so a repeat cannot inflate validScts past a minScts policy (the count is over the set
1022
+ // of logs, not raw SCT copies). The duplicate row is still recorded for diagnostics.
1023
+ if (row.valid && !seenLogs[sct.logIdHex]) {
1024
+ seenLogs[sct.logIdHex] = true;
1025
+ validScts++;
1026
+ if (c.log.operator != null && !seenOps[c.log.operator]) { seenOps[c.log.operator] = true; intrinsic.push(operators, c.log.operator); }
1027
+ }
1028
+ intrinsic.push(results, row);
1029
+ }
1030
+ var operatorCount = operators.length;
1031
+ var policyOk = validScts >= minScts && operatorCount >= minOperators;
1032
+ return {
1033
+ policyOk: policyOk, totalScts: scts.length + unknownScts, validScts: validScts, unknownScts: unknownScts,
1034
+ operatorCount: operatorCount, operators: operators, required: { minScts: minScts, minOperators: minOperators },
1035
+ results: results,
1036
+ reason: policyOk ? null : ("CT policy not met: " + validScts + " valid SCT(s) from " + operatorCount + " distinct operator(s); require at least " + minScts + " SCT(s) from at least " + minOperators + " operator(s)"),
1037
+ };
1038
+ }
1039
+
1040
+ // A cert / issuer argument as a parsed x509: a DER Buffer, a PEM string, or an unmodified
1041
+ // pki.schema.x509.parse result. Routed through guard.parsed (provenance-based, getter-free) so a
1042
+ // caller object cannot spoof "already parsed" through a lying accessor; a malformed one is a typed
1043
+ // ct/bad-cert-entry, never a silently-skipped input.
1044
+ function _asParsedCert(v, field) {
1045
+ // Inline require (circular load with schema-x509 -> schema-pkix), matching _resolveNotAfter above.
1046
+ var x509 = require("./schema-x509.js"); // allow:inline-require -- circular load (see _resolveNotAfter)
1047
+ return guard.parsed.acceptDerived(v, "certificate", x509.parse, _ctErr, "ct/bad-cert-entry", field);
1048
+ }
1049
+
1050
+ /**
1051
+ * @primitive pki.ct.x509CertEntry
1052
+ * @signature pki.ct.x509CertEntry(cert, issuer) -> { entryType, tbsCertificate, issuerKeyHash }
1053
+ * @since 0.6.1
1054
+ * @status stable
1055
+ * @spec RFC 6962
1056
+ * @related pki.ct.verifySctList, pki.ct.reconstructSignedData, pki.ct.verifySct
1057
+ *
1058
+ * Reconstruct the precertificate log entry from a FINAL certificate carrying embedded SCTs, so those
1059
+ * SCTs can be verified end to end (RFC 6962 sec. 3.2): "reconstruct this TBSCertificate from the final
1060
+ * certificate by extracting the TBSCertificate from it and deleting the SCT extension". Returns the
1061
+ * `{ entryType: 1, tbsCertificate, issuerKeyHash }` entry `pki.ct.verifySctList` / `reconstructSignedData`
1062
+ * consume, where `tbsCertificate` is the leaf TBS with ONLY the signedCertificateTimestampList extension
1063
+ * removed and `issuerKeyHash` is `SHA-256(issuer SubjectPublicKeyInfo DER)` (32 bytes). The signed
1064
+ * `tbs_certificate` an SCT covers is also without the CT poison extension (RFC 6962 sec. 3.2, "without
1065
+ * the signature and the poison extension"), which sits in the same position the SCT list takes in the
1066
+ * final certificate, so deleting the SCT-list extension IS the whole reconstruction: no poison extension
1067
+ * is re-added. (The Precertificate Signing Certificate case, where the precert issuer differs from the
1068
+ * final issuer, is out of scope; the common flow issues both from the one CA.)
1069
+ *
1070
+ * The removal is byte surgery on the CA-signed bytes, never a value re-serialization: the extensions
1071
+ * list and the two enclosing containers are rebuilt from every OTHER element's raw DER, so the only
1072
+ * bytes that change are the removed extension and the recomputed canonical DER length prefixes. `cert`
1073
+ * and `issuer` each accept a DER `Buffer`, a PEM string, or a `pki.schema.x509.parse` result. A
1074
+ * certificate with no extensions or no SCT-list extension is `ct/no-sct-extension`; an argument that is
1075
+ * not a certificate is `ct/bad-cert-entry`, and malformed certificate bytes carry the X.509 parse fault.
1076
+ *
1077
+ * @example
1078
+ * // requires: leafCertDer -- the DER of a final certificate carrying embedded SCTs
1079
+ * // requires: issuerCertDer -- the DER of the certificate that issued it
1080
+ * // requires: sctExtValue -- the leaf's signedCertificateTimestampList extension value
1081
+ * // requires: logList -- a pki.ct.parseLogList result
1082
+ * var entry = pki.ct.x509CertEntry(leafCertDer, issuerCertDer);
1083
+ * var verdict = await pki.ct.verifySctList(entry, pki.ct.parseSctList(sctExtValue), logList,
1084
+ * { certNotAfter: pki.schema.x509.parse(leafCertDer).validity.notAfter });
1085
+ */
1086
+ // The issuer contributes only its SubjectPublicKeyInfo (issuerKeyHash = SHA-256 of it), so it accepts
1087
+ // anything x509CertEntry's cert does PLUS a bare { subjectPublicKeyInfo: { bytes } } -- a trust anchor
1088
+ // carries its key without a full certificate, and requiring a whole cert there would refuse a leaf
1089
+ // validated directly under an anchor.
1090
+ function _issuerSpki(v, field) {
1091
+ if (v != null && typeof v === "object" && !intrinsic.isBuffer(v) && v.subjectPublicKeyInfo != null && v.subjectPublicKeyInfo.bytes != null) {
1092
+ // Validate the bare-form bytes as a BufferSource so a malformed value fails through the typed CT
1093
+ // error contract, not a native TypeError from the SHA-256 update downstream.
1094
+ return guard.bytes.view(v.subjectPublicKeyInfo.bytes, CtError, "ct/bad-cert-entry", field + " subjectPublicKeyInfo.bytes");
1095
+ }
1096
+ return _asParsedCert(v, field).subjectPublicKeyInfo.bytes;
1097
+ }
1098
+
1099
+ function x509CertEntry(cert, issuer) {
1100
+ var leaf = _asParsedCert(cert, "cert"), issSpki = _issuerSpki(issuer, "issuer");
1101
+ var tbs = asn1.decode(leaf.tbsBytes);
1102
+ // extensions [3] EXPLICIT is the last TBSCertificate field (context-constructed tag 3).
1103
+ var wrapIdx = -1;
1104
+ for (var i = 0; i < tbs.children.length; i++) {
1105
+ if (tbs.children[i].tagClass === "context" && tbs.children[i].tagNumber === 3) { wrapIdx = i; break; }
1106
+ }
1107
+ if (wrapIdx < 0) throw _ctErr("ct/no-sct-extension", "the certificate has no extensions, so it carries no embedded SCT list (RFC 6962 sec. 3.2)");
1108
+ var extsSeq = tbs.children[wrapIdx].children[0]; // the SEQUENCE OF Extension
1109
+ var sctOid = asn1.build.oid(oid.byName("signedCertificateTimestampList"));
1110
+ var kept = [], removed = 0;
1111
+ for (var j = 0; j < extsSeq.children.length; j++) {
1112
+ var ext = extsSeq.children[j];
1113
+ // ext.children[0] is the extnID OID node; compare its exact TLV bytes to the SCT-list OID.
1114
+ if (ext.children[0] && intrinsic.bufferEquals(ext.children[0].bytes, sctOid)) { removed++; continue; }
1115
+ intrinsic.push(kept, ext.bytes);
1116
+ }
1117
+ if (removed === 0) throw _ctErr("ct/no-sct-extension", "the certificate carries no signedCertificateTimestampList extension (RFC 6962 sec. 3.2)");
1118
+ // Rebuild the TBSCertificate SEQUENCE from raw child bytes; canonical DER lengths reproduce the
1119
+ // original framing minus the removed extension. When the SCT-list was the ONLY extension, `kept` is
1120
+ // empty and the extensions field is dropped ENTIRELY -- RFC 5280 Extensions is SIZE (1..MAX) and the
1121
+ // [3] field is OPTIONAL, so an empty [3] SEQUENCE {} would be invalid and would not match the signed
1122
+ // precert TBS, whose extensions field is likewise absent once its sole poison extension is removed.
1123
+ var tbsChildren = [];
1124
+ for (var k = 0; k < tbs.children.length; k++) {
1125
+ if (k !== wrapIdx) { intrinsic.push(tbsChildren, tbs.children[k].bytes); continue; }
1126
+ if (kept.length > 0) intrinsic.push(tbsChildren, asn1.build.explicit(3, asn1.build.sequence(kept)));
1127
+ }
1128
+ var issuerKeyHash = nodeCrypto.createHash("sha256").update(issSpki).digest();
1129
+ return { entryType: 1, tbsCertificate: asn1.build.sequence(tbsChildren), issuerKeyHash: issuerKeyHash };
875
1130
  }
876
1131
 
877
1132
  /**
878
1133
  * @primitive pki.ct.verifyLogListSignature
879
1134
  * @signature pki.ct.verifyLogListSignature(json, signature, publicKey) -> Promise<boolean>
880
1135
  * @since 0.2.29
881
- * @status experimental
1136
+ * @status stable
882
1137
  * @spec RFC 6962, RFC 8017
883
1138
  * @related pki.ct.parseLogList, pki.ct.verifySct
884
1139
  *
@@ -1019,7 +1274,7 @@ function _fetchBody(transport, url, req, label) {
1019
1274
  * @primitive pki.ct.fetchLogList
1020
1275
  * @signature pki.ct.fetchLogList(opts) -> Promise<{ logs, byLogId, version, timestamp, raw, status, contentType, tls }>
1021
1276
  * @since 0.3.21
1022
- * @status experimental
1277
+ * @status stable
1023
1278
  * @spec RFC 6962
1024
1279
  * @related pki.ct.parseLogList, pki.ct.verifyLogListSignature, pki.ct.verifySctWithLogList
1025
1280
  *
@@ -1113,6 +1368,8 @@ module.exports = {
1113
1368
  signSct: signSct,
1114
1369
  parseLogList: parseLogList,
1115
1370
  verifySctWithLogList: verifySctWithLogList,
1371
+ verifySctList: verifySctList,
1372
+ x509CertEntry: x509CertEntry,
1116
1373
  verifyLogListSignature: verifyLogListSignature,
1117
1374
  fetchLogList: fetchLogList,
1118
1375
  HASH_ALGORITHMS: HASH_ALGORITHMS,
package/lib/est.js CHANGED
@@ -162,7 +162,7 @@ function _knownOpts(opts, known, verb) {
162
162
  * @primitive pki.est.transferDecode
163
163
  * @signature pki.est.transferDecode(body) -> Buffer
164
164
  * @since 0.1.24
165
- * @status experimental
165
+ * @status stable
166
166
  * @spec RFC 8951, RFC 4648
167
167
  * @related pki.est.transferEncode
168
168
  *
@@ -200,7 +200,7 @@ function transferDecode(body) {
200
200
  * @primitive pki.est.transferEncode
201
201
  * @signature pki.est.transferEncode(der) -> string
202
202
  * @since 0.1.24
203
- * @status experimental
203
+ * @status stable
204
204
  * @spec RFC 8951, RFC 4648
205
205
  * @related pki.est.transferDecode
206
206
  *
@@ -389,7 +389,7 @@ function splitMultipartMixed(body, contentType) {
389
389
  * @primitive pki.est.parseCertsOnly
390
390
  * @signature pki.est.parseCertsOnly(der) -> { certificates, crls }
391
391
  * @since 0.1.24
392
- * @status experimental
392
+ * @status stable
393
393
  * @spec RFC 7030, RFC 5272, RFC 5652
394
394
  * @related pki.est.findIssuedCert, pki.schema.cms.parse
395
395
  *
@@ -695,7 +695,7 @@ var NOT_IMPLEMENTED_OPS = { fullcmc: 1 };
695
695
  * @primitive pki.est.classifyResponse
696
696
  * @signature pki.est.classifyResponse(status, headers, body, opts?) -> verdict
697
697
  * @since 0.1.24
698
- * @status experimental
698
+ * @status stable
699
699
  * @spec RFC 7030, RFC 8951
700
700
  * @related pki.est.paths, pki.est.parseCertsOnly
701
701
  *
@@ -796,7 +796,7 @@ function classifyResponse(status, headers, body, opts) {
796
796
  * @primitive pki.est.paths
797
797
  * @signature pki.est.paths(baseUrl, opts?) -> { cacerts, simpleenroll, ... }
798
798
  * @since 0.1.24
799
- * @status experimental
799
+ * @status stable
800
800
  * @spec RFC 7030
801
801
  * @related pki.est.classifyResponse
802
802
  *
@@ -1310,7 +1310,7 @@ function _certsResult(op, res, opts, csrSpki) {
1310
1310
  * @primitive pki.est.fullcmc
1311
1311
  * @signature pki.est.fullcmc(baseUrl, request, opts?) -> Promise<verdict | { retry, retryAfterSeconds }>
1312
1312
  * @since 0.4.16
1313
- * @status experimental
1313
+ * @status stable
1314
1314
  * @spec RFC 7030, RFC 8951, RFC 5273, RFC 5272
1315
1315
  * @related pki.cmc.build, pki.cmc.verify, pki.est.simpleenroll
1316
1316
  *
@@ -2024,7 +2024,7 @@ function _enroll(op, baseUrl, csrInput, opts) {
2024
2024
  * @primitive pki.est.cacerts
2025
2025
  * @signature pki.est.cacerts(baseUrl, opts?) -> Promise<{ certificates, crls } | { retry, retryAfterSeconds }>
2026
2026
  * @since 0.3.16
2027
- * @status experimental
2027
+ * @status stable
2028
2028
  * @spec RFC 7030, RFC 8951
2029
2029
  * @related pki.est.simpleenroll, pki.transport.https
2030
2030
  *
@@ -2068,7 +2068,7 @@ function cacerts(baseUrl, opts) {
2068
2068
  * @primitive pki.est.simpleenroll
2069
2069
  * @signature pki.est.simpleenroll(baseUrl, csr, opts?) -> Promise<{ certificate, chain, certificates } | { retry, retryAfterSeconds }>
2070
2070
  * @since 0.3.16
2071
- * @status experimental
2071
+ * @status stable
2072
2072
  * @spec RFC 7030, RFC 5272
2073
2073
  * @related pki.csr.sign, pki.est.simplereenroll, pki.est.cacerts
2074
2074
  *
@@ -2106,7 +2106,7 @@ function simpleenroll(baseUrl, csrInput, opts) {
2106
2106
  * @primitive pki.est.simplereenroll
2107
2107
  * @signature pki.est.simplereenroll(baseUrl, csr, opts?) -> Promise<{ certificate, chain, certificates } | { retry, retryAfterSeconds }>
2108
2108
  * @since 0.3.16
2109
- * @status experimental
2109
+ * @status stable
2110
2110
  * @spec RFC 7030
2111
2111
  * @related pki.est.simpleenroll, pki.est.reenrollGuard
2112
2112
  *
@@ -2285,7 +2285,7 @@ async function _serverkeygenResult(res, opts, derived) {
2285
2285
  * @primitive pki.est.serverkeygen
2286
2286
  * @signature pki.est.serverkeygen(baseUrl, csr, opts?) -> Promise<{ certificates, privateKey } | { certificates, encryptedKey } | { retry, retryAfterSeconds, retryAfterDate }>
2287
2287
  * @since 0.3.28
2288
- * @status experimental
2288
+ * @status stable
2289
2289
  * @spec RFC 7030, RFC 8951
2290
2290
  * @related pki.est.simpleenroll, pki.est.parseServerKeygenResponse
2291
2291
  *
@@ -2364,7 +2364,7 @@ function _csrattrsResult(res, opts) {
2364
2364
  * @primitive pki.est.csrattrs
2365
2365
  * @signature pki.est.csrattrs(baseUrl, opts?) -> Promise<{ available: true, attrs, plan } | { available: false, attrs: null }>
2366
2366
  * @since 0.3.28
2367
- * @status experimental
2367
+ * @status stable
2368
2368
  * @spec RFC 7030, RFC 8951, RFC 9908
2369
2369
  * @related pki.est.simpleenroll, pki.est.buildEnrollAttributes
2370
2370
  *
package/lib/hpke.js CHANGED
@@ -455,7 +455,7 @@ var suites = {
455
455
  * @primitive pki.hpke.setupS
456
456
  * @signature pki.hpke.setupS(suiteIds, recipientPublicKey, opts?) -> { enc, context }
457
457
  * @since 0.2.2
458
- * @status experimental
458
+ * @status stable
459
459
  * @spec RFC 9180
460
460
  * @related pki.hpke.setupR, pki.hpke.seal
461
461
  *
@@ -495,7 +495,7 @@ function _setupS(ids, pkR, opts) {
495
495
  * @primitive pki.hpke.setupR
496
496
  * @signature pki.hpke.setupR(suiteIds, enc, recipientPrivateKey, opts?) -> context
497
497
  * @since 0.2.2
498
- * @status experimental
498
+ * @status stable
499
499
  * @spec RFC 9180
500
500
  * @related pki.hpke.setupS, pki.hpke.open
501
501
  *
@@ -538,7 +538,7 @@ function _setupR(ids, enc, skR, opts) {
538
538
  * @primitive pki.hpke.seal
539
539
  * @signature pki.hpke.seal(suiteIds, recipientPublicKey, opts, aad, pt) -> { enc, ct }
540
540
  * @since 0.2.2
541
- * @status experimental
541
+ * @status stable
542
542
  * @spec RFC 9180
543
543
  * @related pki.hpke.open, pki.hpke.setupS
544
544
  *
@@ -574,7 +574,7 @@ function seal(ids, pkR, opts, aad, pt) {
574
574
  * @primitive pki.hpke.open
575
575
  * @signature pki.hpke.open(suiteIds, enc, recipientPrivateKey, opts, aad, ct) -> pt
576
576
  * @since 0.2.2
577
- * @status experimental
577
+ * @status stable
578
578
  * @spec RFC 9180
579
579
  * @related pki.hpke.seal, pki.hpke.setupR
580
580
  *
@@ -245,7 +245,7 @@ function _classifyError(e, C) {
245
245
  * @primitive pki.transport.https
246
246
  * @signature pki.transport.https(defaults?) -> transport
247
247
  * @since 0.3.16
248
- * @status experimental
248
+ * @status stable
249
249
  * @spec RFC 7030, RFC 8996
250
250
  * @defends tls-downgrade (CWE-757), server-impersonation (CWE-297), response-flooding (CWE-770), ssrf (CWE-918)
251
251
  * @related pki.est.cacerts, pki.est.simpleenroll
package/lib/lint.js CHANGED
@@ -714,7 +714,7 @@ function _applyThreshold(report, severity) {
714
714
  * @primitive pki.lint.certificate
715
715
  * @signature pki.lint.certificate(input, opts?) -> LintReport
716
716
  * @since 0.2.10
717
- * @status experimental
717
+ * @status stable
718
718
  * @spec RFC 5280, CA/Browser Forum TLS BR
719
719
  * @related pki.schema.x509.parse, pki.inspect.certificate, pki.path.validate
720
720
  *
@@ -772,7 +772,7 @@ function certificate(input, opts) {
772
772
  * @primitive pki.lint.rules
773
773
  * @signature pki.lint.rules(profile?) -> [{id, severity, source, citation}]
774
774
  * @since 0.2.10
775
- * @status experimental
775
+ * @status stable
776
776
  * @spec RFC 5280, CA/Browser Forum TLS BR
777
777
  *
778
778
  * Enumerate the rule registry (all rules, or one profile's). Each entry exposes its
@@ -790,7 +790,7 @@ function rules(profile) {
790
790
  * @primitive pki.lint.profiles
791
791
  * @signature pki.lint.profiles() -> [string]
792
792
  * @since 0.2.10
793
- * @status experimental
793
+ * @status stable
794
794
  * @spec RFC 5280, CA/Browser Forum TLS BR
795
795
  *
796
796
  * List the known lint-profile names.
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- the single signature seam for an inbound signed OCSPRequest (RFC 6960 sec. 4.1.1),
6
+ // composed by pki.ocsp.verifyRequest. It routes the request-signature check through the same
7
+ // certification-path signature engine (its EdDSA low-order-point and algorithm-confusion gates
8
+ // included) that pki.ocsp.verify uses for a response and pki.crl.verify / pki.cms.verify use for
9
+ // theirs, so no second, weaker verifier of an untrusted request can drift. The producing side runs
10
+ // its own check over what it just signed (pki.ocsp.buildRequest's signer path); this is the
11
+ // different question a responder asks about a request someone else produced. The slot, the wiring
12
+ // assertion and the octet-alignment refusal are the shared seam (lib/verify-seam.js). It is not
13
+ // wired into index.js and is reached only by require.
14
+
15
+ var frameworkError = require("./framework-error");
16
+ var seam = require("./verify-seam");
17
+
18
+ var _seam = seam.makeSeam("ocsp-request-verify", frameworkError.OcspError, "ocsp/bad-input");
19
+
20
+ // Verify a signed OCSPRequest's signature over its exact tbsRequest bytes under the SPKI of the
21
+ // requestor certificate the request carries (RFC 6960 sec. 4.1.1). Unlike a CSR there is no
22
+ // self-signature: the verifying key is the requestor's, identified to the responder by
23
+ // requestorName and proven by the certificate embedded in (or supplied alongside) the request.
24
+ function verifyRequestSignature(signatureAlgorithm, signature, signerSpkiBytes, tbsRequestBytes) {
25
+ return _seam.verify(signatureAlgorithm, signature, signerSpkiBytes, tbsRequestBytes);
26
+ }
27
+
28
+ module.exports = { setEngine: _seam.setEngine, verifyRequestSignature: verifyRequestSignature };