@blamejs/pki 0.5.7 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +405 -386
- package/MIGRATING.md +43 -0
- package/README.md +12 -12
- package/lib/acme.js +31 -31
- package/lib/asn1-der.js +10 -10
- package/lib/attrcert-sign.js +19 -19
- package/lib/byte-reader.js +6 -6
- package/lib/byte-writer.js +5 -5
- package/lib/cbor-det.js +27 -24
- package/lib/cmc-build.js +97 -92
- package/lib/cmc-verify.js +106 -30
- package/lib/cmp-build.js +25 -25
- package/lib/cmp-session.js +70 -70
- package/lib/cmp-verify.js +71 -57
- package/lib/cms-compress.js +7 -7
- package/lib/cms-decrypt.js +90 -72
- package/lib/cms-encrypt.js +32 -32
- package/lib/cms-sign.js +74 -55
- package/lib/cms-verify.js +97 -75
- package/lib/composite-sig.js +13 -13
- package/lib/constants.js +4 -4
- package/lib/crl-sign.js +22 -22
- package/lib/crl-verify.js +7 -6
- package/lib/crmf-sign.js +14 -14
- package/lib/csr-sign.js +8 -8
- package/lib/ct.js +37 -37
- package/lib/edwards-point.js +7 -7
- package/lib/est.js +98 -55
- package/lib/framework-error.js +5 -5
- package/lib/guard-all.js +3 -3
- package/lib/guard-async.js +4 -4
- package/lib/guard-bytes.js +79 -79
- package/lib/guard-compress.js +17 -17
- package/lib/guard-crypto.js +1 -1
- package/lib/guard-encoding.js +15 -15
- package/lib/guard-header.js +3 -3
- package/lib/guard-identifier.js +16 -16
- package/lib/guard-json.js +15 -15
- package/lib/guard-limits.js +7 -7
- package/lib/guard-name.js +81 -16
- package/lib/guard-parsed.js +80 -80
- package/lib/guard-range.js +19 -19
- package/lib/guard-secret.js +11 -10
- package/lib/guard-text.js +6 -6
- package/lib/guard-time.js +10 -10
- package/lib/hpke.js +18 -17
- package/lib/http-digest.js +35 -35
- package/lib/http-retry-after.js +13 -13
- package/lib/http-transport.js +20 -19
- package/lib/inspect.js +53 -53
- package/lib/ip-utils.js +2 -2
- package/lib/jose.js +13 -13
- package/lib/key.js +16 -16
- package/lib/lint.js +51 -51
- package/lib/merkle.js +51 -36
- package/lib/mime.js +18 -18
- package/lib/ocsp-verify.js +10 -10
- package/lib/ocsp.js +13 -13
- package/lib/oid.js +29 -29
- package/lib/path-validate.js +114 -113
- package/lib/pbes2.js +16 -16
- package/lib/pkcs12-build.js +53 -53
- package/lib/pki-build.js +21 -19
- package/lib/rc2.js +1 -1
- package/lib/rfc3339.js +5 -5
- package/lib/schema-all.js +31 -31
- package/lib/schema-attrcert.js +12 -12
- package/lib/schema-c509.js +144 -142
- package/lib/schema-cmc.js +58 -58
- package/lib/schema-cmp.js +43 -43
- package/lib/schema-cms.js +45 -45
- package/lib/schema-crl.js +7 -7
- package/lib/schema-crmf.js +28 -28
- package/lib/schema-csr.js +12 -12
- package/lib/schema-csrattrs.js +16 -16
- package/lib/schema-engine.js +18 -18
- package/lib/schema-ocsp.js +15 -15
- package/lib/schema-pkcs12.js +20 -20
- package/lib/schema-pkcs8.js +2 -2
- package/lib/schema-pkix.js +131 -126
- package/lib/schema-smime.js +19 -19
- package/lib/schema-tsp.js +12 -12
- package/lib/schema-x509.js +3 -3
- package/lib/shbs.js +18 -18
- package/lib/sign-scheme.js +13 -13
- package/lib/sigstore.js +10 -11
- package/lib/sleep.js +1 -1
- package/lib/smime.js +308 -96
- package/lib/tls-cert-compress.js +18 -18
- package/lib/trust.js +27 -27
- package/lib/tsp-sign.js +17 -17
- package/lib/validator-all.js +1 -1
- package/lib/validator-attcert.js +1 -1
- package/lib/validator-cose.js +43 -44
- package/lib/validator-keydesc.js +3 -3
- package/lib/validator-sig.js +13 -13
- package/lib/validator-tls.js +11 -11
- package/lib/validator-tpm.js +20 -19
- package/lib/webauthn-mds.js +66 -66
- package/lib/webauthn.js +33 -33
- package/lib/webcrypto.js +15 -15
- package/lib/x509-sign.js +13 -13
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/lint.js
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* a spec-clause citation, and a human message. It ships the RFC 5280 certificate profile
|
|
13
13
|
* plus a representative CA/Browser Forum TLS Baseline Requirements subset.
|
|
14
14
|
*
|
|
15
|
-
* Unlike every other toolkit entry, the lint
|
|
15
|
+
* Unlike every other toolkit entry, the lint data path never throws. A linter surveys a
|
|
16
16
|
* corpus that includes malformed members, so `pki.lint.certificate(hostileBytes)` returns
|
|
17
17
|
* a report whose worst finding is a `fatal` id `lint/unparseable` (carrying the inner
|
|
18
|
-
* `PkiError.code`)
|
|
19
|
-
* unknown profile, an out-of-range severity threshold, or a wrong-type input
|
|
18
|
+
* `PkiError.code`) and does not raise. The one throw path is config-time misuse (an
|
|
19
|
+
* unknown profile, an out-of-range severity threshold, or a wrong-type input), which
|
|
20
20
|
* raises a typed `LintError`. This deliberate inversion of the toolkit's fail-closed-throw
|
|
21
21
|
* posture is what lets an operator lint a whole directory without a try/catch per file.
|
|
22
22
|
* @spec RFC 5280, CA/Browser Forum TLS Baseline Requirements
|
|
@@ -41,7 +41,7 @@ function _cfg(code, message, cause) { return new LintError(code, message, cause)
|
|
|
41
41
|
// Strict-parse pre-emption: this toolkit's parser is strict-DER, so several RFC 5280
|
|
42
42
|
// profile violations a lenient linter (zlint/pkilint) would report as a SPECIFIC finding
|
|
43
43
|
// are instead rejected fail-closed at PARSE and surface here as a single `fatal`
|
|
44
|
-
// lint/unparseable whose context.code names the exact structural violation
|
|
44
|
+
// lint/unparseable whose context.code names the exact structural violation: e.g. a
|
|
45
45
|
// duplicate extension OID (x509/duplicate-extension), a pathLenConstraint without cA
|
|
46
46
|
// (bad-basic-constraints), an explicit cA=FALSE, or a signatureAlgorithm/tbs mismatch.
|
|
47
47
|
// The registry therefore ships no dedicated rule for a violation the parser already
|
|
@@ -53,14 +53,14 @@ function _cfg(code, message, cause) { return new LintError(code, message, cause)
|
|
|
53
53
|
var NS = pkix.makeNS("lint", LintError, oid);
|
|
54
54
|
var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
|
|
55
55
|
|
|
56
|
-
// Coverage note: the rules and helpers below carry defensive guards
|
|
57
|
-
// on fields the strict parser
|
|
56
|
+
// Coverage note: the rules and helpers below carry defensive guards, `|| []` / `|| ""`
|
|
57
|
+
// on fields the strict parser always sets (extensions, serialNumberHex, subject.rdns), and
|
|
58
58
|
// `!Buffer.isBuffer` / `instanceof Date` / minimal-encoding checks against malformed-but-
|
|
59
59
|
// parsed shapes the strict parser never produces. These are fail-safe belts, not reachable
|
|
60
|
-
// paths; they stay verified-unreachable (documented)
|
|
60
|
+
// paths; they stay verified-unreachable (documented) and are kept, since the lint
|
|
61
61
|
// data path must never throw even on an unexpected upstream shape.
|
|
62
62
|
|
|
63
|
-
// ---- the advisory result surface
|
|
63
|
+
// ---- the advisory result surface: graded findings, not a throw ----
|
|
64
64
|
|
|
65
65
|
// Severity ordering (zlint's LintStatus). NA (not applicable) and NE (not effective --
|
|
66
66
|
// outside a rule's date window) are rule OUTCOMES, tracked in counts, not severities.
|
|
@@ -91,8 +91,8 @@ function _finding(rule, detail) {
|
|
|
91
91
|
// on. That makes it a decision like any other, so it takes the same derivation: the parser's record,
|
|
92
92
|
// not the caller's object.
|
|
93
93
|
//
|
|
94
|
-
// Returns the
|
|
95
|
-
// the rules must run against
|
|
94
|
+
// Returns the derived certificate or null, never a boolean, because the derived value is what
|
|
95
|
+
// the rules must run against: answering "yes, that is a certificate" and then linting the object
|
|
96
96
|
// would be the check computed and thrown away. Never throws: _ingest's contract is to return a
|
|
97
97
|
// finding for bad DATA and reserve throws for a wrong-TYPE argument.
|
|
98
98
|
function _derivedCert(o) {
|
|
@@ -106,7 +106,7 @@ function _derivedCert(o) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
// Returns { cert } on success, or { fatal: <Finding> } when hostile bytes do not parse
|
|
109
|
-
// (the never-throw data path). Throws LintError
|
|
109
|
+
// (the never-throw data path). Throws LintError only on a wrong-type input (config misuse).
|
|
110
110
|
function _ingest(input) {
|
|
111
111
|
var derived = _derivedCert(input);
|
|
112
112
|
if (derived) return { cert: derived };
|
|
@@ -151,7 +151,7 @@ function _hasEku(cert, ekuName) {
|
|
|
151
151
|
function _makeCtx(cert, profile) {
|
|
152
152
|
var explicitTls = profile === "cabf-tls";
|
|
153
153
|
// A CA certificate is not a TLS server (leaf) certificate even when it carries a
|
|
154
|
-
// serverAuth EKU, so the default profile must
|
|
154
|
+
// serverAuth EKU, so the default profile must not apply the CABF leaf rules (SAN
|
|
155
155
|
// required, CN-in-SAN, validity ceiling, ...) to it. An explicit cabf-tls selection
|
|
156
156
|
// still lints whatever the caller hands it as a server cert.
|
|
157
157
|
var bc = _decodeOrNull(cert, "basicConstraints");
|
|
@@ -211,7 +211,7 @@ function _serialOctets(cert) {
|
|
|
211
211
|
// so the linter and the identity comparators agree on what a well-formed dNSName is. Returns a reason or null.
|
|
212
212
|
function _dnsNameProblem(s) { return pkix.dnsNameProblem(s); }
|
|
213
213
|
|
|
214
|
-
// A genuine IPv4 or IPv6 literal
|
|
214
|
+
// A genuine IPv4 or IPv6 literal, so a CN is validated against an iPAddress SAN and not a
|
|
215
215
|
// dNSName. Routed through the shared strict validator (no node:net, so the toolkit needs no
|
|
216
216
|
// networking module): a per-octet-bounded dotted quad, or a full RFC 4291 IPv6 form
|
|
217
217
|
// (including the IPv4-mapped / dual-stack tail). Rejects 999.999.999.999 and "api:443".
|
|
@@ -231,26 +231,26 @@ function _subjectCNs(cert) {
|
|
|
231
231
|
return out;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
// An extension's
|
|
235
|
-
// it cannot
|
|
236
|
-
// path-validate's PROCESSED_EXTENSIONS (the RFC 5280 sec. 6.1 path-processing set),
|
|
237
|
-
// table: certExtensionDecoders also
|
|
234
|
+
// An extension's criticality is honoured (RFC 5280 4.2: a consumer MUST reject a critical extension
|
|
235
|
+
// it cannot process) only when this toolkit actually processes its semantics. That authority is
|
|
236
|
+
// path-validate's PROCESSED_EXTENSIONS (the RFC 5280 sec. 6.1 path-processing set), never the decoder
|
|
237
|
+
// table: certExtensionDecoders also decodes many extensions purely for display (qcStatements, the MS
|
|
238
238
|
// enterprise-CA extensions, the SCT list, the *KeyIdentifiers, freshestCRL, issuerAltName) whose
|
|
239
|
-
// critical semantics are not enforced
|
|
239
|
+
// critical semantics are not enforced. path-validate rejects a critical instance of any of those
|
|
240
240
|
// as unrecognized-critical, so the linter flags them too (structural decodability is not validation
|
|
241
|
-
// processing). The one decode-only extension that is
|
|
242
|
-
// (RFC 6962 sec. 3.1 REQUIRES it critical), so it is allow-listed. Everything else
|
|
243
|
-
// extension
|
|
244
|
-
// is no extension)
|
|
241
|
+
// processing). The one decode-only extension that is legitimately critical is precertificatePoison
|
|
242
|
+
// (RFC 6962 sec. 3.1 REQUIRES it critical), so it is allow-listed. Everything else, a processed
|
|
243
|
+
// extension or an unregistered OID (algorithm / EKU-purpose OID the name registry resolves but that
|
|
244
|
+
// is no extension), falls out correctly: processed -> recognized, unregistered -> unknown. Driving
|
|
245
245
|
// this off the validator's set (not the decoder table) means a newly-decoded extension is
|
|
246
246
|
// flagged-when-critical automatically until its semantics are actually processed.
|
|
247
247
|
var _CRITICAL_LEGIT = {};
|
|
248
248
|
_CRITICAL_LEGIT[oid.byName("precertificatePoison")] = true;
|
|
249
249
|
// Mirrors path-validate's unrecognizedCriticalExtension. An extension not in PROCESSED_EXTENSIONS is
|
|
250
250
|
// unknown everywhere (precertificatePoison is the one legitimately-critical exception). An extension
|
|
251
|
-
// that
|
|
252
|
-
// flagged: path-validate decides target status purely by path position (i === n), so
|
|
253
|
-
//
|
|
251
|
+
// that is processed for an intermediate but unprocessed on the target (policyMappings) is also
|
|
252
|
+
// flagged: path-validate decides target status purely by path position (i === n), so any certificate,
|
|
253
|
+
// a CA included, can be validated as the final/target certificate, where a critical instance is
|
|
254
254
|
// rejected. A linter has no path context and cannot rule that out, so it flags these regardless of
|
|
255
255
|
// cA-ness (policyMappings is SHOULD-be-non-critical everywhere, so this never mis-flags conformant input).
|
|
256
256
|
function _isUnknownExtension(extOid) {
|
|
@@ -267,7 +267,7 @@ function _rsaModulusBits(spki) {
|
|
|
267
267
|
var pk = spki.publicKey && (spki.publicKey.bytes || spki.publicKey);
|
|
268
268
|
if (!Buffer.isBuffer(pk)) return null;
|
|
269
269
|
var seq = asn1.decode(pk);
|
|
270
|
-
// RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
|
|
270
|
+
// RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }: exactly two
|
|
271
271
|
// INTEGERs, and the modulus MUST be positive. A negative/zero modulus (a high-bit key
|
|
272
272
|
// mis-encoded without a sign pad) or a missing exponent is malformed, not a real key.
|
|
273
273
|
if (!seq.children || seq.children.length !== 2) return null;
|
|
@@ -292,17 +292,17 @@ function _ecCurveName(spki) {
|
|
|
292
292
|
|
|
293
293
|
// RFC 5280 marks several extensions MUST (error) or SHOULD (warn) be critical. The shape is
|
|
294
294
|
// uniform: applies when the extension is present, fires when its raw `critical` flag is not
|
|
295
|
-
// true. The rule reads `ctx.raw(name).critical`
|
|
296
|
-
// a structural property of the extension, independent of its contents.
|
|
295
|
+
// true. The rule reads `ctx.raw(name).critical` without decoding the value, because criticality
|
|
296
|
+
// is a structural property of the extension, independent of its contents.
|
|
297
297
|
// ---- RFC 5280 4.2.1.4 userNotice DisplayText ----
|
|
298
|
-
// certificatePolicies surfaces each policy's qualifiers as
|
|
298
|
+
// certificatePolicies surfaces each policy's qualifiers as raw bytes (an external verifier may hash
|
|
299
299
|
// them), so the DisplayText values are read out of those bytes here. The UserNotice walk itself lives
|
|
300
300
|
// in schema-pkix (pki.inspect renders the same values), so the two consumers cannot disagree about
|
|
301
301
|
// which members are DisplayText or how a BMPString decodes.
|
|
302
302
|
var OID_UNOTICE = oid.byName("unotice");
|
|
303
303
|
var _T_BMP = asn1.TAGS.BMP_STRING, _T_VISIBLE = asn1.TAGS.VISIBLE_STRING, _T_UTF8 = asn1.TAGS.UTF8_STRING;
|
|
304
|
-
// The C0 and C1 control ranges, tested by code point
|
|
305
|
-
//
|
|
304
|
+
// The C0 and C1 control ranges, tested by code point instead of a regex: the characters this rule is
|
|
305
|
+
// about cannot appear in the source, and a regex holding them would be a control byte here.
|
|
306
306
|
function _hasControlChar(str) {
|
|
307
307
|
for (var i = 0; i < str.length; i++) {
|
|
308
308
|
var c = str.charCodeAt(i);
|
|
@@ -316,7 +316,7 @@ function _hasControlChar(str) {
|
|
|
316
316
|
// The two decodes below are fail-safe belts on the never-throw data path, not reachable paths: the
|
|
317
317
|
// shared decoder ran pkix.assertPolicyQualifiers before surfacing qualifiersBytes, so the bytes are
|
|
318
318
|
// already known to decode as a SEQUENCE whose every element is a two-member PolicyQualifierInfo led
|
|
319
|
-
// by a readable OID. An undecodable extension never reaches here at all
|
|
319
|
+
// by a readable OID. An undecodable extension never reaches here at all; it is the separate
|
|
320
320
|
// extension-undecodable finding.
|
|
321
321
|
function _policyDisplayTexts(ctx) {
|
|
322
322
|
var d = ctx.decode("certificatePolicies");
|
|
@@ -333,8 +333,8 @@ function _policyDisplayTexts(ctx) {
|
|
|
333
333
|
try { qid = asn1.read.oid(pq.children[0]); } catch (_e2) { return; }
|
|
334
334
|
if (qid !== OID_UNOTICE) return;
|
|
335
335
|
// Entries whose contents did not decode arrive with a null `text` and keep their tag: the rules
|
|
336
|
-
// below that read the text skip them, while the encoding rule
|
|
337
|
-
// answers
|
|
336
|
+
// below that read the text skip them, while the encoding rule, which the ASN.1 tag alone
|
|
337
|
+
// answers, still sees them.
|
|
338
338
|
out = out.concat(pkix.userNoticeTexts(pq.children[1]));
|
|
339
339
|
});
|
|
340
340
|
});
|
|
@@ -344,8 +344,8 @@ function _hasPolicyDisplayText(cert, ctx) { return _policyDisplayTexts(ctx).leng
|
|
|
344
344
|
|
|
345
345
|
// RFC 5280 marks several extensions MUST (error) or SHOULD (warn) be critical. The shape is
|
|
346
346
|
// uniform: applies when the extension is present, fires when its raw `critical` flag is not
|
|
347
|
-
// true. The rule reads `ctx.raw(name).critical`
|
|
348
|
-
// a structural property of the extension, independent of its contents.
|
|
347
|
+
// true. The rule reads `ctx.raw(name).critical` without decoding the value, because criticality
|
|
348
|
+
// is a structural property of the extension, independent of its contents.
|
|
349
349
|
function _criticalityRule(name, id, severity, citation, message) {
|
|
350
350
|
return {
|
|
351
351
|
id: id, severity: severity, source: "rfc5280", citation: citation, message: message,
|
|
@@ -441,7 +441,7 @@ var RFC5280_RULES = [
|
|
|
441
441
|
// public keys used to validate digital signatures on certificates" (keyCertSign). A CA
|
|
442
442
|
// key used exclusively for other purposes (CRL signing, key management) MAY carry a
|
|
443
443
|
// non-critical basicConstraints, so the rule applies only when keyCertSign is asserted
|
|
444
|
-
// (or keyUsage is absent)
|
|
444
|
+
// (or keyUsage is absent); gating on cA alone would false-positive on a CRL-signing CA.
|
|
445
445
|
id: "lint/rfc5280/basic-constraints-not-critical", severity: "error", source: "rfc5280", citation: "RFC 5280 4.2.1.9",
|
|
446
446
|
message: "a CA certificate that validates certificate signatures must mark basicConstraints critical",
|
|
447
447
|
appliesTo: function (cert, ctx) {
|
|
@@ -466,8 +466,8 @@ var RFC5280_RULES = [
|
|
|
466
466
|
_criticalityRule("inhibitAnyPolicy", "lint/rfc5280/inhibit-any-policy-not-critical", "error",
|
|
467
467
|
"RFC 5280 4.2.1.14", "the inhibitAnyPolicy extension must be marked critical"),
|
|
468
468
|
// 4.2.1.3: "When present, conforming CAs SHOULD mark this extension as critical." The
|
|
469
|
-
// recommendation directs the
|
|
470
|
-
// including an end-entity leaf
|
|
469
|
+
// recommendation directs the issuing CA whenever it includes keyUsage, in any certificate
|
|
470
|
+
// including an end-entity leaf, and does not restrict it to certificates whose subject is
|
|
471
471
|
// itself a CA. So the rule is present-gated (matching zlint's w_ext_key_usage_not_critical),
|
|
472
472
|
// not CA-subject-gated; a SHOULD, hence warn.
|
|
473
473
|
_criticalityRule("keyUsage", "lint/rfc5280/key-usage-not-critical", "warn",
|
|
@@ -487,9 +487,9 @@ var RFC5280_RULES = [
|
|
|
487
487
|
// would refuse certificates that are in the wild and otherwise valid. Reporting them is exactly what
|
|
488
488
|
// a linter is for, so each rule carries the severity its normative word does.
|
|
489
489
|
{
|
|
490
|
-
// DisplayText is SIZE (1..200)
|
|
490
|
+
// DisplayText is SIZE (1..200) at both ends, but the two ends get separate ids instead of one
|
|
491
491
|
// length rule, because the section treats them differently: it tells certificate users to handle a
|
|
492
|
-
// notice
|
|
492
|
+
// notice above 200 gracefully, and says nothing of the sort about an empty one. An operator acting
|
|
493
493
|
// on that advice suppresses the over-long finding; folding both into a single id would silently
|
|
494
494
|
// suppress the empty case along with it, which has no such carve-out.
|
|
495
495
|
id: "lint/rfc5280/explicit-text-too-long", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.4",
|
|
@@ -604,9 +604,9 @@ var CABF_TLS_RULES = [
|
|
|
604
604
|
{
|
|
605
605
|
id: "lint/cabf-tls/eku-missing-serverauth", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.6",
|
|
606
606
|
message: "a TLS server certificate's extKeyUsage must include id-kp-serverAuth",
|
|
607
|
-
// Only meaningful when the caller
|
|
608
|
-
// default profile a cert is recognized as TLS
|
|
609
|
-
// vacuous and reports NA
|
|
607
|
+
// Only meaningful when the caller explicitly lints against the TLS profile; under the
|
|
608
|
+
// default profile a cert is recognized as TLS by its serverAuth EKU, so the check is
|
|
609
|
+
// vacuous and reports NA, never firing on every non-TLS certificate.
|
|
610
610
|
appliesTo: function (cert, ctx) { return ctx.explicitTlsProfile; },
|
|
611
611
|
check: function (cert, ctx) { return _hasEku(cert, "serverAuth") ? null : true; },
|
|
612
612
|
},
|
|
@@ -618,8 +618,8 @@ var CABF_TLS_RULES = [
|
|
|
618
618
|
var spki = cert.subjectPublicKeyInfo, name = spki && spki.algorithm && spki.algorithm.name;
|
|
619
619
|
// Fail closed: a key whose size/curve cannot be confirmed to meet the minimum is
|
|
620
620
|
// flagged, not passed. RSA below 2048 bits (or an unreadable modulus), or an EC key
|
|
621
|
-
//
|
|
622
|
-
// which resolve to a null curve) is a finding
|
|
621
|
+
// that is not on an approved named curve (including absent / explicit / invalid parameters,
|
|
622
|
+
// which resolve to a null curve) is a finding: CABF permits only P-256/384/521.
|
|
623
623
|
if (RSA_KEY_ALGS[name]) { var bits = _rsaModulusBits(spki); return (bits === null || bits < 2048) ? { context: { rsaBits: bits } } : null; }
|
|
624
624
|
if (name === "ecPublicKey") { var curve = _ecCurveName(spki); return APPROVED_EC_CURVES.indexOf(curve) === -1 ? { context: { curve: curve } } : null; }
|
|
625
625
|
// Any other key type (e.g. EdDSA) is out of this rule's RSA-size / EC-curve scope --
|
|
@@ -694,7 +694,7 @@ function _selectRules(profile) {
|
|
|
694
694
|
if (profile == null || profile === "all" || profile === "default") return ALL_RULES;
|
|
695
695
|
if (!PROFILES[profile]) throw _cfg("lint/unknown-profile", "unknown lint profile \"" + profile + "\" (known: " + Object.keys(PROFILES).join(", ") + ")");
|
|
696
696
|
// An explicit cabf-tls run still needs the always-on rfc5280 structural rules? No: the
|
|
697
|
-
// caller asked for
|
|
697
|
+
// caller asked for one profile. Return exactly that profile's rules.
|
|
698
698
|
return PROFILES[profile];
|
|
699
699
|
}
|
|
700
700
|
|
|
@@ -721,10 +721,10 @@ function _applyThreshold(report, severity) {
|
|
|
721
721
|
* `pki.schema.x509` object. Returns a `LintReport`
|
|
722
722
|
* `{ findings: [{id, severity, source, citation, message, context?}], counts, worst, ran }`.
|
|
723
723
|
*
|
|
724
|
-
* The
|
|
725
|
-
* `lint/unparseable`
|
|
726
|
-
* (`opts.profile` unknown, `opts.severity` out of range, or a wrong-type input)
|
|
727
|
-
* typed `LintError`.
|
|
724
|
+
* The data path never throws: hostile bytes produce a single `fatal` finding
|
|
725
|
+
* `lint/unparseable` and no exception. The one throw path is config-time misuse
|
|
726
|
+
* (`opts.profile` unknown, `opts.severity` out of range, or a wrong-type input), which
|
|
727
|
+
* raises a typed `LintError`.
|
|
728
728
|
*
|
|
729
729
|
* @opts profile One of `pki.lint.profiles()` (default runs every profile). Selecting
|
|
730
730
|
* `"cabf-tls"` lints the input AS a TLS server certificate.
|
package/lib/merkle.js
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* RFC 6962 (Certificate Transparency) / RFC 9162 (CT 2.0) Merkle-tree hash
|
|
13
13
|
* and proof-verification core -- the load-bearing primitive a static-CT
|
|
14
14
|
* client, a Merkle-Tree-Certificates relying party, and a sigstore / Rekor
|
|
15
|
-
* inclusion check all compose.
|
|
16
|
-
* zero new crypto.
|
|
15
|
+
* inclusion check all compose. Every verb is strict verification over
|
|
16
|
+
* SHA-256: zero new crypto.
|
|
17
17
|
*
|
|
18
18
|
* `leafHash` / `nodeHash` / `emptyRootHash` build the tree hashes with the
|
|
19
|
-
* two domain-separation prefixes fixed by the spec
|
|
19
|
+
* two domain-separation prefixes fixed by the spec: a leaf is
|
|
20
20
|
* `SHA-256(0x00 || entry)`, an interior node is `SHA-256(0x01 || left ||
|
|
21
21
|
* right)`, the empty tree is `SHA-256("")`. Those `0x00` / `0x01` prefixes
|
|
22
22
|
* are the second-preimage defense: without them a leaf whose bytes equal a
|
|
@@ -24,16 +24,16 @@
|
|
|
24
24
|
*
|
|
25
25
|
* `verifyInclusion` folds an audit path back to a root and constant-time-
|
|
26
26
|
* compares it to a trusted checkpoint root; `verifyConsistency` reconstructs
|
|
27
|
-
*
|
|
27
|
+
* both the old and the new root from a consistency proof (the append-only
|
|
28
28
|
* guarantee lives in the old-root leg). Both are fail-closed: a malformed
|
|
29
29
|
* coordinate, an out-of-range index, an inverted window, a wrong hash length,
|
|
30
30
|
* or a proof whose node count does not match the tree geometry throws a typed
|
|
31
|
-
* `merkle/*` error; the
|
|
31
|
+
* `merkle/*` error; the one boolean-`false` result is the final root
|
|
32
32
|
* comparison ("root matched" vs "did not"). A `false` from `verifyInclusion`
|
|
33
|
-
* means "not proven present against this root", never "validly absent"
|
|
33
|
+
* means "not proven present against this root", never "validly absent": an
|
|
34
34
|
* inclusion proof cannot express absence. Tree coordinates are uint64, carried
|
|
35
|
-
* as `BigInt` so a large index is never `Number`-narrowed. This is
|
|
36
|
-
* format
|
|
35
|
+
* as `BigInt` so a large index is never `Number`-narrowed. This is not a DER
|
|
36
|
+
* format. Like `pki.ct` it is a companion module reached explicitly, never
|
|
37
37
|
* routed by the detect-and-parse orchestrator.
|
|
38
38
|
*
|
|
39
39
|
* @card
|
|
@@ -171,11 +171,11 @@ function emptyRootHash() {
|
|
|
171
171
|
* Verify an RFC 6962 / RFC 9162 audit (inclusion) proof: fold `leafHash` up the
|
|
172
172
|
* audit path and constant-time-compare the reconstructed root to `rootHash`.
|
|
173
173
|
* Returns `true` iff the proof binds the leaf to the root; a well-formed proof
|
|
174
|
-
* that does not match returns `false
|
|
175
|
-
*
|
|
176
|
-
* non-negative integer (or a Number >= 2^53),
|
|
177
|
-
* treeSize`, a non-32-byte hash, or a proof whose
|
|
178
|
-
*
|
|
174
|
+
* that does not match returns `false`, meaning "not proven present against this
|
|
175
|
+
* root" and never "validly absent". A malformed input throws a typed `merkle/*`
|
|
176
|
+
* error: a coordinate that is not a non-negative integer (or a Number >= 2^53),
|
|
177
|
+
* `treeSize` 0, `leafIndex >= treeSize`, a non-32-byte hash, or a proof whose
|
|
178
|
+
* node count does not match the tree geometry.
|
|
179
179
|
*
|
|
180
180
|
* @opts
|
|
181
181
|
* leafIndex: number | bigint, // 0-based leaf position (uint64; pass BigInt above 2^53)
|
|
@@ -235,19 +235,26 @@ function verifyInclusion(opts) {
|
|
|
235
235
|
*
|
|
236
236
|
* Verify an RFC 6962 / RFC 9162 consistency proof between an older tree of
|
|
237
237
|
* `oldSize` leaves (root `oldRoot`) and a newer tree of `newSize` leaves (root
|
|
238
|
-
* `newRoot`). Reconstructs
|
|
239
|
-
* each; returns `true` iff both match
|
|
240
|
-
* old-root leg
|
|
241
|
-
* is a rewritten history and returns `false
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* `
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* `
|
|
249
|
-
*
|
|
250
|
-
*
|
|
238
|
+
* `newRoot`). Reconstructs both roots from the proof and constant-time-compares
|
|
239
|
+
* each; returns `true` iff both match. The append-only guarantee lives in the
|
|
240
|
+
* old-root leg: a proof that yields a valid `newRoot` but the wrong `oldRoot`
|
|
241
|
+
* is a rewritten history and returns `false`. Equal non-zero sizes require an
|
|
242
|
+
* empty proof and `oldRoot == newRoot`.
|
|
243
|
+
*
|
|
244
|
+
* An `oldSize` of 0 with a non-empty newer tree is refused as
|
|
245
|
+
* `merkle/no-consistency-claim`. RFC 6962 sec. 2.1.2 defines the proof for
|
|
246
|
+
* `0 < oldSize < newSize`: the empty tree is a prefix of every tree by
|
|
247
|
+
* definition, so there is no proof to check and nothing at all binds `newRoot`.
|
|
248
|
+
* Returning `true` there would say a step was proven append-only when nothing had
|
|
249
|
+
* been proven, and a monitor written `if (!verifyConsistency(...)) alarm()` would
|
|
250
|
+
* take that silence for evidence. A caller with no prior tree has an inclusion
|
|
251
|
+
* question about the new one, or should start from a checkpoint it already
|
|
252
|
+
* trusts. Two empty trees are still answered:
|
|
253
|
+
* that is the degenerate identity case, and both roots must be `emptyRootHash()`.
|
|
254
|
+
*
|
|
255
|
+
* A malformed input throws a typed `merkle/*` error: `oldSize > newSize`, a
|
|
256
|
+
* non-empty proof where the geometry requires none (or empty where it requires
|
|
257
|
+
* one), a non-32-byte hash, or a wrong node count.
|
|
251
258
|
*
|
|
252
259
|
* @opts
|
|
253
260
|
* oldSize: number | bigint, // leaf count of the older tree (uint64)
|
|
@@ -273,18 +280,26 @@ function verifyConsistency(opts) {
|
|
|
273
280
|
|
|
274
281
|
if (oldSize > newSize) throw new MerkleError("merkle/old-size-exceeds-new", "oldSize " + oldSize + " exceeds newSize " + newSize);
|
|
275
282
|
if (oldSize === 0n) {
|
|
276
|
-
if (proof.length !== 0) throw new MerkleError("merkle/bad-proof-length", "an empty older tree admits only the empty consistency proof");
|
|
277
283
|
var er = emptyRootHash();
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
// empty root. When the NEW tree is also empty, its root must be the empty
|
|
281
|
-
// root too (a bogus newRoot must not pass); when newSize > 0 an empty-old
|
|
282
|
-
// proof places no binding on newRoot.
|
|
284
|
+
// newSize 0 is the degenerate identity case, not a consistency proof: both trees are the empty
|
|
285
|
+
// tree, so both roots must be the empty root and a bogus newRoot must not pass.
|
|
283
286
|
if (newSize === 0n) {
|
|
284
|
-
|
|
285
|
-
return
|
|
287
|
+
if (proof.length !== 0) throw new MerkleError("merkle/bad-proof-length", "two empty trees admit only the empty consistency proof");
|
|
288
|
+
return _ctEq(oldRoot, er) && _ctEq(newRoot, er);
|
|
286
289
|
}
|
|
287
|
-
|
|
290
|
+
// newSize > 0 falls outside the definition. RFC 6962 sec. 2.1.2 defines PROOF(m, D[n]) for
|
|
291
|
+
// 0 < m < n, and an empty older tree is a prefix of every tree by definition: there is no
|
|
292
|
+
// proof to check and nothing whatever is bound about newRoot, which is why the value is not
|
|
293
|
+
// read on this branch. Returning the same `true` the honest path returns would state that a
|
|
294
|
+
// step was proven append-only when nothing was proven, and a monitor written
|
|
295
|
+
// `if (!verifyConsistency(...)) alarm()` would take silence for evidence. A boolean cannot
|
|
296
|
+
// carry the difference, so the question is refused. A caller with no prior tree has an
|
|
297
|
+
// inclusion question about the new tree, not a consistency question.
|
|
298
|
+
throw new MerkleError("merkle/no-consistency-claim",
|
|
299
|
+
"an empty older tree (oldSize 0) makes no consistency claim about a non-empty newer tree: " +
|
|
300
|
+
"RFC 6962 sec. 2.1.2 defines the proof for 0 < oldSize < newSize, and nothing here binds " +
|
|
301
|
+
"newRoot. Verify an inclusion proof against the new tree, or start from a signed tree head " +
|
|
302
|
+
"you already trust");
|
|
288
303
|
}
|
|
289
304
|
if (oldSize === newSize) {
|
|
290
305
|
if (proof.length !== 0) throw new MerkleError("merkle/sizes-equal-nonempty-proof", "equal tree sizes require an empty consistency proof");
|
|
@@ -319,7 +334,7 @@ function verifyConsistency(opts) {
|
|
|
319
334
|
sn >>= 1n;
|
|
320
335
|
}
|
|
321
336
|
if (sn !== 0n) throw new MerkleError("merkle/bad-proof-length", "consistency proof is shorter than the geometry requires");
|
|
322
|
-
// Evaluate
|
|
337
|
+
// Evaluate both constant-time compares unconditionally before combining, so a
|
|
323
338
|
// `&&` never short-circuits the second `timingSafeEqual` (which would leak,
|
|
324
339
|
// via timing, whether the old-root leg matched).
|
|
325
340
|
var okOld = _ctEq(fr, oldRoot);
|
package/lib/mime.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* splits a multipart body on its boundary, and builds an entity/multipart back. There is NO crypto
|
|
10
10
|
* here -- the CMS layer signs/verifies the canonical bytes this module produces.
|
|
11
11
|
*
|
|
12
|
-
* The
|
|
13
|
-
* is computed over that part's canonical MIME form, so the signer and verifier MUST share
|
|
14
|
-
* canonicalizer
|
|
12
|
+
* The load-bearing rule is canonicalization: the detached signature over a multipart/signed first part
|
|
13
|
+
* is computed over that part's canonical MIME form, so the signer and verifier MUST share one
|
|
14
|
+
* canonicalizer, this module. It carries the caller's typed ErrorClass `E` (constructed
|
|
15
15
|
* `new E(code, message)`), exactly as the byte-reader / guard family do, so every consumer keeps its
|
|
16
16
|
* own `domain/reason` fault code.
|
|
17
17
|
*/
|
|
@@ -50,14 +50,14 @@ function _unfoldHeaders(headerText) {
|
|
|
50
50
|
return out;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// Parse a Content-Type / any structured header value into { value, type, params }
|
|
53
|
+
// Parse a Content-Type / any structured header value into { value, type, params }: the media type
|
|
54
54
|
// (lowercased) plus its parameters (names lowercased, quotes stripped). Tolerant of extra whitespace.
|
|
55
55
|
function _parseStructured(headerValue) {
|
|
56
56
|
var parts = _splitSemicolons(headerValue); // always >= 1 element
|
|
57
57
|
var type = _stripComments(parts[0]).trim().toLowerCase();
|
|
58
58
|
var params = {};
|
|
59
59
|
for (var i = 1; i < parts.length; i++) {
|
|
60
|
-
// RFC 5322 CFWS: a comment around the name/value is folding whitespace, not part of either
|
|
60
|
+
// RFC 5322 CFWS: a comment around the name/value is folding whitespace, not part of either, so strip it
|
|
61
61
|
// so "(note) hp" resolves to the "hp" parameter and a trailing comment does not leak into the value.
|
|
62
62
|
var p = _stripComments(parts[i]);
|
|
63
63
|
var eq = p.indexOf("=");
|
|
@@ -70,7 +70,7 @@ function _parseStructured(headerValue) {
|
|
|
70
70
|
return { value: headerValue.trim(), type: type, params: params };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// Remove RFC 5322 comments
|
|
73
|
+
// Remove RFC 5322 comments: a "(...)" span (nesting; a backslash quoted-pair escapes the next byte) outside
|
|
74
74
|
// a double-quoted string. A "(" inside a quoted string is literal qtext, not a comment. Used to drop CFWS
|
|
75
75
|
// from a structured-header token before the name/value is read, so a comment cannot forge or hide a parameter.
|
|
76
76
|
function _stripComments(s) {
|
|
@@ -93,10 +93,10 @@ function _splitSemicolons(s) {
|
|
|
93
93
|
for (var i = 0; i < s.length; i++) {
|
|
94
94
|
var ch = s[i];
|
|
95
95
|
// RFC 5322 quoted-pair: inside a quoted-string OR a comment a backslash escapes the next char (so an
|
|
96
|
-
// escaped `"` / `)` does not end the span, and a `;` in an escaped value does not split)
|
|
96
|
+
// escaped `"` / `)` does not end the span, and a `;` in an escaped value does not split), so keep both bytes.
|
|
97
97
|
if ((inQ || depth > 0) && ch === "\\" && i + 1 < s.length) { cur += ch + s[i + 1]; i++; }
|
|
98
98
|
else if (ch === '"' && depth === 0) { inQ = !inQ; cur += ch; }
|
|
99
|
-
// RFC 5322 comment: parentheses (outside a quoted-string) delimit a nesting comment
|
|
99
|
+
// RFC 5322 comment: parentheses (outside a quoted-string) delimit a nesting comment, so a `;` or `=` inside
|
|
100
100
|
// one is CFWS, not structural, so a Content-Type comment like `charset=x (note; hp=y)` cannot forge a parameter.
|
|
101
101
|
else if (ch === "(" && !inQ) { depth++; cur += ch; }
|
|
102
102
|
else if (ch === ")" && !inQ && depth > 0) { depth--; cur += ch; }
|
|
@@ -127,7 +127,7 @@ function parse(input, E, code) {
|
|
|
127
127
|
var nm = rawHeaders[i].slice(0, colon).trim();
|
|
128
128
|
var body = rawHeaders[i].slice(colon + 1);
|
|
129
129
|
// value: the trimmed field body (structured-header parsing wants it trimmed). rawValue: the field body
|
|
130
|
-
// with only the single conventional ": " delimiter space removed
|
|
130
|
+
// with only the single conventional ": " delimiter space removed. Leading/trailing whitespace inside
|
|
131
131
|
// the body is preserved, so a consumer that must reproduce the exact authenticated field body (S/MIME
|
|
132
132
|
// header protection) is not handed a lossily-trimmed value.
|
|
133
133
|
headers.push({ name: nm, lname: nm.toLowerCase(), value: body.trim(), rawValue: body.replace(/^ /, "") });
|
|
@@ -151,9 +151,9 @@ function canonicalizeText(bodyBytes) {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
// The canonical MIME form of a part for signing (RFC 8551 sec. 3.1.1): line endings normalized to the
|
|
154
|
-
// CRLF pair over the
|
|
154
|
+
// CRLF pair over the exact bytes, never a header re-serialization, which would diverge from the
|
|
155
155
|
// signer's digest (and would break a header-less clear-signed part, as OpenSSL's `smime -sign` emits).
|
|
156
|
-
// The signer and verifier both call
|
|
156
|
+
// The signer and verifier both call this function, so their digests cannot diverge; a transport that mangles
|
|
157
157
|
// CR/LF is repaired identically on both sides. (A binary clear-signed part must carry a base64
|
|
158
158
|
// Content-Transfer-Encoding first -- line-ending canonicalization is defined for text, sec. 3.1.1.)
|
|
159
159
|
function canonicalize(input, E, code) {
|
|
@@ -192,7 +192,7 @@ function splitMultipart(bodyBytes, boundary, E, code) {
|
|
|
192
192
|
// a line (at the body start, or immediately after an LF) AND its line MUST be a complete delimiter --
|
|
193
193
|
// the token, then an optional "--" (close), then only transport-padding whitespace, then CRLF / LF /
|
|
194
194
|
// end-of-buffer. So a "--boundary" substring mid-content, a "--boundaryextra" (a different token), a
|
|
195
|
-
// "--boundary garbage" line, or one not at a line start is
|
|
195
|
+
// "--boundary garbage" line, or one not at a line start is not matched, so an attacker cannot inject a
|
|
196
196
|
// fake delimiter inside a part's content.
|
|
197
197
|
function _findDelim(body, delim, from) {
|
|
198
198
|
for (var at = body.indexOf(delim, from); at >= 0; at = body.indexOf(delim, at + delim.length)) {
|
|
@@ -217,15 +217,15 @@ function _afterLine(buf, from, E, code) {
|
|
|
217
217
|
|
|
218
218
|
// Serialize an ordered list of header fields + a body into a canonical MIME entity: each field as
|
|
219
219
|
// "Name: value\r\n" (routed through the header-injection guard so a CR/LF/NUL in a value can never inject a
|
|
220
|
-
// header or split the message), then the blank-line separator, then the body
|
|
220
|
+
// header or split the message), then the blank-line separator, then the body, all CRLF-canonicalized so
|
|
221
221
|
// the bytes match a CMS signer's/verifier's digest. `fields` is [{ name, value }]; `body` is a byte source
|
|
222
|
-
// (or null/empty). This is the single header/entity emitter
|
|
222
|
+
// (or null/empty). This is the single header/entity emitter: an inline "Content-Type: ...\r\n" string
|
|
223
223
|
// concatenation is the shape this replaces, so a composer cannot re-inline an unguarded header.
|
|
224
224
|
function buildEntity(fields, body, E, code) {
|
|
225
225
|
var head = "";
|
|
226
226
|
for (var i = 0; i < fields.length; i++) {
|
|
227
227
|
var f = fields[i];
|
|
228
|
-
// Serialize the
|
|
228
|
+
// Serialize the value the guard validated and returned, never a second coercion of f.value (a stateful
|
|
229
229
|
// toString() could return CRLF on the second call and inject a header past the check).
|
|
230
230
|
var v = guard.header.assertField(f.name, f.value, E, code);
|
|
231
231
|
head += f.name + ": " + v + "\r\n";
|
|
@@ -249,10 +249,10 @@ function paramCount(headerValue, name) {
|
|
|
249
249
|
return n;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
// Count parameters with this attribute name
|
|
252
|
+
// Count parameters with this attribute name, with or without a value. paramCount only counts "name=value"
|
|
253
253
|
// parts, so a bare/valueless attribute (e.g. "Content-Type: text/plain; hp") is invisible to it;
|
|
254
|
-
// paramNameCount still sees the attribute name, so a consumer can fail closed on a malformed bare parameter
|
|
255
|
-
// or on a bare+valued duplicate ("hp; hp=x") that paramCount undercounts
|
|
254
|
+
// paramNameCount still sees the attribute name, so a consumer can fail closed on a malformed bare parameter,
|
|
255
|
+
// or on a bare+valued duplicate ("hp; hp=x") that paramCount undercounts, instead of silently ignoring it.
|
|
256
256
|
// Comment- + quoted-string-aware, like paramCount; part 0 is the media type (not a parameter), so attributes
|
|
257
257
|
// start at index 1.
|
|
258
258
|
function paramNameCount(headerValue, name) {
|
package/lib/ocsp-verify.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// @internal -- no operator-facing namespace. The documented surfaces are pki.path.ocspChecker
|
|
6
6
|
// (revocation during path validation) and pki.ocsp.verify (standalone RFC 6960 sec. 3.2 client
|
|
7
|
-
// acceptance), which
|
|
7
|
+
// acceptance), which both compose this single verify core. There is deliberately no second, weaker
|
|
8
8
|
// OCSP response-verification path: a standalone verify that re-derived the responder-authorization
|
|
9
9
|
// gates would be the exact fail-open the out-of-path-signer-cert-full-validation discipline exists
|
|
10
10
|
// to prevent.
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
//
|
|
19
19
|
// Rule set (gap-checked verbatim against RFC 6960 sec. 3.2 / 4.1.1 / 4.2.2.2 / 4.2.2.2.1):
|
|
20
20
|
// - authorizeResponder: the issuing CA directly (responderID identifies the issuer) OR a
|
|
21
|
-
// CA-issued delegate valid at `time` bearing id-kp-OCSPSigning (anyEKU / absent EKU do
|
|
21
|
+
// CA-issued delegate valid at `time` bearing id-kp-OCSPSigning (anyEKU / absent EKU do not
|
|
22
22
|
// authorize), keyUsage-permits-signing, no unknown/malformed critical extension, and
|
|
23
23
|
// id-pkix-ocsp-nocheck (a transport-free verify cannot otherwise confirm the responder is
|
|
24
24
|
// unrevoked); the delegate's own issuance signature is verified under the issuer key.
|
|
25
|
-
// - certIdMatches: serial + issuerNameHash + issuerKeyHash under the CertID's
|
|
25
|
+
// - certIdMatches: serial + issuerNameHash + issuerKeyHash under the CertID's own hashAlgorithm
|
|
26
26
|
// (a serial-only match is a cross-CA substitution and is rejected).
|
|
27
27
|
// - currency: thisUpdate <= time and a bounded nextUpdate > time (a nextUpdate-less response is
|
|
28
28
|
// unusable per the lightweight profile).
|
|
@@ -50,7 +50,7 @@ var OID_KEY_USAGE = oid.byName("keyUsage");
|
|
|
50
50
|
|
|
51
51
|
function ocspDigest(alg, buf) { return subtle.digest(alg, buf).then(function (h) { return Buffer.from(h); }); }
|
|
52
52
|
|
|
53
|
-
// The subjectPublicKey BIT STRING
|
|
53
|
+
// The subjectPublicKey BIT STRING value (past the unused-bits octet) of an SPKI DER: the exact
|
|
54
54
|
// bytes an OCSP CertID issuerKeyHash / byKey KeyHash hash over (RFC 6960 sec. 4.1.1). Throws on a
|
|
55
55
|
// malformed SPKI; the caller fails closed.
|
|
56
56
|
function ocspKeyValue(spkiDer) {
|
|
@@ -59,7 +59,7 @@ function ocspKeyValue(spkiDer) {
|
|
|
59
59
|
|
|
60
60
|
// makeOcspVerify(deps) -> the bound OCSP verify core. deps = { verifyWithSpki, decodeExt, findExt,
|
|
61
61
|
// unrecognizedCriticalExtension, validateCriticalExtensionStructure, compositeKeyUsageCheck,
|
|
62
|
-
// isNullOrAbsentParams, spliceSpkiParameters, dnEqual }
|
|
62
|
+
// isNullOrAbsentParams, spliceSpkiParameters, dnEqual }, the signature engine and RFC 5280
|
|
63
63
|
// cert-profile gates owned by the caller.
|
|
64
64
|
function makeOcspVerify(deps) {
|
|
65
65
|
var verifyWithSpki = deps.verifyWithSpki;
|
|
@@ -99,8 +99,8 @@ function makeOcspVerify(deps) {
|
|
|
99
99
|
return false;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
// A SingleResponse's CertID names the target cert
|
|
103
|
-
// (under the CertID's
|
|
102
|
+
// A SingleResponse's CertID names the target cert if and only if serial, issuerNameHash and issuerKeyHash
|
|
103
|
+
// (under the CertID's own hashAlgorithm) match the issuer. `issuerNameCandidates` is every RFC
|
|
104
104
|
// 5280 sec. 7.1-equal byte encoding of the validated issuer DN to try.
|
|
105
105
|
async function ocspCertIdMatches(certID, cert, issuerNameCandidates, issuerKeyBits) {
|
|
106
106
|
if (certID.serialNumberHex !== cert.serialNumberHex) return false;
|
|
@@ -170,13 +170,13 @@ function makeOcspVerify(deps) {
|
|
|
170
170
|
return null;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
// A BIT STRING is octet-aligned iff its unused-bits count is 0
|
|
173
|
+
// A BIT STRING is octet-aligned iff its unused-bits count is 0, reused from the caller's
|
|
174
174
|
// guard so a non-octet-aligned responder signature is never verified.
|
|
175
175
|
function isOctetAligned(bitString) { return !!bitString && bitString.unusedBits === 0; }
|
|
176
176
|
|
|
177
177
|
// evaluateResponse(resp, cert, issuer, issuerKeyBits, issuerNameCandidates, time, historical) ->
|
|
178
|
-
// a granular per-response
|
|
179
|
-
// matching SingleResponses (a revoked SingleResponse shadows a good one
|
|
178
|
+
// a granular per-response summary for one parsed OCSPResponse, aggregating over all of its
|
|
179
|
+
// matching SingleResponses (a revoked SingleResponse shadows a good one within the response, the
|
|
180
180
|
// same fail-closed law the multi-response aggregator applies). Never throws (fail-closed):
|
|
181
181
|
// { applicable, responderAuthorized, signatureValid, matched, revoked:{reason,revocationReason}?,
|
|
182
182
|
// sawGood, sawUnknownStatus, thisUpdate?, nextUpdate?, reason }
|