@blamejs/pki 0.5.18 → 0.5.20
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 +43 -0
- package/lib/cms-verify.js +18 -5
- package/lib/guard-all.js +2 -0
- package/lib/guard-intrinsic.js +27 -0
- package/lib/guard-list.js +85 -0
- package/lib/jose.js +55 -35
- package/lib/key.js +6 -0
- package/lib/ocsp-verify.js +9 -2
- package/lib/path-validate.js +53 -27
- package/lib/sign-scheme.js +16 -1
- package/lib/tsp-sign.js +12 -4
- package/lib/validator-attcert.js +10 -1
- package/lib/webcrypto.js +165 -90
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.5.20 — 2026-08-20
|
|
8
|
+
|
|
9
|
+
Five verdicts that turned on list membership now decide it by comparison, so replacing an array method after the module loads cannot change what a certificate, a responder or a signed message is found to be.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `guard-list`, an internal choke point for membership decided by comparison. Four verbs cover the rule shapes a verdict needs: is this value present, are all of these present, does any member satisfy a test, do all of them. An absent list is honestly empty rather than a throw, because these run on verdict paths where a throw would read as a different verdict. It is not on the public namespace; the surface is the verdicts above.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Certification-path building asks its loop-detection and fetch-dedupe sets through captured methods. These bound the work a hostile chain can cause, and a replaced `Set.prototype.has` answering false let a cycle be walked and a URL be fetched repeatedly until a size cap stopped it.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `pki.path.validate` holds its `requiredEku` gate against a replaced `Array.prototype.indexOf`. The purpose test IS the decision about what the certificate may be used for, and dispatched live it reported `serverAuth` present on a leaf whose extended key usage carried only `emailProtection`, returning a valid chain for a certificate the caller had asked it to refuse. Certificate-policy membership and the `anyPolicy` scans in the policy tree are decided the same way.
|
|
22
|
+
- `pki.cms.verify` keeps `trusted: false` for a message no supplied anchor covers. The message-level verdict combined the per-signer answers with `every`, and a replaced one reported the whole set trusted without consulting a single signer.
|
|
23
|
+
- An OCSP response is still refused when its delegate lacks id-kp-OCSPSigning. Responder authorization is the decision about who may answer for an issuer, and reading the purpose list through the prototype let a delegate the CA never authorized speak for the whole issuer.
|
|
24
|
+
- A TPM attestation certificate still needs the tcg-kp-AIKCertificate purpose. That test is what makes a leaf an attestation key, so reading it live let an ordinary certificate be accepted as one.
|
|
25
|
+
- A timestamp token carrying an unrecognized critical extension is still refused (RFC 3161 sec. 2.4.2). The scan for a critical extension IS the gate, and a replaced `Array.prototype.some` answering false made the extension invisible and the unusable token acceptable.
|
|
26
|
+
- `pki.cms.verify` keeps `valid: false` for a message whose signature does not verify. The soundness verdict combined the per-signer answers the same way the trust verdict did, so a corrupted signature reported `valid: true`.
|
|
27
|
+
- `pki.path.validate` asks its processed-extension tables for their OWN entries. Freezing a table prevents a write to it and does not prevent an inherited lookup, so a name planted on `Object.prototype` answered for every OID the table did not carry and an unrecognized critical extension counted as processed. On a CA certificate only one of the two tables is consulted, so the chain was accepted rather than refused (RFC 5280 sec. 6.1.4).
|
|
28
|
+
- A trust anchor's `purposes` map is read for its own entries too. The map is the operator's restriction, and an inherited `true` granted the purpose on every anchor whose map did not name it. `distrustAfter` is read the same way.
|
|
29
|
+
- The ML-DSA digest-strength rule (RFC 9882 sec. 3.3) is one question both the signer and the CMS verifier ask, and it answers from the policy table's own entries. A digest name planted on `Object.prototype` answered for every name a parameter set's row omits, so a below-strength digest passed the strength check: `sha256` was accepted for ML-DSA-87 on both paths. The check also had two copies, which is why it now lives in the module that owns the table.
|
|
30
|
+
|
|
31
|
+
## v0.5.19 — 2026-08-20
|
|
32
|
+
|
|
33
|
+
The crypto engine and the JWS signer decide with operations captured at load, so code that runs after the module does cannot change what a permission check concludes.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- `pki.webcrypto` and `pki.jose` are held to the live-read rule the guard family already follows: the check over `lib/` that refuses reading an operation from the runtime at call time covers every module that takes the captures, and these two now do. Operator-visible behavior is unchanged except where a refusal that could be bypassed now holds.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- The WebCrypto key-usage check holds against a replaced `Array.prototype.indexOf`. The membership test IS the permission decision, so it runs through the captured method: dispatched live, a key permitted only for `verify` passed the engine's gate for `sign` and failed later, in the platform's own crypto, with an untyped fault instead of `webcrypto/invalid-access`.
|
|
42
|
+
- A CryptoKey's usage list is a copy this engine took with a captured `slice`, so a replaced one cannot leave the caller holding the array the key is checked against for the rest of its life. The same applies to the partition that decides which usages each half of a generated pair is minted with.
|
|
43
|
+
- `pki.key.import`, `pki.key.generate` and `subtle.generateKey` refuse a non-array `usages` by name. It reached the key constructor, where `Array.prototype.slice` on a number yields an empty list, so the call minted a key permitted for nothing rather than naming the argument that was wrong. The asymmetric branch of `generateKey` partitions the usages before either half is constructed and `filter` treats a non-list the same way, so it is checked at that verb's own door and the whole pair is refused.
|
|
44
|
+
- The AES-GCM ciphertext and authentication tag are split with a captured `subarray`, so a replaced one cannot hand the tag check a different range of the same buffer.
|
|
45
|
+
- A JWK thumbprint (RFC 7638 sec. 3.3) is built with a captured serializer. The canonical JSON is the thumbprint's preimage, so a replaced `JSON.stringify` changes the identity every consumer compares keys by.
|
|
46
|
+
- The JOSE header checks -- own-name reads, the `crit` list walk, the embedded-JWK shape test -- and the protected-header serialization all run on captured operations, so a caller who reaches the header through an accessor cannot decide which of them apply.
|
|
47
|
+
- A digest runs under the algorithm the caller named. The hash name is case-folded to pick its registry row, and dispatched live a replaced `String.prototype.toUpperCase` answering `SHA-1` gave a caller who asked for SHA-256 a 20-byte digest. The same fold picks the HMAC block size and decides whether a key's algorithm matches the operation's.
|
|
48
|
+
- `getRandomValues` settles the kinds WebCrypto refuses from the value's internal slot rather than by `instanceof` against a replaceable global, so a real `Float32Array` cannot pass a check written to reject it by swapping the constructor the comparison names.
|
|
49
|
+
|
|
7
50
|
## v0.5.18 — 2026-08-20
|
|
8
51
|
|
|
9
52
|
Every producing verb refuses an authoring field it does not read, so a misspelling is a refusal at the call instead of a signed artifact that quietly omits what was asked for.
|
package/lib/cms-verify.js
CHANGED
|
@@ -39,7 +39,9 @@ var cmsEncrypt = require("./cms-encrypt");
|
|
|
39
39
|
var cmsDecrypt = require("./cms-decrypt");
|
|
40
40
|
var cmsCompress = require("./cms-compress");
|
|
41
41
|
var signScheme = require("./sign-scheme");
|
|
42
|
-
|
|
42
|
+
// The digest-strength question is asked of sign-scheme, which owns the policy table, so the signer
|
|
43
|
+
// and this verifier cannot drift (RFC 9882 sec. 3.3).
|
|
44
|
+
var _mldsaDigestSuitable = signScheme.mldsaDigestSuitable;
|
|
43
45
|
var SLHDSA_BY_OID = signScheme.SLHDSA_BY_OID; // shared SLH-DSA set -> { wc, digest } (RFC 9814 sec. 4 pinned digest)
|
|
44
46
|
var validator = require("./validator-all");
|
|
45
47
|
var compositeSig = require("./composite-sig");
|
|
@@ -71,7 +73,7 @@ var DIGEST_HASH = {
|
|
|
71
73
|
// foreign fault and loses this module's own unsupported-algorithm verdict.
|
|
72
74
|
var SIG_HASH = { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
73
75
|
// Is `name` a message-digest algorithm this verifier supports?
|
|
74
|
-
function _supportedDigest(name) { return !!DIGEST_HASH[name]; }
|
|
76
|
+
function _supportedDigest(name) { return guard.intrinsic.hasOwn(DIGEST_HASH, name) && !!DIGEST_HASH[name]; }
|
|
75
77
|
// The digest of `content` under the named algorithm, resolved to a Buffer.
|
|
76
78
|
function _computeDigest(name, content) {
|
|
77
79
|
return subtle.digest(DIGEST_HASH[name], content).then(function (d) { return Buffer.from(d); });
|
|
@@ -366,7 +368,7 @@ function _verifyOne(si, content, eContentType, parsedCerts, csTarget) {
|
|
|
366
368
|
// meet the parameter set's security strength -- a below-strength digest is the weaker link and is
|
|
367
369
|
// rejected fail-closed (the RFC's "verifiers MAY reject", taken by the strict-verifier posture).
|
|
368
370
|
// With signed attributes absent the digestAlgorithm has no meaning (sec. 3.3) and is not checked.
|
|
369
|
-
if (scheme.kind === "mldsa" && si.signedAttrsBytes && !
|
|
371
|
+
if (scheme.kind === "mldsa" && si.signedAttrsBytes && !_mldsaDigestSuitable(scheme.name, si.digestAlgorithm.name)) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.digestAlgorithm.name + " message digest is below the security strength of " + scheme.name + " (RFC 9882 sec. 3.3)" });
|
|
370
372
|
// RFC 9814 sec. 4: an SLH-DSA SignerInfo's message-digest algorithm is the one paired with the
|
|
371
373
|
// parameter set (the sec. 4 list, always at least twice the tree-hash size); the signer emits it
|
|
372
374
|
// and this strict verifier requires it, so a digest that does not match the set's paired hash fails
|
|
@@ -768,7 +770,9 @@ function _verify(input, opts) {
|
|
|
768
770
|
// message a second time to learn it, and a check that needs a second parse is a check most
|
|
769
771
|
// callers will not write.
|
|
770
772
|
var res = {
|
|
771
|
-
|
|
773
|
+
// Signature soundness is decided by comparison for the same reason the trust verdict below
|
|
774
|
+
// is: `every` replaced after load reports the whole set sound without reading one signer.
|
|
775
|
+
valid: signers.length > 0 && guard.list.allMatch(signers, function (s) { return s.ok === true; }),
|
|
772
776
|
eContentType: parsed.encapContentInfo.eContentType,
|
|
773
777
|
signers: signers,
|
|
774
778
|
};
|
|
@@ -873,6 +877,12 @@ function _keyUsagePermitsSigning(parsedCerts, der) {
|
|
|
873
877
|
return true; // absent: unconstrained (sec. 4.2.1.3)
|
|
874
878
|
}
|
|
875
879
|
|
|
880
|
+
// Every signer trusted? The answer IS the message-level trust verdict, so it is decided by
|
|
881
|
+
// comparison instead of by an `every`. See guard-list.
|
|
882
|
+
function _allTrusted(signers) {
|
|
883
|
+
return guard.list.allMatch(signers, function (s) { return s.trusted === true; });
|
|
884
|
+
}
|
|
885
|
+
|
|
876
886
|
// `cfg` is verify's SNAPSHOT, never the caller's options object -- the parameter is named for that
|
|
877
887
|
// so a future caller cannot hand it the live one without noticing.
|
|
878
888
|
function _applyTrust(res, parsedCerts, cfg) {
|
|
@@ -963,7 +973,10 @@ function _applyTrust(res, parsedCerts, cfg) {
|
|
|
963
973
|
});
|
|
964
974
|
});
|
|
965
975
|
}, Promise.resolve()).then(function () {
|
|
966
|
-
|
|
976
|
+
// The trust verdict is decided by comparison, not by a prototype method: `every` replaced after
|
|
977
|
+
// load answers true without consulting a single signer, and a message no anchor covers reports
|
|
978
|
+
// trusted:true.
|
|
979
|
+
res.trusted = res.valid && res.signers.length > 0 && _allTrusted(res.signers);
|
|
967
980
|
});
|
|
968
981
|
}
|
|
969
982
|
// Parse a candidate cert DER and index its SKI; a cert that will not parse is skipped (it
|
package/lib/guard-all.js
CHANGED
|
@@ -55,6 +55,7 @@ var bytes = require("./guard-bytes");
|
|
|
55
55
|
var text = require("./guard-text");
|
|
56
56
|
var limits = require("./guard-limits");
|
|
57
57
|
var crypto = require("./guard-crypto");
|
|
58
|
+
var list = require("./guard-list");
|
|
58
59
|
var range = require("./guard-range");
|
|
59
60
|
var time = require("./guard-time");
|
|
60
61
|
var name = require("./guard-name");
|
|
@@ -74,6 +75,7 @@ module.exports = {
|
|
|
74
75
|
text: text,
|
|
75
76
|
limits: limits,
|
|
76
77
|
crypto: crypto,
|
|
78
|
+
list: list,
|
|
77
79
|
range: range,
|
|
78
80
|
time: time,
|
|
79
81
|
name: name,
|
package/lib/guard-intrinsic.js
CHANGED
|
@@ -84,6 +84,10 @@ var _jsonStringify = JSON.stringify;
|
|
|
84
84
|
var _mathFloor = Math.floor;
|
|
85
85
|
var _mathCeil = Math.ceil;
|
|
86
86
|
var _mathMin = Math.min;
|
|
87
|
+
var _mathMax = Math.max;
|
|
88
|
+
// A numeric parse a bound is then compared against: one answering a larger value widens the bound
|
|
89
|
+
// the comparison was written to enforce.
|
|
90
|
+
var _parseInt = parseInt;
|
|
87
91
|
var _promiseResolve = Promise.resolve;
|
|
88
92
|
var _promiseReject = Promise.reject;
|
|
89
93
|
|
|
@@ -102,6 +106,19 @@ var _arrSort = uncurry(Array.prototype.sort);
|
|
|
102
106
|
var _arrConcat = uncurry(Array.prototype.concat);
|
|
103
107
|
var _arrSlice = uncurry(Array.prototype.slice);
|
|
104
108
|
var _arrJoin = uncurry(Array.prototype.join);
|
|
109
|
+
var _arrPush = uncurry(Array.prototype.push);
|
|
110
|
+
|
|
111
|
+
// The string and byte-view operations a decision is then made on. A `toUpperCase` or `toLowerCase`
|
|
112
|
+
// replaced after load changes which registry row an algorithm name resolves to; a `subarray`
|
|
113
|
+
// returning a different window hands the next step bytes from somewhere else in the same buffer;
|
|
114
|
+
// and a radix conversion is how a key's own numbers are read back before they are compared.
|
|
115
|
+
var _strToUpperCase = uncurry(String.prototype.toUpperCase);
|
|
116
|
+
var _strToLowerCase = uncurry(String.prototype.toLowerCase);
|
|
117
|
+
var _strIndexOf = uncurry(String.prototype.indexOf);
|
|
118
|
+
var _taSubarray = uncurry(Uint8Array.prototype.subarray);
|
|
119
|
+
var _abSlice = uncurry(ArrayBuffer.prototype.slice);
|
|
120
|
+
var _numToString = uncurry(Number.prototype.toString);
|
|
121
|
+
var _bigIntToString = uncurry(BigInt.prototype.toString);
|
|
105
122
|
|
|
106
123
|
// The collection operations, uncurried. A guard's cycle-safe chain walk is a `Set` it owns, and
|
|
107
124
|
// `has` is what stops it: one answering `true` ends the walk at the first prototype, so a scan that
|
|
@@ -270,6 +287,16 @@ module.exports = {
|
|
|
270
287
|
floor: _mathFloor,
|
|
271
288
|
ceil: _mathCeil,
|
|
272
289
|
min: _mathMin,
|
|
290
|
+
max: _mathMax,
|
|
291
|
+
parseInt: _parseInt,
|
|
292
|
+
push: _arrPush,
|
|
293
|
+
toUpperCase: _strToUpperCase,
|
|
294
|
+
toLowerCase: _strToLowerCase,
|
|
295
|
+
stringIndexOf: _strIndexOf,
|
|
296
|
+
subarray: _taSubarray,
|
|
297
|
+
arrayBufferSlice: _abSlice,
|
|
298
|
+
numberToString: _numToString,
|
|
299
|
+
bigIntToString: _bigIntToString,
|
|
273
300
|
promiseResolve: _promiseResolve,
|
|
274
301
|
promiseReject: _promiseReject,
|
|
275
302
|
forEach: _arrForEach,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
//
|
|
5
|
+
// @internal, so no operator-facing namespace. The documented surface is every verdict whose rule
|
|
6
|
+
// composes this guard: pki.path.validate (requiredEku, the policy tree), pki.cms.verify (the
|
|
7
|
+
// message trust verdict), the OCSP delegated-responder check, the TPM AIK purpose check, and the
|
|
8
|
+
// timestamp unknown-critical-extension gate.
|
|
9
|
+
//
|
|
10
|
+
// guard-list, membership decided by comparison and never by a prototype method.
|
|
11
|
+
//
|
|
12
|
+
// Defends the intrinsic-substitution class (CWE-349 / CWE-693: a control the caller can neutralize).
|
|
13
|
+
// Written `list.indexOf(v) !== -1` or `list.some(fn)`, a membership test asks the runtime, at the
|
|
14
|
+
// moment of the call, which function `indexOf` or `some` is. A caller who receives control after
|
|
15
|
+
// this package loads, through an option accessor or a `toJSON` or while a verb is suspended on an
|
|
16
|
+
// await, can replace it. The answer becomes whatever they chose, while every check around it still
|
|
17
|
+
// runs and still passes.
|
|
18
|
+
//
|
|
19
|
+
// The distinction that matters is what the answer IS. A scan over a local accumulator produces a
|
|
20
|
+
// value. A scan that decides whether a certificate carries a purpose, whether a policy is expected,
|
|
21
|
+
// or whether every signer is trusted IS the rule. Measured on the tree before this guard existed:
|
|
22
|
+
// with `Array.prototype.indexOf` replaced to report every value present, a certificate restricted
|
|
23
|
+
// to emailProtection satisfied a requiredEku of serverAuth, and pki.path.validate went from
|
|
24
|
+
// valid:false to valid:true.
|
|
25
|
+
//
|
|
26
|
+
// Capturing the method at load (guard-intrinsic) is the other half of the same defense, and is what
|
|
27
|
+
// a module already inside that discipline uses. This guard is for the rule sites themselves. A loop
|
|
28
|
+
// over `===` has nothing to replace, so it holds whatever the prototype says, in a module that has
|
|
29
|
+
// been swept and in one that has not.
|
|
30
|
+
|
|
31
|
+
// contains(list, value) -> boolean. Strict-equality membership, the `indexOf(v) !== -1` rule.
|
|
32
|
+
// A null / undefined list is honestly empty, never a throw: these run on verdict paths.
|
|
33
|
+
// @enforced-by behavioral -- `.indexOf(x) !== -1` has no rename-proof form that separates a
|
|
34
|
+
// membership RULE from its legitimate siblings: the identical token is a substring search
|
|
35
|
+
// (`authority.indexOf(":")`, `addr.indexOf("@")`) and a local index lookup, and a shape matching
|
|
36
|
+
// all of them fired on 37 sites of which most were strings. What makes a site this guard's is
|
|
37
|
+
// what the answer IS -- a decision about a certificate, a policy or a signer -- and that is a
|
|
38
|
+
// judgment no regex makes. The RED vectors are the guard: a codeSigning-only certificate still
|
|
39
|
+
// fails a serverAuth requiredEku, and an unauthorized responder is still refused, each with
|
|
40
|
+
// Array.prototype.indexOf replaced after load.
|
|
41
|
+
function contains(list, value) {
|
|
42
|
+
if (!list) return false;
|
|
43
|
+
for (var i = 0, n = list.length; i < n; i++) if (list[i] === value) return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// containsAll(list, values) -> boolean. Every one of `values` present in `list` -- the shape a
|
|
48
|
+
// "these purposes are all required" rule needs. No `values` is vacuously satisfied.
|
|
49
|
+
// @enforced-by behavioral -- the composition of contains() has no rename-proof shape of its own;
|
|
50
|
+
// the RED vectors (a codeSigning-only certificate still fails a serverAuth requiredEku with
|
|
51
|
+
// Array.prototype.indexOf replaced) are the guard.
|
|
52
|
+
function containsAll(list, values) {
|
|
53
|
+
if (!values) return true;
|
|
54
|
+
for (var i = 0, n = values.length; i < n; i++) if (!contains(list, values[i])) return false;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// anyMatches(list, predicate) -> boolean. The `some(fn)` rule: does any member satisfy it? The
|
|
59
|
+
// predicate is the caller's own function, so what this guard removes is the DISPATCH, not the test.
|
|
60
|
+
// @enforced-by behavioral -- `.some(` has legitimate non-rule siblings (building a list, reporting
|
|
61
|
+
// a count), so a blanket detector would false-fire; the RED vectors (a critical extension is
|
|
62
|
+
// still seen, an untrusted signer still reported) are the guard.
|
|
63
|
+
function anyMatches(list, predicate) {
|
|
64
|
+
if (!list) return false;
|
|
65
|
+
for (var i = 0, n = list.length; i < n; i++) if (predicate(list[i], i)) return true;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// allMatch(list, predicate) -> boolean. The `every(fn)` rule, same reasoning as anyMatches. An
|
|
70
|
+
// empty list is vacuously true, matching Array.prototype.every -- a caller that needs "non-empty
|
|
71
|
+
// AND all" tests the length itself, as pki.cms.verify does for the trust verdict.
|
|
72
|
+
// @enforced-by behavioral -- see anyMatches; the RED vector (an untrusted signer keeps
|
|
73
|
+
// trusted:false with Array.prototype.every replaced) is the guard.
|
|
74
|
+
function allMatch(list, predicate) {
|
|
75
|
+
if (!list) return true;
|
|
76
|
+
for (var i = 0, n = list.length; i < n; i++) if (!predicate(list[i], i)) return false;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
contains: contains,
|
|
82
|
+
containsAll: containsAll,
|
|
83
|
+
anyMatches: anyMatches,
|
|
84
|
+
allMatch: allMatch,
|
|
85
|
+
};
|
package/lib/jose.js
CHANGED
|
@@ -42,6 +42,24 @@ var webcrypto = wcEngine.webcrypto;
|
|
|
42
42
|
var frameworkError = require("./framework-error");
|
|
43
43
|
var guard = require("./guard-all");
|
|
44
44
|
var edwardsPoint = require("./edwards-point");
|
|
45
|
+
// What this module reads from the runtime while it is deciding something, taken at load. A JWS is
|
|
46
|
+
// signed and verified against a header the caller owns, and the reads below are the ones that decide
|
|
47
|
+
// what that header IS: whether a value is a list, whether a name is the caller's own, which
|
|
48
|
+
// conversion turns a claim into the string compared against a profile. A caller who receives control
|
|
49
|
+
// afterwards -- through an accessor on the header, or a `toJSON` while it is being serialized -- can
|
|
50
|
+
// replace any of them, and every check around them still runs and still passes. See guard-intrinsic.
|
|
51
|
+
var intrinsic = require("./guard-intrinsic");
|
|
52
|
+
var _isArray = intrinsic.isArray;
|
|
53
|
+
var _hasOwn = intrinsic.hasOwn;
|
|
54
|
+
var _keys = intrinsic.keys;
|
|
55
|
+
var _isBuffer = intrinsic.isBuffer;
|
|
56
|
+
var _bufferFrom = intrinsic.bufferFrom;
|
|
57
|
+
var _stringify = intrinsic.stringify;
|
|
58
|
+
var _ObjectProto = intrinsic.ObjectProto;
|
|
59
|
+
var _push = intrinsic.uncurry(Array.prototype.push);
|
|
60
|
+
var _join = intrinsic.join;
|
|
61
|
+
var _map = intrinsic.map;
|
|
62
|
+
var _bufToString = intrinsic.uncurry(Buffer.prototype.toString);
|
|
45
63
|
|
|
46
64
|
var JoseError = frameworkError.JoseError;
|
|
47
65
|
function E(code, message, cause) { return new JoseError(code, message, cause); }
|
|
@@ -65,11 +83,11 @@ var LIMITS = constants.LIMITS;
|
|
|
65
83
|
* pki.jose.base64url.encode(Buffer.from([1, 2, 3])); // -> "AQID"
|
|
66
84
|
*/
|
|
67
85
|
function b64uEncode(bytes) {
|
|
68
|
-
if (!
|
|
86
|
+
if (!_isBuffer(bytes)) throw E("jose/bad-input", "base64url.encode requires a Buffer");
|
|
69
87
|
// Re-view through the byte guard: a detached-backed Buffer reads as zero-length,
|
|
70
88
|
// so a bare .toString would silently encode "" -- fail closed instead.
|
|
71
89
|
bytes = guard.bytes.view(bytes, JoseError, "jose/bad-input", "base64url.encode input");
|
|
72
|
-
return bytes
|
|
90
|
+
return _bufToString(bytes, "base64url");
|
|
73
91
|
}
|
|
74
92
|
|
|
75
93
|
/**
|
|
@@ -182,19 +200,19 @@ var PROFILES = {
|
|
|
182
200
|
// { alg, algRow, header }; throws a typed jose/* fault on any violation.
|
|
183
201
|
function _checkHeader(header, profileName) {
|
|
184
202
|
var profile = PROFILES[profileName];
|
|
185
|
-
if (!profile) throw E("jose/bad-input", "unknown JWS profile " +
|
|
186
|
-
if (!header || typeof header !== "object" ||
|
|
203
|
+
if (!profile) throw E("jose/bad-input", "unknown JWS profile " + _stringify(profileName));
|
|
204
|
+
if (!header || typeof header !== "object" || _isArray(header)) throw E("jose/bad-header", "the protected header must be a JSON object");
|
|
187
205
|
// Every field is read as an OWN property. JSON.stringify (used by sign) omits
|
|
188
206
|
// inherited members, so an alg/nonce/url living on the prototype (a polluted or
|
|
189
207
|
// Object.create(...) header) would pass here yet serialize to a header MISSING
|
|
190
208
|
// them -- so require own-ness, matching what the serialized JWS will actually carry.
|
|
191
|
-
var own = function (k) { return
|
|
209
|
+
var own = function (k) { return _hasOwn(header, k); };
|
|
192
210
|
// b64 (RFC 7797) MUST NOT be present at all -- the unencoded-payload downgrade.
|
|
193
211
|
if (own("b64")) throw E("jose/bad-jws", "the RFC 7797 b64 header parameter is forbidden");
|
|
194
212
|
var alg = header.alg;
|
|
195
213
|
if (!own("alg") || typeof alg !== "string") throw E("jose/bad-header", "the protected header must carry a string alg");
|
|
196
214
|
var algRow = profile.algs[alg];
|
|
197
|
-
if (!algRow) throw E("jose/bad-alg", "unsupported or forbidden algorithm " +
|
|
215
|
+
if (!algRow) throw E("jose/bad-alg", "unsupported or forbidden algorithm " + _stringify(alg) + " for this JWS profile");
|
|
198
216
|
if (profile.requireUrl && (!own("url") || typeof header.url !== "string" || header.url.length === 0)) throw E("jose/bad-header", "the protected header must carry a non-empty string url");
|
|
199
217
|
// nonce discipline.
|
|
200
218
|
var hasNonce = own("nonce");
|
|
@@ -219,20 +237,20 @@ function _checkHeader(header, profileName) {
|
|
|
219
237
|
} else if (profile.keyId === "jwk") {
|
|
220
238
|
if (!hasJwk || hasKid) throw E("jose/bad-header", "this JWS must carry an embedded jwk, not kid");
|
|
221
239
|
}
|
|
222
|
-
if (hasJwk && (!header.jwk || typeof header.jwk !== "object" ||
|
|
240
|
+
if (hasJwk && (!header.jwk || typeof header.jwk !== "object" || _isArray(header.jwk))) throw E("jose/bad-header", "jwk must be a JSON object");
|
|
223
241
|
// crit: protected-only, non-empty, no standard names, no duplicates, all understood.
|
|
224
|
-
if (
|
|
242
|
+
if (_hasOwn(header, "crit")) {
|
|
225
243
|
var crit = header.crit;
|
|
226
|
-
if (!
|
|
244
|
+
if (!_isArray(crit) || crit.length === 0) throw E("jose/bad-crit", "crit must be a non-empty array");
|
|
227
245
|
var seen = {};
|
|
228
246
|
for (var c = 0; c < crit.length; c++) {
|
|
229
247
|
var name = crit[c];
|
|
230
248
|
if (typeof name !== "string") throw E("jose/bad-crit", "crit entries must be strings");
|
|
231
|
-
if (
|
|
249
|
+
if (_hasOwn(seen, name)) throw E("jose/bad-crit", "duplicate crit entry " + _stringify(name));
|
|
232
250
|
seen[name] = 1;
|
|
233
|
-
if (UNDERSTOOD_HEADER[name]) throw E("jose/bad-crit", "crit must not name a standard header parameter " +
|
|
251
|
+
if (UNDERSTOOD_HEADER[name]) throw E("jose/bad-crit", "crit must not name a standard header parameter " + _stringify(name));
|
|
234
252
|
// Anything else is an extension parameter this implementation does not process.
|
|
235
|
-
throw E("jose/bad-crit", "unprocessed critical header parameter " +
|
|
253
|
+
throw E("jose/bad-crit", "unprocessed critical header parameter " + _stringify(name));
|
|
236
254
|
}
|
|
237
255
|
}
|
|
238
256
|
return { alg: alg, algRow: algRow, header: header };
|
|
@@ -267,9 +285,9 @@ function _cryptoAlg(algRow, jwk, key) {
|
|
|
267
285
|
// The key type the JWK MUST have for this alg (fail closed on confusion).
|
|
268
286
|
function _assertKeyType(algRow, jwk) {
|
|
269
287
|
if (!jwk || typeof jwk !== "object" || typeof jwk.kty !== "string") throw E("jose/bad-key", "a JWK object with a kty is required");
|
|
270
|
-
if (jwk.kty !== algRow.kty) throw E("jose/bad-alg", "the key type " +
|
|
288
|
+
if (jwk.kty !== algRow.kty) throw E("jose/bad-alg", "the key type " + _stringify(jwk.kty) + " does not match the algorithm");
|
|
271
289
|
if (algRow.kty === "EC" && jwk.crv !== algRow.crv) throw E("jose/bad-alg", "the EC curve does not match the algorithm");
|
|
272
|
-
if (algRow.kty === "OKP" && jwk.crv !== "Ed25519" && jwk.crv !== "Ed448") throw E("jose/bad-alg", "unsupported OKP curve " +
|
|
290
|
+
if (algRow.kty === "OKP" && jwk.crv !== "Ed25519" && jwk.crv !== "Ed448") throw E("jose/bad-alg", "unsupported OKP curve " + _stringify(jwk.crv));
|
|
273
291
|
// node/OpenSSL import a low-order (e.g. identity) OKP point without complaint and it verifies a
|
|
274
292
|
// forged signature; reject a non-full-order Edwards point through the shared gate, the same check
|
|
275
293
|
// every EdDSA verify path in the toolkit applies.
|
|
@@ -278,7 +296,7 @@ function _assertKeyType(algRow, jwk) {
|
|
|
278
296
|
}
|
|
279
297
|
// An AKP (ML-DSA) JWK carries its parameter set in `alg` (RFC 9964); it MUST match
|
|
280
298
|
// the algorithm's set, or an ML-DSA-44 key could embed an ML-DSA-65 public JWK.
|
|
281
|
-
if (algRow.kty === "AKP" && jwk.alg !== algRow.subtle) throw E("jose/bad-alg", "the AKP parameter set " +
|
|
299
|
+
if (algRow.kty === "AKP" && jwk.alg !== algRow.subtle) throw E("jose/bad-alg", "the AKP parameter set " + _stringify(jwk.alg) + " does not match the algorithm " + algRow.subtle);
|
|
282
300
|
}
|
|
283
301
|
|
|
284
302
|
// The exact signature byte length this alg + key must produce.
|
|
@@ -317,10 +335,10 @@ var PRIVATE_JWK_MEMBERS = ["d", "p", "q", "dp", "dq", "qi", "k", "priv"];
|
|
|
317
335
|
* pki.jose.assertPublicJwk({ kty: "EC", crv: "P-256", x: "...", y: "..." });
|
|
318
336
|
*/
|
|
319
337
|
function assertPublicJwk(jwk) {
|
|
320
|
-
if (!jwk || typeof jwk !== "object" ||
|
|
338
|
+
if (!jwk || typeof jwk !== "object" || _isArray(jwk)) throw E("jose/bad-key", "a JWK object is required");
|
|
321
339
|
for (var i = 0; i < PRIVATE_JWK_MEMBERS.length; i++) {
|
|
322
|
-
if (
|
|
323
|
-
throw E("jose/private-key-material", "a published JWK must be public-only; it carries the private member " +
|
|
340
|
+
if (_hasOwn(jwk, PRIVATE_JWK_MEMBERS[i])) {
|
|
341
|
+
throw E("jose/private-key-material", "a published JWK must be public-only; it carries the private member " + _stringify(PRIVATE_JWK_MEMBERS[i]));
|
|
324
342
|
}
|
|
325
343
|
}
|
|
326
344
|
return jwk;
|
|
@@ -374,9 +392,9 @@ var _VERIFY_KEYS = { key: 1, profile: 1 };
|
|
|
374
392
|
async function verify(jws, opts) {
|
|
375
393
|
opts = opts || {};
|
|
376
394
|
guard.identifier.assertKnownKeys(opts, _VERIFY_KEYS, E, "jose/bad-input", "unknown pki.jose.verify option ");
|
|
377
|
-
if (!jws || typeof jws !== "object" ||
|
|
378
|
-
if (
|
|
379
|
-
if (
|
|
395
|
+
if (!jws || typeof jws !== "object" || _isArray(jws)) throw E("jose/bad-jws", "a flattened JWS object is required");
|
|
396
|
+
if (_hasOwn(jws, "signatures")) throw E("jose/bad-jws", "the multi-signature signatures member is forbidden");
|
|
397
|
+
if (_hasOwn(jws, "header")) throw E("jose/bad-jws", "the unprotected header member is forbidden");
|
|
380
398
|
if (typeof jws.protected !== "string" || typeof jws.signature !== "string") throw E("jose/bad-jws", "protected and signature must be base64url strings");
|
|
381
399
|
if (typeof jws.payload !== "string") throw E("jose/bad-jws", "payload must be a base64url string (never detached)");
|
|
382
400
|
var header = parseJson(b64uDecode(jws.protected));
|
|
@@ -393,7 +411,7 @@ async function verify(jws, opts) {
|
|
|
393
411
|
// matters most -- a `key` that came back null from a lookup would verify against whatever the
|
|
394
412
|
// message carried, and the verdict would report the embedded key as though none was named.
|
|
395
413
|
// `undefined` alone means "not supplied", so spreading an options object stays safe.
|
|
396
|
-
if (opts.key !== undefined && (typeof opts.key !== "object" || opts.key === null ||
|
|
414
|
+
if (opts.key !== undefined && (typeof opts.key !== "object" || opts.key === null || _isArray(opts.key))) {
|
|
397
415
|
throw E("jose/bad-key", "opts.key was supplied but is not a JWK object, so the key this message must be signed under cannot be established");
|
|
398
416
|
}
|
|
399
417
|
var jwk = header.jwk || opts.key;
|
|
@@ -419,7 +437,7 @@ async function verify(jws, opts) {
|
|
|
419
437
|
var want = _expectedSigBytes(checked.algRow, jwk);
|
|
420
438
|
if (want != null && sig.length !== want) throw E("jose/bad-signature", "the signature length " + sig.length + " is not the " + want + " bytes " + checked.alg + " requires");
|
|
421
439
|
// Signing input from the RECEIVED base64url strings verbatim (never a re-serialize).
|
|
422
|
-
var signingInput =
|
|
440
|
+
var signingInput = _bufferFrom(jws.protected + "." + jws.payload, "ascii");
|
|
423
441
|
var key;
|
|
424
442
|
try { key = await webcrypto.subtle.importKey("jwk", jwk, _importParams(checked.algRow, jwk), false, ["verify"]); }
|
|
425
443
|
catch (e) { throw E("jose/bad-key", "the JWK could not be imported for verification", e); }
|
|
@@ -483,23 +501,23 @@ async function sign(opts) {
|
|
|
483
501
|
// those questions separately -- assertPublicJwk sees a public JWK while the bytes that ship carry
|
|
484
502
|
// the private one, and the door sees a jwk-mode header while the signer sees a kid-mode one.
|
|
485
503
|
var headerJson;
|
|
486
|
-
try { headerJson =
|
|
504
|
+
try { headerJson = _stringify(opts.protected); }
|
|
487
505
|
catch (e) { throw E("jose/bad-header", "the protected header must be JSON-serializable", e); }
|
|
488
506
|
// Read back through the same bounded reader a received JWS goes through, so a header this side
|
|
489
507
|
// produces is held to the caps and the duplicate-member rule the other side enforces.
|
|
490
|
-
var headerBytes = headerJson === undefined ? null :
|
|
508
|
+
var headerBytes = headerJson === undefined ? null : _bufferFrom(headerJson, "utf8");
|
|
491
509
|
var header = headerBytes === null ? opts.protected : parseJson(headerBytes);
|
|
492
510
|
// Which of the two key sources is in force is known only once the header has been read, so the
|
|
493
511
|
// narrower table runs here. It re-walks the bag, which also catches a name that code added.
|
|
494
512
|
var embedsJwk = !!(header && typeof header === "object" && header.jwk);
|
|
495
513
|
if (embedsJwk) {
|
|
496
514
|
guard.identifier.assertKnownKeys(opts, _SIGN_KEYS_EMBEDDED_JWK, E, "jose/bad-input", function (k) {
|
|
497
|
-
return "unknown pki.jose.sign option " +
|
|
515
|
+
return "unknown pki.jose.sign option " + _stringify(k) +
|
|
498
516
|
(k === "jwk" ? "; the protected header already embeds the public JWK" : "");
|
|
499
517
|
});
|
|
500
518
|
}
|
|
501
519
|
var payload = opts.payload;
|
|
502
|
-
if (!
|
|
520
|
+
if (!_isBuffer(payload)) throw E("jose/bad-input", "payload must be a Buffer (empty Buffer for POST-as-GET)");
|
|
503
521
|
// Re-view before the length===0 fast-path: a DETACHED payload reads as zero-length
|
|
504
522
|
// and would be silently signed as an empty POST-as-GET body -- fail closed instead.
|
|
505
523
|
payload = guard.bytes.view(payload, JoseError, "jose/bad-input", "payload");
|
|
@@ -523,14 +541,14 @@ async function sign(opts) {
|
|
|
523
541
|
if (header.jwk) { _assertKeyType(checked.algRow, header.jwk); assertPublicJwk(header.jwk); }
|
|
524
542
|
var protectedB64 = b64uEncode(headerBytes); // the exact bytes every check above read
|
|
525
543
|
var payloadB64 = payload.length === 0 ? "" : b64uEncode(payload);
|
|
526
|
-
var signingInput =
|
|
544
|
+
var signingInput = _bufferFrom(protectedB64 + "." + payloadB64, "ascii");
|
|
527
545
|
var jwk = header.jwk || opts.jwk || {};
|
|
528
546
|
// opts.key is documented as a CryptoKey, so one created outside this engine signs here too: it
|
|
529
547
|
// carries none of the material this engine signs with, and is re-imported through it rather than
|
|
530
548
|
// reaching subtle.sign as a key it cannot read. A key whose material this process cannot reach at
|
|
531
549
|
// all -- non-extractable, or held behind another implementation's interface -- is refused there.
|
|
532
550
|
var signKey = await wcEngine.adoptKey(opts.key, null, ["sign"], E, "jose/bad-input");
|
|
533
|
-
var sigBuf =
|
|
551
|
+
var sigBuf = _bufferFrom(await webcrypto.subtle.sign(_cryptoAlg(checked.algRow, jwk, signKey), signKey, signingInput));
|
|
534
552
|
// Pin the produced signature length the same way verify does. A key whose curve /
|
|
535
553
|
// hash / modulus does not match the header alg (a P-384 key under ES256, an
|
|
536
554
|
// HS512 key under HS256) still signs, but emits the wrong length -- caught here
|
|
@@ -590,15 +608,17 @@ var THUMBPRINT_MEMBERS = {
|
|
|
590
608
|
async function thumbprint(jwk) {
|
|
591
609
|
if (!jwk || typeof jwk !== "object" || typeof jwk.kty !== "string") throw E("jose/bad-key", "a JWK object with a kty is required");
|
|
592
610
|
var members = THUMBPRINT_MEMBERS[jwk.kty];
|
|
593
|
-
if (!members) throw E("jose/bad-key", "unsupported JWK key type " +
|
|
611
|
+
if (!members) throw E("jose/bad-key", "unsupported JWK key type " + _stringify(jwk.kty));
|
|
594
612
|
var parts = [];
|
|
595
613
|
for (var i = 0; i < members.length; i++) {
|
|
596
614
|
var m = members[i];
|
|
597
|
-
if (typeof jwk[m] !== "string") throw E("jose/bad-key", "the JWK is missing the required member " +
|
|
598
|
-
|
|
615
|
+
if (typeof jwk[m] !== "string") throw E("jose/bad-key", "the JWK is missing the required member " + _stringify(m));
|
|
616
|
+
// The canonical form IS the thumbprint preimage (RFC 7638 sec. 3.3), so the serializer that
|
|
617
|
+
// builds it is the captured one: a replacement changes the identity every consumer compares.
|
|
618
|
+
_push(parts, _stringify(m) + ":" + _stringify(jwk[m]));
|
|
599
619
|
}
|
|
600
|
-
var canonical = "{" + parts
|
|
601
|
-
var digest =
|
|
620
|
+
var canonical = "{" + _join(parts, ",") + "}";
|
|
621
|
+
var digest = _bufferFrom(await webcrypto.subtle.digest("SHA-256", _bufferFrom(canonical, "utf8")));
|
|
602
622
|
return b64uEncode(digest);
|
|
603
623
|
}
|
|
604
624
|
|
|
@@ -630,7 +650,7 @@ async function thumbprint(jwk) {
|
|
|
630
650
|
* pss.map(function (r) { return r.alg; }); // -> ["PS256", "PS384", "PS512"]
|
|
631
651
|
*/
|
|
632
652
|
function sigAlgs() {
|
|
633
|
-
return
|
|
653
|
+
return _map(_keys(SIG_ALGS), function (alg) {
|
|
634
654
|
var row = SIG_ALGS[alg];
|
|
635
655
|
var out = { alg: alg, kty: row.kty };
|
|
636
656
|
if (row.crv) out.crv = row.crv;
|
package/lib/key.js
CHANGED
|
@@ -305,6 +305,10 @@ async function import_(input, opts) {
|
|
|
305
305
|
}
|
|
306
306
|
var algorithm = opts.algorithm != null ? opts.algorithm : _inferAlgorithm(detected);
|
|
307
307
|
var isPublic = detected.format === "spki";
|
|
308
|
+
// Checked at this verb's own door rather than left for the engine to discover: a non-list reached
|
|
309
|
+
// the CryptoKey copy, where `Array.prototype.slice` on a number yields an empty list, so the key
|
|
310
|
+
// was minted permitted for NOTHING instead of the call naming the argument that was wrong.
|
|
311
|
+
if (opts.usages != null && !Array.isArray(opts.usages)) throw _err("key/bad-input", "opts.usages must be an array of key-usage strings");
|
|
308
312
|
var usages = opts.usages || _importUsages(_algName(algorithm), isPublic);
|
|
309
313
|
var extractable = opts.extractable != null ? opts.extractable : false;
|
|
310
314
|
try {
|
|
@@ -344,6 +348,8 @@ async function generate(algorithm, opts) {
|
|
|
344
348
|
guard.identifier.assertKnownKeys(opts, _GENERATE_OPTS, _err, "key/bad-input",
|
|
345
349
|
"pki.key.generate has an unknown option (the WebCrypto spelling is `extractable`): ");
|
|
346
350
|
var extractable = opts.extractable != null ? opts.extractable : true;
|
|
351
|
+
// The same door as import: a non-list is named here rather than becoming an empty usage set.
|
|
352
|
+
if (opts.usages != null && !Array.isArray(opts.usages)) throw _err("key/bad-input", "opts.usages must be an array of key-usage strings");
|
|
347
353
|
var usages = opts.usages || _generateUsages(_algName(algorithm));
|
|
348
354
|
var pair;
|
|
349
355
|
try { pair = await subtle.generateKey(algorithm, extractable, usages); }
|
package/lib/ocsp-verify.js
CHANGED
|
@@ -45,6 +45,9 @@ OCSP_CERTID_HASHES[oid.byName("sha256")] = "SHA-256";
|
|
|
45
45
|
OCSP_CERTID_HASHES[oid.byName("sha384")] = "SHA-384";
|
|
46
46
|
OCSP_CERTID_HASHES[oid.byName("sha512")] = "SHA-512";
|
|
47
47
|
var OID_OCSP_SIGNING = oid.byName("ocspSigning");
|
|
48
|
+
// Does this extended-key-usage list assert the given purpose? The answer IS an authorization
|
|
49
|
+
// decision, so it goes through the membership guard rather than a prototype method. See guard-list.
|
|
50
|
+
var _asserts = guard.list.contains;
|
|
48
51
|
var OID_OCSP_NOCHECK = oid.byName("ocspNoCheck");
|
|
49
52
|
var OID_EKU = oid.byName("extKeyUsage");
|
|
50
53
|
var OID_KEY_USAGE = oid.byName("keyUsage");
|
|
@@ -105,7 +108,7 @@ function makeOcspVerify(deps) {
|
|
|
105
108
|
// 5280 sec. 7.1-equal byte encoding of the validated issuer DN to try.
|
|
106
109
|
async function ocspCertIdMatches(certID, cert, issuerNameCandidates, issuerKeyBits) {
|
|
107
110
|
if (certID.serialNumberHex !== cert.serialNumberHex) return false;
|
|
108
|
-
var hashName = OCSP_CERTID_HASHES[certID.hashAlgorithm.oid];
|
|
111
|
+
var hashName = guard.intrinsic.hasOwn(OCSP_CERTID_HASHES, certID.hashAlgorithm.oid) ? OCSP_CERTID_HASHES[certID.hashAlgorithm.oid] : null;
|
|
109
112
|
if (!hashName) return false;
|
|
110
113
|
var keyHash = await ocspDigest(hashName, issuerKeyBits);
|
|
111
114
|
if (!certID.issuerKeyHash.equals(keyHash)) return false;
|
|
@@ -154,7 +157,11 @@ function makeOcspVerify(deps) {
|
|
|
154
157
|
var eku;
|
|
155
158
|
try { eku = decodeExt(rc, OID_EKU); }
|
|
156
159
|
catch (_e) { continue; }
|
|
157
|
-
|
|
160
|
+
// The membership test IS the delegation rule, so it is decided by comparison rather than by a
|
|
161
|
+
// prototype method: dispatched live, an `Array.prototype.indexOf` replaced after load reports
|
|
162
|
+
// the purpose present on ANY certificate, and a cert an attacker put in the response's own
|
|
163
|
+
// certs bag is accepted as the authorized responder.
|
|
164
|
+
if (!eku || !_asserts(eku.value, OID_OCSP_SIGNING)) continue;
|
|
158
165
|
// A delegate asserting keyUsage MUST permit digitalSignature; an absent keyUsage is
|
|
159
166
|
// unrestricted, an unreadable one is not authoritative.
|
|
160
167
|
var ku;
|