@blamejs/pki 0.8.53 → 0.8.55
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 +35 -0
- package/README.md +1 -1
- package/lib/asn1-der.js +112 -4
- package/lib/cmc-build.js +3 -3
- package/lib/cmp-session.js +2 -2
- package/lib/cmp-verify.js +2 -2
- package/lib/crmf-sign.js +1 -1
- package/lib/ct.js +9 -11
- package/lib/guard-der.js +94 -31
- package/lib/guard-encoding.js +2 -1
- package/lib/guard-identifier.js +25 -1
- package/lib/guard-name.js +6 -3
- package/lib/guard-text.js +6 -2
- package/lib/path-validate.js +65 -12
- package/lib/sigstore.js +13 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@ 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.8.55 — 2026-10-06
|
|
8
|
+
|
|
9
|
+
Tell a caller when the path search stopped looking, rather than reporting the chain it had.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pki.path.build` puts `depthLimited: true` on a `{ valid:false }` return when `opts.maxDepth` stopped an expansion that still had candidates waiting. The chain reported alongside it is the best one the search ASSEMBLED, not the best one the pool holds, so a caller treating `valid:false` as a verdict about the certificates should read the field and raise `maxDepth` instead. It is absent from a successful return and from a failing one the search explored to its end, including a chain whose last hop lands exactly on the cap with nothing left to expand: the claim is that the bound truncated the search, not that the search reached it.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `pki.sigstore.verifyBundle` reports `sigstore/chain-incomplete` where it reported `sigstore/chain-invalid`, for a bundle whose Fulcio chain the path search stopped expanding at its depth bound. A rotated CA reissues under one subject name, so a bundle can carry a short branch that assembles and fails beside a long one that reaches an anchor past the bound; the short branch's failure was being published as the verdict on the bundle. The candidate budget already answered this way, and the two bounds now agree. Nothing about a chain that was explored to its end changed: a bundle whose assembled path genuinely fails validation is still `sigstore/chain-invalid`.
|
|
18
|
+
- A reached bound stays a bound rather than becoming a refusal: a bounded depth-first search visits equal-scoring candidates in the order they arrive, so a bundle large enough to exhaust the expansion ceiling can still depend on its own ordering. That is a property of bounding the search at all. Raise `maxDepth` and `maxCandidatesConsidered` for a pool whose shape needs more of the graph walked.
|
|
19
|
+
- `depthLimited` reports a candidate the cap kept the search from expanding, and nothing weaker. A candidate already on the chain is one the cycle check would skip at any depth, so it is not counted: counting it would report a truncated search where the search was exhausted, and turn a chain this toolkit refuses into one it claims it never finished reading. Two cases it does not report, both of which leave the older behavior in place rather than overstating: a chain whose last hop lands on the cap with nothing left to match, and a cap that also suppressed an Authority Information Access fetch for the certificate at that hop. The second needs the fetch's own URL eligibility answered to be certain a fetch was available, so it is left unclaimed rather than guessed at.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `pki.ct` and `pki.path.build` take their declared options through one shared snapshot instead of two copies of the same loop. Both read every declared name once, before anything derives from one, and both keep an omitted option OMITTED rather than recording it as a name holding `undefined`, which is what lets an absent `transport` still mean "open the connection yourself, under the pinned anchors". Behavior is unchanged at both verbs; what the shared door adds is that the next caller to copy an options bag by hand is caught before it ships.
|
|
24
|
+
|
|
25
|
+
## v0.8.54 — 2026-10-06
|
|
26
|
+
|
|
27
|
+
Refuse the universal tags X.680 assigns to no type, and tell a caller which those are.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `pki.asn1.reservedUniversalTag(tagNumber)` answers whether X.680 reserves a universal tag number, which is the question `pki.asn1.decode` asks before it accepts an element. It returns `false` for every assigned type, including the seven this codec carries no reader for, so it is not a statement about what can be read: a caller deciding whether bytes are worth passing on gets the same answer the decoder will give. A value that is not a tag number answers `false` rather than throwing, as `pki.asn1.isPrintableString` and `pki.oid.isDottedDecimal` do of a value of the wrong type, and the type is checked before anything is compared, so `37.5` and `Infinity` are not read as reserved for sitting inside the addenda range and no `valueOf` of a caller's object is invoked inside the predicate.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `pki.asn1.decode` refuses a universal tag 15 element and any universal tag from 37 up, with `asn1/reserved-tag` naming the row of X.680 Table 1 that reserves it. Every reader in the toolkit inherits the refusal, so a message carrying such an element is refused at its own verb: `pki.schema.parse` and `pki.schema.detectFormat` report `schema/bad-der` before any format detector reads the children, which matters because a child nothing refuses is a child a detector may count. `pki.inspect.asn1` still prints the header walk beside the refusal, so an operator can see what the bytes claimed. Nothing that was accepted before and is assigned by X.680 changed: the four long-form time types and the two internationalized-resource-identifier types at tags 31 to 36 decode as they did.
|
|
36
|
+
- `pki.asn1.encode` and `pki.asn1.encodeIdentifier` refuse the same tags, with the same `asn1/reserved-tag` code, rather than emitting bytes this toolkit's own decoder calls malformed. The class is read off the masked identifier bits as the decoder reads it, so a `classBits` carrying only the constructed bit is the universal class here too. Every other class is untouched, which is what makes the rule narrow enough to be safe: a context `[15]` is how an optional field is written across X.509 and CMS, and it encodes as it did. So do the assigned universal types at tags 31 to 36. The DER form restrictions are deliberately not enforced at this door: a constructed OCTET STRING or character string is a legitimate BER encoding that DER alone narrows to the primitive form, and this low-level encoder is how those bytes are minted for the BER reader that accepts them. A tag number that encodes nothing is a different matter from a form one set of encoding rules permits and another does not.
|
|
37
|
+
- `pki.asn1.encode` and `pki.asn1.encodeIdentifier` require `classBits` to be a number naming a class: `0x00` universal, `0x40` application, `0x80` context or `0xc0` private, with `0x20` constructed allowed alongside. Anything else is `asn1/bad-tag`, as a tag number of the wrong type already was. The low five bits of that argument were being written into a single-octet identifier, so a value carrying any of them emitted a tag other than the one passed: `encodeIdentifier(0x0e, false, 1)` wrote `0f`, universal 15. A value that was not a number was narrowed into a class instead of refused, so `"128"` and `0x100000080` both encoded as a context `[1]`. For every argument still accepted the bytes are unchanged. A tag number past the four-octet high-tag form now reports `asn1/tag-too-large` ahead of any reserved-tag refusal, which is the order `pki.asn1.decode` has always refused that input in.
|
|
38
|
+
- `pki.asn1.TAGS` is frozen. Assigning to it reached the encoders that compose an identifier from a tag number and the comparisons that test one against it, so a single assignment moved what a SEQUENCE is for every caller in the process. The numbers are X.680's; read them, and build a structure with a tag of your own through `pki.asn1.encode`.
|
|
39
|
+
- Eight places that reported a failure by reading `message` off whatever was thrown now describe the thrown value the way the rest of the toolkit does. `pki.cmc.build`, `pki.cmp` session and verification, and `pki.crmf` are the verbs affected, and only the text of a message changes, in the case where what was caught is not an error or carries no readable message: it is named by its type instead of by a fixed phrase, and a `message` that answers from an accessor is read once rather than twice, so the text reported is the text that was tested. A `message` accessor that throws no longer replaces the failure being reported with its own. The error codes are unchanged.
|
|
40
|
+
- A caught value whose `message` is the empty string is described by its type as well. It was being reported as the empty string it was, which left the sentence built around it ending at its colon with nothing after it. This reaches every message assembled that way, including the ones `pki.ocsp.httpRequest` and the revocation fetcher build. A message of only whitespace is still a message and is reported unchanged.
|
|
41
|
+
|
|
7
42
|
## v0.8.53 — 2026-10-06
|
|
8
43
|
|
|
9
44
|
Put a copy of a message's algorithm identifiers where its signature or its MAC covers them, and refuse one whose copy disagrees.
|
package/README.md
CHANGED
|
@@ -237,7 +237,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
237
237
|
|
|
238
238
|
| Namespace | What it does |
|
|
239
239
|
|---|---|
|
|
240
|
-
| `pki.asn1` | Strict, bounded DER codec. `decode` returns a zero-copy node tree, `build.*` emits canonical DER values, `read.*` are typed leaf readers. Plus `encode`, `TAGS`, OID content encode/decode, and the `isPrintableString` string predicate |
|
|
240
|
+
| `pki.asn1` | Strict, bounded DER codec. `decode` returns a zero-copy node tree, `build.*` emits canonical DER values, `read.*` are typed leaf readers. A universal tag X.680 Table 1 reserves and assigns to no type is refused as `asn1/reserved-tag`, and `reservedUniversalTag` answers which tags those are without attempting a decode. Plus `encode`, `TAGS`, OID content encode/decode, and the `isPrintableString` string predicate |
|
|
241
241
|
| `pki.cbor` | Strict, bounded deterministic CBOR codec (RFC 8949). `decode` returns a zero-copy node tree, plus `read.*` typed leaf readers including the keyed lookup `read.mapGet` (text or COSE-label integer key, with the map's major type asserted in the accessor). Fail-closed on every non-canonical shape: indefinite length, non-minimal argument, unsorted or duplicate map keys, non-shortest float, trailing bytes. The `build.*` encoders hold an integer argument to being an integer rather than converting one: a string in any radix, a padded or empty string, and a boolean are each refused, so an unsupplied value cannot encode as zero |
|
|
242
242
|
| `pki.oid` | Two-way OID ↔ name registry, seeded with RFC 5280 and the NIST PQC arcs — `name`, `byName`, `register`, `toArcs`/`fromArcs`, `toDER`/`fromDER`, and the `isDottedDecimal` string predicate |
|
|
243
243
|
| `pki.webcrypto` | A W3C `SubtleCrypto` engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2 and SHA, plus post-quantum ML-DSA-44/65/87, SLH-DSA, and ML-KEM-512/768/1024 key generation with certificate and PKCS#8 import. The RFC 9935 seed / expandedKey / both private-key CHOICE is validated fail-closed, so an OpenSSL-legacy bare seed or an internally inconsistent key is rejected with a typed error. `encapsulateBits` and `decapsulateBits` are the ML-KEM key-establishment pair the CMS `KEMRecipientInfo` arm rides on, with the FIPS 203 §7.3 ciphertext-length check enforced here so a direct caller inherits it. Cross-implementation use is undefined in the specification, so a `CryptoKey` from a different WebCrypto implementation is refused here with a typed fault naming its origin; the `pki.*` verbs adopt such a key instead, whether it came from the platform or from a separately installed copy of this toolkit |
|
package/lib/asn1-der.js
CHANGED
|
@@ -64,6 +64,49 @@ function _asn1Error(c, m) { return new Asn1Error(c, m); }
|
|
|
64
64
|
function _oidErr(c, m) { return new OidError(c, m); }
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* @primitive pki.asn1.reservedUniversalTag
|
|
69
|
+
* @signature pki.asn1.reservedUniversalTag(tagNumber) -> boolean
|
|
70
|
+
* @since 0.8.54
|
|
71
|
+
* @status stable
|
|
72
|
+
* @spec X.680, ISO/IEC 8824-1
|
|
73
|
+
* @related pki.asn1.decode
|
|
74
|
+
*
|
|
75
|
+
* Whether X.680 reserves this universal tag number, which means no ASN.1 type is assigned to it and
|
|
76
|
+
* no DER encoding of any type uses it. Table 1 reserves three rows: tag 0 for the encoding rules,
|
|
77
|
+
* tag 15 for future editions of the Recommendation, and tag 37 upward for its addenda. `decode`
|
|
78
|
+
* refuses an element carrying any of them with `asn1/reserved-tag`, so this answers the same
|
|
79
|
+
* question ahead of the attempt.
|
|
80
|
+
*
|
|
81
|
+
* The question is not "does this codec have a reader for the tag", which is a smaller set:
|
|
82
|
+
* ObjectDescriptor (7), REAL (9), RELATIVE-OID (13), the time type (14), VideotexString (21),
|
|
83
|
+
* GraphicString (25) and GeneralString (27) are all assigned by X.680 and all arrive here as opaque
|
|
84
|
+
* values, and each is legitimate inside an `ANY`. This returns `false` for every one of them.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* pki.asn1.reservedUniversalTag(0); // -> true (the encoding rules)
|
|
88
|
+
* pki.asn1.reservedUniversalTag(15); // -> true (future editions)
|
|
89
|
+
* pki.asn1.reservedUniversalTag(37); // -> true (addenda)
|
|
90
|
+
* pki.asn1.reservedUniversalTag(9); // -> false (REAL, assigned, no reader here)
|
|
91
|
+
* pki.asn1.reservedUniversalTag(31); // -> false (DATE, assigned)
|
|
92
|
+
*/
|
|
93
|
+
function reservedUniversalTag(tagNumber) {
|
|
94
|
+
/** @internal The type check comes FIRST, and a value that is not a tag number answers `false`
|
|
95
|
+
* rather than throwing, which is how the other public predicates here answer: `isPrintableString`
|
|
96
|
+
* and `oid.isDottedDecimal` both say "not that" of a value of the wrong type. A range test alone
|
|
97
|
+
* calls `37.5` and `Infinity` reserved, since both sit inside the addenda range while being no
|
|
98
|
+
* tag at all, and `tagNumber >= 37` against a caller's object runs that object's `valueOf`, so a
|
|
99
|
+
* comparison reached before this check runs caller code inside a predicate.
|
|
100
|
+
*
|
|
101
|
+
* Then all three rows the table reserves, tag 0 included: this answers what X.680 says, and it
|
|
102
|
+
* says tag 0 is reserved. The decode door keeps a separate branch for tag 0 ahead of the general
|
|
103
|
+
* one so its refusal can cite the end-of-contents encoding X.690 sec. 8.1.5 gives that octet
|
|
104
|
+
* pair, which is the useful thing to tell whoever sent it; the answer to "is it reserved" is the
|
|
105
|
+
* same from either place. */
|
|
106
|
+
if (typeof tagNumber !== "number" || !intrinsic.isInteger(tagNumber) || tagNumber < 0) return false;
|
|
107
|
+
return tagNumber === 0 || tagNumber === 15 || tagNumber >= 37;
|
|
108
|
+
}
|
|
109
|
+
|
|
67
110
|
var UNIVERSAL_TYPES = intrinsic.assign(intrinsic.create(null), {
|
|
68
111
|
BOOLEAN: { tag: 0x01, form: "primitive" },
|
|
69
112
|
INTEGER: { tag: 0x02, form: "primitive" },
|
|
@@ -89,14 +132,21 @@ var UNIVERSAL_TYPES = intrinsic.assign(intrinsic.create(null), {
|
|
|
89
132
|
BMP_STRING: { tag: 0x1e, form: "primitive" },
|
|
90
133
|
});
|
|
91
134
|
|
|
135
|
+
/** @internal Frozen, because it is exported and every structure in the toolkit names its tags
|
|
136
|
+
* through it. Left writable, `pki.asn1.TAGS.SEQUENCE = 15` reached the encoders that compose an
|
|
137
|
+
* identifier from it and the guard that compares one against it, so one assignment by anything
|
|
138
|
+
* sharing the process moved what a SEQUENCE is for the whole process. The numbers are X.680's and
|
|
139
|
+
* no caller has a reason to change one. */
|
|
92
140
|
var TAGS = intrinsic.create(null);
|
|
93
141
|
Object.keys(UNIVERSAL_TYPES).forEach(function (k) { TAGS[k] = UNIVERSAL_TYPES[k].tag; });
|
|
142
|
+
intrinsic.freeze(TAGS);
|
|
94
143
|
|
|
95
144
|
var CLASS_UNIVERSAL = 0x00;
|
|
96
145
|
var CLASS_APPLICATION = 0x40;
|
|
97
146
|
var CLASS_CONTEXT = 0x80;
|
|
98
147
|
var CLASS_PRIVATE = 0xc0;
|
|
99
148
|
var CONSTRUCTED_BIT = 0x20;
|
|
149
|
+
var CLASS_MASK = 0xc0;
|
|
100
150
|
|
|
101
151
|
var CONSTRUCTED_ONLY_UNIVERSAL_TAGS = Object.create(null);
|
|
102
152
|
Object.keys(UNIVERSAL_TYPES).forEach(function (k) {
|
|
@@ -185,7 +235,7 @@ function _decodeTLV(buf, start, limit, depth, maxDepth, ber, strDepth, ctr) {
|
|
|
185
235
|
var p = start;
|
|
186
236
|
if (p >= limit) throw new Asn1Error("asn1/truncated", "expected an identifier octet");
|
|
187
237
|
var first = buf[p]; p += 1;
|
|
188
|
-
var tagClassBits = first &
|
|
238
|
+
var tagClassBits = first & CLASS_MASK;
|
|
189
239
|
var constructed = (first & CONSTRUCTED_BIT) !== 0;
|
|
190
240
|
var tagNumber = first & 0x1f;
|
|
191
241
|
|
|
@@ -221,6 +271,12 @@ function _decodeTLV(buf, start, limit, depth, maxDepth, ber, strDepth, ctr) {
|
|
|
221
271
|
"universal tag 0 is the reserved end-of-contents encoding (X.690 sec. 8.1.5) and is not an element");
|
|
222
272
|
}
|
|
223
273
|
|
|
274
|
+
if (tagClassBits === CLASS_UNIVERSAL && reservedUniversalTag(tagNumber)) {
|
|
275
|
+
throw new Asn1Error("asn1/reserved-tag",
|
|
276
|
+
"universal tag " + tagNumber + " is reserved by X.680 Table 1 (" + (tagNumber === 15
|
|
277
|
+
? "for future editions" : "for addenda") + ") and is not assigned to any type");
|
|
278
|
+
}
|
|
279
|
+
|
|
224
280
|
if (tagClassBits === CLASS_UNIVERSAL && CONSTRUCTED_ONLY_UNIVERSAL_TAGS[tagNumber] && !constructed) {
|
|
225
281
|
throw new Asn1Error("asn1/bad-tlv", "a universal constructed-only type (SEQUENCE/SET/EXTERNAL/EMBEDDED PDV/CHARACTER STRING) must be constructed");
|
|
226
282
|
}
|
|
@@ -707,9 +763,13 @@ function encodeLength(n) {
|
|
|
707
763
|
return Buffer.concat([Buffer.from([0x80 | bytes.length]), Buffer.from(bytes)]);
|
|
708
764
|
}
|
|
709
765
|
|
|
766
|
+
/** @internal The identifier comes from the one function that composes one. Writing the octet here
|
|
767
|
+
* instead put a second author of an identifier in the file, and it answered differently: with
|
|
768
|
+
* `TAGS.SEQUENCE` reassigned to 15 this emitted `2f00`, which `decode` refuses, while every
|
|
769
|
+
* `build.*` helper routed through `encodeIdentifier` and refused it. */
|
|
710
770
|
function sequenceTlv(node) {
|
|
711
771
|
var content = node.content != null ? node.content : Buffer.concat((node.children || []).map(function (c) { return c.bytes; }));
|
|
712
|
-
return Buffer.concat([
|
|
772
|
+
return Buffer.concat([encodeIdentifier(CLASS_UNIVERSAL, true, TAGS.SEQUENCE), encodeLength(content.length), content]);
|
|
713
773
|
}
|
|
714
774
|
|
|
715
775
|
/** @internal A constructed value written into ONE allocation, rather than concatenating the children
|
|
@@ -738,12 +798,43 @@ function encodeIdentifier(classBits, constructed, tagNumber) {
|
|
|
738
798
|
if (typeof tagNumber !== "number" || !isFinite(tagNumber) || tagNumber < 0 || Math.floor(tagNumber) !== tagNumber) {
|
|
739
799
|
throw new Asn1Error("asn1/bad-tag", "tag number must be a non-negative integer");
|
|
740
800
|
}
|
|
741
|
-
|
|
801
|
+
/** @internal The class bits are taken ONCE, into the number the checks and the emitted octet all
|
|
802
|
+
* use, and then checked to be a class. The low five bits of this argument are OR'd into the
|
|
803
|
+
* single-octet identifier, so a `classBits` carrying any of them emits a DIFFERENT tag than the
|
|
804
|
+
* one handed in: `encodeIdentifier(0x0e, false, 1)` wrote `0f`, universal 15, past a reserved-tag
|
|
805
|
+
* check that had been shown tag 1. The four class values are the whole domain, with the
|
|
806
|
+
* constructed bit allowed alongside because a caller may fold it in rather than pass it. */
|
|
807
|
+
var cls = classBits | 0;
|
|
808
|
+
if (typeof classBits !== "number" || cls !== classBits ||
|
|
809
|
+
(cls & ~(CLASS_MASK | CONSTRUCTED_BIT)) !== 0) {
|
|
810
|
+
throw new Asn1Error("asn1/bad-tag", "class bits must be one of 0x00 universal, 0x40 application, " +
|
|
811
|
+
"0x80 context or 0xc0 private, optionally with 0x20 constructed, got " + _typeOf(classBits));
|
|
812
|
+
}
|
|
813
|
+
/** @internal The size bound before the reserved rows, which is the order the decoder refuses them
|
|
814
|
+
* in: a tag past the four-octet high-tag form it reads is unreadable whatever the table says
|
|
815
|
+
* about the number, so both directions name `asn1/tag-too-large` for it and the reserved code
|
|
816
|
+
* keeps meaning that X.680 reserved the row. 0x0fffffff is the largest number four 7-bit octets
|
|
817
|
+
* hold, so this is the bound the body-length check below expressed. */
|
|
818
|
+
if (tagNumber > 0x0fffffff) throw new Asn1Error("asn1/tag-too-large", "high-tag-number too large");
|
|
819
|
+
/** @internal The reserved rows, refused in BOTH directions. X.680 Table 1 assigns no type to
|
|
820
|
+
* universal 0, 15, or 37 upward, so there is no encoding of any type for `decode` to accept and
|
|
821
|
+
* it refuses all three; emitting them anyway hands a caller bytes this codec's own decoder calls
|
|
822
|
+
* malformed. The DER FORM rules are deliberately NOT enforced here: a constructed OCTET STRING or
|
|
823
|
+
* string type is a legitimate BER encoding (X.690 sec. 8.21.1) that DER alone restricts to the
|
|
824
|
+
* primitive form (sec. 10.2), and this encoder is the escape hatch those bytes are minted through
|
|
825
|
+
* for the BER door that reads them. A tag number that encodes nothing is a different thing from a
|
|
826
|
+
* form one encoding rule allows and another does not. */
|
|
827
|
+
if ((cls & CLASS_MASK) === CLASS_UNIVERSAL && reservedUniversalTag(tagNumber)) {
|
|
828
|
+
throw new Asn1Error("asn1/reserved-tag", "universal tag " + tagNumber + " is reserved by X.680 " +
|
|
829
|
+
"Table 1 (" + (tagNumber === 0 ? "for the encoding rules, which give 00 00 to the " +
|
|
830
|
+
"end-of-contents marker" : tagNumber === 15 ? "for future editions" : "for addenda") +
|
|
831
|
+
") and is not assigned to any type, so no encoding of it is a DER element");
|
|
832
|
+
}
|
|
833
|
+
var lead = cls | (constructed ? CONSTRUCTED_BIT : 0);
|
|
742
834
|
if (tagNumber < 0x1f) return Buffer.from([lead | tagNumber]);
|
|
743
835
|
var body = [];
|
|
744
836
|
var v = tagNumber;
|
|
745
837
|
do { _unshift(body, v & 0x7f); v = Math.floor(v / 128); } while (v > 0);
|
|
746
|
-
if (body.length > 4) throw new Asn1Error("asn1/tag-too-large", "high-tag-number too large");
|
|
747
838
|
for (var i = 0; i < body.length - 1; i++) body[i] |= 0x80;
|
|
748
839
|
return Buffer.concat([Buffer.from([lead | 0x1f]), Buffer.from(body)]);
|
|
749
840
|
}
|
|
@@ -760,6 +851,22 @@ function encodeIdentifier(classBits, constructed, tagNumber) {
|
|
|
760
851
|
* buffer. Most callers use the higher-level `build.*` helpers; this is the
|
|
761
852
|
* escape hatch for context-tagged and implicitly-tagged constructions.
|
|
762
853
|
*
|
|
854
|
+
* `classBits` is a number naming a class: `0x00` universal, `0x40`
|
|
855
|
+
* application, `0x80` context or `0xc0` private, with `0x20` constructed
|
|
856
|
+
* allowed alongside. Anything else is `asn1/bad-tag`, because the low five
|
|
857
|
+
* bits of this argument are written into a single-octet identifier and a
|
|
858
|
+
* value carrying any of them emits a tag other than the one passed.
|
|
859
|
+
*
|
|
860
|
+
* A universal-class tag number that X.680 Table 1 reserves is refused with
|
|
861
|
+
* `asn1/reserved-tag`, the code `pki.asn1.decode` gives the same bytes, so the
|
|
862
|
+
* escape hatch cannot author an element the decoder calls malformed. Ask
|
|
863
|
+
* `pki.asn1.reservedUniversalTag` which numbers those are. Every other class
|
|
864
|
+
* takes any tag number the 4-octet high-tag form holds, a context `[15]`
|
|
865
|
+
* included; past that form the code is `asn1/tag-too-large`, as it is on the
|
|
866
|
+
* way in. The DER form restrictions are not applied here: a constructed
|
|
867
|
+
* OCTET STRING or character string is a legal BER encoding, and this is the
|
|
868
|
+
* door it is written through.
|
|
869
|
+
*
|
|
763
870
|
* @example
|
|
764
871
|
* pki.asn1.encode(0x00, false, pki.asn1.TAGS.NULL, Buffer.alloc(0));
|
|
765
872
|
*/
|
|
@@ -1059,6 +1166,7 @@ function _asBufferArray(arr, who) {
|
|
|
1059
1166
|
|
|
1060
1167
|
module.exports = {
|
|
1061
1168
|
TAGS: TAGS,
|
|
1169
|
+
reservedUniversalTag: reservedUniversalTag,
|
|
1062
1170
|
decode: decode,
|
|
1063
1171
|
isPrintableString: isPrintableString,
|
|
1064
1172
|
encode: encodeTLV,
|
package/lib/cmc-build.js
CHANGED
|
@@ -253,7 +253,7 @@ function encodeRequest(req, ids, index) {
|
|
|
253
253
|
try { csr.parse(_der(req.tcr, "a tcr certificationRequest")); }
|
|
254
254
|
catch (e) {
|
|
255
255
|
throw E("cmc/bad-input", "a tcr request must be a PKCS#10 CertificationRequest: " +
|
|
256
|
-
(e
|
|
256
|
+
guard.text.describeThrown(e), e);
|
|
257
257
|
}
|
|
258
258
|
var id = ids.claim(req.bodyPartID, "a tcr request");
|
|
259
259
|
return { bodyPartID: id, der: b.contextConstructed(0, Buffer.concat([b.integer(BigInt(id)), _der(req.tcr, "a tcr certificationRequest")])) };
|
|
@@ -263,7 +263,7 @@ function encodeRequest(req, ids, index) {
|
|
|
263
263
|
try { crmf.parse(b.sequence([msg])); }
|
|
264
264
|
catch (e) {
|
|
265
265
|
throw E("cmc/bad-input", "a crm request must be an RFC 4211 CertReqMsg: " +
|
|
266
|
-
(e
|
|
266
|
+
guard.text.describeThrown(e), e);
|
|
267
267
|
}
|
|
268
268
|
var certReqId = ids.claim(_certReqIdOf(msg), "a crm request");
|
|
269
269
|
var node = asn1.decode(msg);
|
|
@@ -947,7 +947,7 @@ async function _build(spec, signer, opts) {
|
|
|
947
947
|
} catch (e) {
|
|
948
948
|
throw E("cmc/bad-input",
|
|
949
949
|
"the assembled PKIData does not parse as one, so it would be refused by its recipient: " +
|
|
950
|
-
(e
|
|
950
|
+
guard.text.describeThrown(e), e);
|
|
951
951
|
}
|
|
952
952
|
|
|
953
953
|
/** @internal RFC 5272 sec. 3.2 gives a Full PKI Request two carriers, and a request uses one: a
|
package/lib/cmp-session.js
CHANGED
|
@@ -971,8 +971,8 @@ function session(opts) {
|
|
|
971
971
|
var res;
|
|
972
972
|
try { res = await _engine.build(leaf, { trustAnchors: _asCertList(opts.trustAnchors), intermediates: pool, validate: true, time: opts.time != null ? opts.time : new Date() }); }
|
|
973
973
|
catch (e) {
|
|
974
|
-
if (e.code === "path/bad-input") throw _err("cmp/bad-input", "invalid trust / validation options for issued-certificate validation: " + (e
|
|
975
|
-
throw _err("cmp/bad-cert-response", "the issued certificate could not be validated to a supplied trust anchor: " + (e
|
|
974
|
+
if (e.code === "path/bad-input") throw _err("cmp/bad-input", "invalid trust / validation options for issued-certificate validation: " + guard.text.describeThrown(e), e);
|
|
975
|
+
throw _err("cmp/bad-cert-response", "the issued certificate could not be validated to a supplied trust anchor: " + guard.text.describeThrown(e), e);
|
|
976
976
|
}
|
|
977
977
|
if (!res || res.valid !== true) throw _err("cmp/bad-cert-response", "the issued certificate did not validate to a supplied trust anchor (its signature or chain is invalid) (RFC 5280 sec. 6.1)");
|
|
978
978
|
}
|
package/lib/cmp-verify.js
CHANGED
|
@@ -671,9 +671,9 @@ async function _chainSigner(signer, m, opts, extra) {
|
|
|
671
671
|
return guard.verdict.of({ trusted: true, reason: null, chain: chain });
|
|
672
672
|
} catch (e) {
|
|
673
673
|
if (e && e.code === "path/bad-input") {
|
|
674
|
-
throw _err("cmp/bad-input", "invalid trust / validation options for signer-certificate path validation: " + (e
|
|
674
|
+
throw _err("cmp/bad-input", "invalid trust / validation options for signer-certificate path validation: " + guard.text.describeThrown(e), e);
|
|
675
675
|
}
|
|
676
|
-
return guard.verdict.of({ trusted: false, reason: "signer certificate path validation failed: " + (e
|
|
676
|
+
return guard.verdict.of({ trusted: false, reason: "signer certificate path validation failed: " + guard.text.describeThrown(e) });
|
|
677
677
|
}
|
|
678
678
|
}
|
|
679
679
|
|
package/lib/crmf-sign.js
CHANGED
|
@@ -943,7 +943,7 @@ async function _assertEnclosedKeyCorresponds(privateKeyDer, templateSpki) {
|
|
|
943
943
|
* failing, not the input. */
|
|
944
944
|
if (e && e.isPkiError === true && e.code === "key/bad-input") {
|
|
945
945
|
throw _err("crmf/bad-input", "pop.privateKey could not be read as a private key to check against " +
|
|
946
|
-
"certTemplate.publicKey: " + (e
|
|
946
|
+
"certTemplate.publicKey: " + guard.text.describeThrown(e), e);
|
|
947
947
|
}
|
|
948
948
|
throw _err("crmf/bad-popo", "the private key enclosed in the encryptedKey proof of possession could not be " +
|
|
949
949
|
"exercised with certTemplate.publicKey, so it cannot be shown to be that key's private half (RFC 4211 sec. 4.2)", e);
|
package/lib/ct.js
CHANGED
|
@@ -1353,18 +1353,16 @@ function _clientDoor(opts, keyName) {
|
|
|
1353
1353
|
}
|
|
1354
1354
|
|
|
1355
1355
|
/** @internal Every DECLARED option the bag can answer, read once into a record of plain values, before
|
|
1356
|
-
* anything derives from one.
|
|
1357
|
-
*
|
|
1358
|
-
* which
|
|
1359
|
-
* prototype chain exactly as the direct reads it replaces did,
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
1356
|
+
* anything derives from one. The presence-preserving snapshot is the one this module needs: absence
|
|
1357
|
+
* survives, so a name the bag cannot answer is left out rather than materialized as `undefined`,
|
|
1358
|
+
* which is what lets an absent `transport` still mean "open the connection yourself, under the
|
|
1359
|
+
* pinned anchors". It consults the prototype chain exactly as the direct reads it replaces did,
|
|
1360
|
+
* because the door accepts an option supplied through a caller's defaults object and an enumeration
|
|
1361
|
+
* omits an inherited FUNCTION member, which silently dropped a `transport` and fell through to
|
|
1362
|
+
* opening a real connection. */
|
|
1363
|
+
// @guard-via guard.identifier.snapshotPresentOptions
|
|
1362
1364
|
function _takeTopLevel(bag, known) {
|
|
1363
|
-
|
|
1364
|
-
intrinsic.forEach(intrinsic.keys(known), function (k) {
|
|
1365
|
-
if (k in bag) o[k] = bag[k];
|
|
1366
|
-
});
|
|
1367
|
-
return o;
|
|
1365
|
+
return guard.identifier.snapshotPresentOptions(bag, known);
|
|
1368
1366
|
}
|
|
1369
1367
|
|
|
1370
1368
|
/** @internal The two values a verdict rests on, made IMMUNE rather than merely taken. Holding a
|
package/lib/guard-der.js
CHANGED
|
@@ -8,23 +8,42 @@ var _forEach = require("./guard-intrinsic").forEach;
|
|
|
8
8
|
var _sizeOf = require("./guard-intrinsic").sizeOf;
|
|
9
9
|
var _create = require("./guard-intrinsic").create;
|
|
10
10
|
var _isBuffer = require("./guard-intrinsic").isBuffer;
|
|
11
|
+
var _isArray = require("./guard-intrinsic").isArray;
|
|
11
12
|
|
|
12
13
|
var asn1 = null;
|
|
13
14
|
function _asn1() { if (asn1 === null) asn1 = require("./asn1-der"); return asn1; }
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
/** @internal Everything this file needs from the codec, captured in ONE step on first use: the two
|
|
17
|
+
* functions it calls, the two tag NUMBERS it compares against, and the reader table. Read per call
|
|
18
|
+
* instead, each is a property of a module object anything sharing the process can assign to, and
|
|
19
|
+
* the verdict follows the assignment: `reservedUniversalTag` replaced by a function answering
|
|
20
|
+
* `true` made `tlv` refuse a valid NULL, and `TAGS.SEQUENCE` reassigned made both `element` and
|
|
21
|
+
* `tlv` refuse a valid empty SEQUENCE. The tag numbers are copied out rather than the table kept,
|
|
22
|
+
* because freezing a record holding a reference leaves what it points at writable. One step rather
|
|
23
|
+
* than two because two initialize at different first calls, and the gap between them was itself a
|
|
24
|
+
* window: warming the codec through a context node left the readers uncaptured, and `read.nullValue`
|
|
25
|
+
* replaced in between turned a NULL with content from refused into accepted. The capture is lazy
|
|
26
|
+
* because the codec requires this file, so neither can be required at the other's load. */
|
|
27
|
+
var CODEC = null;
|
|
28
|
+
function _codec() {
|
|
29
|
+
if (CODEC !== null) return CODEC;
|
|
30
|
+
var m = _asn1();
|
|
31
|
+
var R = m.read, T = m.TAGS;
|
|
32
|
+
var readers = _create(null);
|
|
33
|
+
readers[T.BOOLEAN] = R.boolean; readers[T.INTEGER] = R.integer; readers[T.ENUMERATED] = R.enumerated;
|
|
34
|
+
readers[T.BIT_STRING] = R.bitString; readers[T.OCTET_STRING] = R.octetString;
|
|
35
|
+
readers[T.NULL] = R.nullValue; readers[T.OBJECT_IDENTIFIER] = R.oid; readers[T.UTC_TIME] = R.time;
|
|
36
|
+
readers[T.GENERALIZED_TIME] = R.time; readers[T.NUMERIC_STRING] = R.numericString;
|
|
24
37
|
_forEach([T.UTF8_STRING, T.PRINTABLE_STRING, T.IA5_STRING, T.TELETEX_STRING, T.VISIBLE_STRING,
|
|
25
|
-
T.BMP_STRING, T.UNIVERSAL_STRING], function (t) {
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
T.BMP_STRING, T.UNIVERSAL_STRING], function (t) { readers[t] = R.string; });
|
|
39
|
+
CODEC = _freeze({
|
|
40
|
+
decode: m.decode,
|
|
41
|
+
reservedUniversalTag: m.reservedUniversalTag,
|
|
42
|
+
SEQUENCE: T.SEQUENCE,
|
|
43
|
+
SET: T.SET,
|
|
44
|
+
readers: _freeze(readers),
|
|
45
|
+
});
|
|
46
|
+
return CODEC;
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
var TAG_CLASS_RANK = _create(null);
|
|
@@ -48,30 +67,69 @@ function setOrderOk(kids) {
|
|
|
48
67
|
return dup ? octetAsc : (octetAsc || tagAsc);
|
|
49
68
|
}
|
|
50
69
|
|
|
51
|
-
// @enforced-by
|
|
52
|
-
// @
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
// @enforced-by behavioral -- validating a universal element's content through the codec's own reader
|
|
71
|
+
// @enforced-by behavioral -- for its tag has no rename-proof shape: a re-inline builds a table keyed
|
|
72
|
+
// @enforced-by behavioral -- by a TAGS member under any local name, and an anchor on one name goes
|
|
73
|
+
// @enforced-by behavioral -- silently green on a rename. The RED vectors in guard-der.test.js are the
|
|
74
|
+
// @enforced-by behavioral -- guard, and every format parser's strict-DER vectors route through tlv.
|
|
75
|
+
function element(node, E, code, label) { _element(node, E, code, label); }
|
|
76
|
+
|
|
77
|
+
/** @internal Every field of the node is taken ONCE, into the plain record every rule below decides
|
|
78
|
+
* on, every reader is handed, and the SET-order check compares. Read per rule instead, a node whose
|
|
79
|
+
* `tagNumber` answers from a getter clears one check as the tag it is not and is then handled as the
|
|
80
|
+
* tag it is: a node answering 15 and then 5 was ACCEPTED, over four reads. A Proxy saw two reads of
|
|
81
|
+
* each field, the content validator was handed the caller's object rather than what was checked, and
|
|
82
|
+
* a SET member's getter answered the order comparison with a value the member's own validation never
|
|
83
|
+
* saw. The record is TYPED because each field is read as something: the tag becomes a property key at
|
|
84
|
+
* the reader lookup, which runs a `Symbol.toPrimitive` an object carries, and the form decides a
|
|
85
|
+
* branch a truthy non-boolean enters. `decode` answers every field with a plain value for every
|
|
86
|
+
* input it accepts, so this refuses only a node the codec did not produce. The node `tlv` passes
|
|
87
|
+
* holds plain values; what the record closes is the next caller, which hands this a node it decoded
|
|
88
|
+
* elsewhere or built itself. Returned so the caller's walk compares the members it validated. */
|
|
89
|
+
function _element(node, E, code, label) {
|
|
90
|
+
if (!node || typeof node !== "object") throw E(code, label + " must be a decoded DER node");
|
|
91
|
+
var s = {
|
|
92
|
+
tagClass: node.tagClass,
|
|
93
|
+
tagNumber: node.tagNumber,
|
|
94
|
+
constructed: node.constructed,
|
|
95
|
+
content: node.content,
|
|
96
|
+
bytes: node.bytes,
|
|
97
|
+
};
|
|
98
|
+
var kids = s.constructed === true ? node.children : null;
|
|
99
|
+
if (typeof s.tagClass !== "string" || typeof s.tagNumber !== "number" ||
|
|
100
|
+
typeof s.constructed !== "boolean" || (s.constructed && !_isArray(kids))) {
|
|
101
|
+
throw E(code, label + " must be a node this codec decoded, carrying a tag class, a tag number, " +
|
|
102
|
+
"a form, and members when it is constructed");
|
|
103
|
+
}
|
|
104
|
+
var cls = s.tagClass, tag = s.tagNumber, C = _codec();
|
|
105
|
+
if (cls === "universal" && tag === 0) {
|
|
55
106
|
throw E(code, label + " must not use the reserved end-of-contents encoding (tag 0)");
|
|
56
107
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
108
|
+
/** @internal The other reserved rows, asked of the codec's own predicate so the two cannot
|
|
109
|
+
* disagree about which universal tags X.680 Table 1 assigns to no type. The codec door refuses
|
|
110
|
+
* these before a node exists, so this answers for a node that reached here by another route. */
|
|
111
|
+
if (cls === "universal" && C.reservedUniversalTag(tag)) {
|
|
112
|
+
throw E(code, label + " must not use universal tag " + tag +
|
|
113
|
+
", which X.680 Table 1 reserves and assigns to no type");
|
|
114
|
+
}
|
|
115
|
+
if (s.constructed) {
|
|
116
|
+
if (cls === "universal" && tag !== C.SEQUENCE && tag !== C.SET) {
|
|
117
|
+
throw E(code, label + " of universal constructed type " + tag + " has no strict DER structure validator here");
|
|
61
118
|
}
|
|
62
|
-
var kids =
|
|
63
|
-
for (var i = 0; i <
|
|
64
|
-
if (
|
|
119
|
+
var n = kids.length, members = [];
|
|
120
|
+
for (var i = 0; i < n; i++) members[i] = _element(kids[i], E, code, label);
|
|
121
|
+
if (cls === "universal" && tag === C.SET && !setOrderOk(members)) {
|
|
65
122
|
throw E(code, label + " has a SET whose members are in no canonical DER order (X.690 sec. 11.6 / X.680 sec. 8.6)");
|
|
66
123
|
}
|
|
67
|
-
return;
|
|
124
|
+
return s;
|
|
68
125
|
}
|
|
69
|
-
if (
|
|
70
|
-
var reader =
|
|
71
|
-
if (!reader) throw E(code, label + " of universal type " +
|
|
72
|
-
try { reader(
|
|
126
|
+
if (cls === "universal") {
|
|
127
|
+
var reader = C.readers[tag];
|
|
128
|
+
if (!reader) throw E(code, label + " of universal type " + tag + " has no strict DER content validator here");
|
|
129
|
+
try { reader(s); }
|
|
73
130
|
catch (e) { throw E(code, label + " is not a valid DER element for its type", e); }
|
|
74
131
|
}
|
|
132
|
+
return s;
|
|
75
133
|
}
|
|
76
134
|
|
|
77
135
|
// @enforced-by guard-shape-reinlined
|
|
@@ -79,7 +137,7 @@ function element(node, E, code, label) {
|
|
|
79
137
|
function tlv(content, E, code, label) {
|
|
80
138
|
if (!content || _sizeOf(content) === 0) throw E(code, label + " must be a non-empty DER element");
|
|
81
139
|
var node;
|
|
82
|
-
try { node =
|
|
140
|
+
try { node = _codec().decode(content); }
|
|
83
141
|
catch (e) { throw E(code, label + " must be exactly one well-formed DER element (no trailing data)", e); }
|
|
84
142
|
element(node, E, code, label);
|
|
85
143
|
return content;
|
|
@@ -115,8 +173,13 @@ function paramsAbsentOrNull(params) {
|
|
|
115
173
|
*/
|
|
116
174
|
function sameAlgorithmExactly(a, c) {
|
|
117
175
|
if (!a || !c || a.oid !== c.oid) return false;
|
|
118
|
-
|
|
119
|
-
|
|
176
|
+
/** @internal Each operand's `parameters` is read ONCE, into the value the absence test and the
|
|
177
|
+
* comparison share. Read twice, the first value decided which branch ran and the second was the
|
|
178
|
+
* one compared, so a getter answering a DER NULL and then `null` made an algorithm carrying
|
|
179
|
+
* parameters compare EQUAL to one carrying none. */
|
|
180
|
+
var araw = a.parameters, craw = c.parameters;
|
|
181
|
+
var ap = araw === undefined ? null : araw;
|
|
182
|
+
var cp = craw === undefined ? null : craw;
|
|
120
183
|
if (ap === null || cp === null) return ap === cp;
|
|
121
184
|
return _isBuffer(ap) && _isBuffer(cp) && _sizeOf(ap) === _sizeOf(cp) && _compare(ap, cp) === 0;
|
|
122
185
|
}
|
package/lib/guard-encoding.js
CHANGED
|
@@ -171,7 +171,8 @@ function _rawError(E) {
|
|
|
171
171
|
* characters passes a URL the rule was meant to refuse.
|
|
172
172
|
*
|
|
173
173
|
* @enforced-by guard-shape-reinlined
|
|
174
|
-
* @guard-shape
|
|
174
|
+
* @guard-shape charCodeAt\s*\(\s*[\w$]+\s*,\s*[\w$]+\s*\)\s*>\s*(?:0x7f|127)\b
|
|
175
|
+
*/
|
|
175
176
|
function isAsciiText(s) {
|
|
176
177
|
if (typeof s !== "string") return false;
|
|
177
178
|
for (var i = 0; i < s.length; i++) {
|
package/lib/guard-identifier.js
CHANGED
|
@@ -101,6 +101,28 @@ function snapshotOptions(obj, known) {
|
|
|
101
101
|
return out;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/** @internal The same one-read capture, for a caller that tells an ABSENT option from one present
|
|
105
|
+
* and holding `undefined`. `snapshotOptions` materializes every declared name, which is correct for
|
|
106
|
+
* a caller testing `!== undefined` and wrong for one testing presence: an omitted option becomes an
|
|
107
|
+
* own key, and a presence-based guard then sees an option the caller never passed. Measured, a bag
|
|
108
|
+
* omitting `transport` is ACCEPTED by `assertCallableOption` and REFUSED after `snapshotOptions`,
|
|
109
|
+
* because the materialized `undefined` is a transport that is not callable.
|
|
110
|
+
*
|
|
111
|
+
* Presence consults the prototype chain, because a caller supplying options through a defaults
|
|
112
|
+
* object is supplying them, and an enumeration omits an inherited FUNCTION member: that is how a
|
|
113
|
+
* copy once dropped a `transport` and fell through to opening a real connection where the caller
|
|
114
|
+
* had passed a fake one. The VALUE is still read once per name, which is what the snapshot is for.
|
|
115
|
+
* The two live side by side rather than behind a flag so a caller chooses between them knowingly. */
|
|
116
|
+
// @enforced-by guard-shape-reinlined
|
|
117
|
+
// @guard-shape if \(\w+ in \w+\) \w+\[\w+\] = \w+\[\w+\];
|
|
118
|
+
function snapshotPresentOptions(obj, known) {
|
|
119
|
+
var out = _create(null);
|
|
120
|
+
if (obj === null || obj === undefined) return out;
|
|
121
|
+
var o = _Object(obj);
|
|
122
|
+
_forEach(_getOwnPropertyNames(known), function (k) { if (k in o) out[k] = o[k]; });
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
104
126
|
/** @internal The option names a bag carries, read the way assertKnownKeys read them: every own
|
|
105
127
|
* string key, whatever its descriptor says. Object.keys reports only the enumerable ones, so an
|
|
106
128
|
* option written with `enumerable: false` clears the door and then vanishes from the read.
|
|
@@ -634,7 +656,8 @@ function _showSwitch(value) {
|
|
|
634
656
|
* the object, so an accessor further up the chain cannot answer either.
|
|
635
657
|
*
|
|
636
658
|
* @enforced-by guard-shape-reinlined
|
|
637
|
-
* @guard-shape
|
|
659
|
+
* @guard-shape \.\s*blocksPrivateAddresses\s*===\s*true
|
|
660
|
+
*/
|
|
638
661
|
function declaresOwn(obj, name) {
|
|
639
662
|
if (obj === null || obj === undefined) return false;
|
|
640
663
|
var d;
|
|
@@ -655,6 +678,7 @@ module.exports = intrinsic.freeze({
|
|
|
655
678
|
optionsObject: optionsObject,
|
|
656
679
|
refuseAccessorFields: refuseAccessorFields,
|
|
657
680
|
snapshotOptions: snapshotOptions,
|
|
681
|
+
snapshotPresentOptions: snapshotPresentOptions,
|
|
658
682
|
optionNames: optionNames,
|
|
659
683
|
ownOptions: ownOptions,
|
|
660
684
|
pickSettings: pickSettings,
|
package/lib/guard-name.js
CHANGED
|
@@ -198,6 +198,7 @@ function escapeControlBytes(str) {
|
|
|
198
198
|
var DN_SPECIAL = intrinsic.assign(intrinsic.create(null), { 0x2c: 1, 0x2b: 1, 0x22: 1, 0x5c: 1, 0x3c: 1, 0x3e: 1, 0x3b: 1 });
|
|
199
199
|
|
|
200
200
|
var _hexNibble = require("./guard-encoding").hexNibble;
|
|
201
|
+
var _isAsciiText = require("./guard-encoding").isAsciiText;
|
|
201
202
|
function _isAttrTypeStart(c) { return (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a); }
|
|
202
203
|
function _isAttrTypeChar(c) { return _isAttrTypeStart(c) || (c >= 0x30 && c <= 0x39) || c === 0x2d; }
|
|
203
204
|
function _isDigit(c) { return c >= 0x30 && c <= 0x39; }
|
|
@@ -335,10 +336,12 @@ function emailEqual(a, b) {
|
|
|
335
336
|
return lowerAscii(aHost) === lowerAscii(bHost) ? "match" : "no-match";
|
|
336
337
|
}
|
|
337
338
|
|
|
339
|
+
/** @internal An all-ASCII host, asked of the one place that scans for a byte above 0x7f, with the
|
|
340
|
+
* non-empty requirement kept here because a host part is not optional and `isAsciiText` answers
|
|
341
|
+
* `true` of the empty string. */
|
|
342
|
+
// @guard-via guard.encoding.isAsciiText
|
|
338
343
|
function _asciiHost(h) {
|
|
339
|
-
|
|
340
|
-
for (var i = 0; i < h.length; i++) if (_charCodeAt(h, i) > 0x7f) return false;
|
|
341
|
-
return true;
|
|
344
|
+
return h.length > 0 && _isAsciiText(h);
|
|
342
345
|
}
|
|
343
346
|
|
|
344
347
|
// @enforced-by guard-shape-reinlined
|
package/lib/guard-text.js
CHANGED
|
@@ -102,13 +102,17 @@ function keyOf(v) {
|
|
|
102
102
|
* by its type, which is what `showValue` would have said about it anyway.
|
|
103
103
|
*
|
|
104
104
|
* @enforced-by guard-shape-reinlined
|
|
105
|
-
* @guard-shape
|
|
105
|
+
* @guard-shape ([\w$]+)\s*&&\s*\1\.message\s*\?\s*\1\.message\s*:|([\w$]+)\.message\s*\|\|\s*\2\b
|
|
106
|
+
*/
|
|
106
107
|
function describeThrown(e) {
|
|
107
108
|
var m;
|
|
108
109
|
try { m = e === null || e === undefined ? undefined : e.message; }
|
|
109
110
|
catch (_readFailed) { return _thrownFallback(e); }
|
|
110
111
|
if (typeof m === "string" && m.length > 0) return m;
|
|
111
|
-
|
|
112
|
+
/** @internal An EMPTY message answers nothing readable, so it is described by its type like any
|
|
113
|
+
* other unreadable value. Returned as the empty string it was, it left the sentence built around
|
|
114
|
+
* it ending at its colon with nothing after it, which tells a reader less than the type does. */
|
|
115
|
+
if (m === undefined || m === null || m === "") return _thrownFallback(e);
|
|
112
116
|
try {
|
|
113
117
|
if (typeof m === "symbol") return intrinsic.String(m);
|
|
114
118
|
return "" + m;
|
package/lib/path-validate.js
CHANGED
|
@@ -3301,7 +3301,7 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
3301
3301
|
|
|
3302
3302
|
/**
|
|
3303
3303
|
* @primitive pki.path.build
|
|
3304
|
-
* @signature pki.path.build(leaf, opts) -> Promise<{ valid, path, trustAnchor, result, candidatesConsidered, aiaFetches }>
|
|
3304
|
+
* @signature pki.path.build(leaf, opts) -> Promise<{ valid, path, trustAnchor, result, candidatesConsidered, aiaFetches, depthLimited? }>
|
|
3305
3305
|
* @since 0.3.7
|
|
3306
3306
|
* @status stable
|
|
3307
3307
|
* @spec RFC 4158, RFC 5280
|
|
@@ -3347,7 +3347,12 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
3347
3347
|
* `validate` consumes (anchor-proximal first, leaf last, the anchor excluded). Fail-closed: bad
|
|
3348
3348
|
* options throw `path/bad-input`; no chain to any anchor throws `path/no-path`; chains that
|
|
3349
3349
|
* assemble but none validate return `{ valid:false }` with the best failing `validate` result;
|
|
3350
|
-
* the search bound throws `path/build-limit`.
|
|
3350
|
+
* the search bound throws `path/build-limit`. A `{ valid:false }` return carries
|
|
3351
|
+
* `depthLimited: true` when the depth cap stopped an expansion that still had candidates waiting,
|
|
3352
|
+
* which means the chain reported is the best one ASSEMBLED rather than the best one that exists: a
|
|
3353
|
+
* caller that treats `valid:false` as a verdict about the certificates should read that field and
|
|
3354
|
+
* raise `opts.maxDepth` instead. It is absent on a successful return and on a failing one the
|
|
3355
|
+
* search explored to its end. By default `build` is OFFLINE (zero network), so supply
|
|
3351
3356
|
* intermediates in `opts.candidates`. Set `opts.fetchAia: true` to opt in to fetching a MISSING intermediate
|
|
3352
3357
|
* from a certificate's Authority Information Access `caIssuers` URL (RFC 5280 sec. 4.2.2.1) over
|
|
3353
3358
|
* `pki.transport`: the fetch triggers only on a pool miss, every fetched certificate is UNTRUSTED pool material
|
|
@@ -3511,14 +3516,24 @@ async function build(leaf, opts) {
|
|
|
3511
3516
|
|
|
3512
3517
|
var BUILD_ONLY_OPT = { candidates: 1, intermediates: 1, trustAnchors: 1, maxDepth: 1, maxCandidatesConsidered: 1, validate: 1,
|
|
3513
3518
|
fetchAia: 1, transport: 1, tls: 1, maxAiaFetches: 1, maxAiaPerCert: 1, aiaTimeout: 1, maxResponseBytes: 1 };
|
|
3514
|
-
|
|
3519
|
+
/** @internal The names this door forwards by VALUE: every validate option that is not consumed by
|
|
3520
|
+
* the builder and not one of the switches typed above. Absence has to survive the copy, so the
|
|
3521
|
+
* presence-preserving snapshot is the one to take: an option the caller omitted stays omitted
|
|
3522
|
+
* rather than arriving at `validate` as a name holding `undefined`.
|
|
3523
|
+
* @guard-via guard.identifier.snapshotPresentOptions */
|
|
3524
|
+
var FORWARD_BY_VALUE = intrinsic.create(null);
|
|
3515
3525
|
intrinsic.forEach(intrinsic.keys(_VALIDATE_OPTS), function (k) {
|
|
3516
|
-
if (intrinsic.hasOwn(BUILD_ONLY_OPT, k)) return;
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3526
|
+
if (intrinsic.hasOwn(BUILD_ONLY_OPT, k) || intrinsic.hasOwn(forwardedSwitches, k)) return;
|
|
3527
|
+
FORWARD_BY_VALUE[k] = 1;
|
|
3528
|
+
});
|
|
3529
|
+
var forwarded = guard.identifier.snapshotPresentOptions(opts, FORWARD_BY_VALUE);
|
|
3530
|
+
/** @internal A switch this door typed is forwarded as the value it typed, overlaid after the copy.
|
|
3531
|
+
* Reading it off the options object again would ask a second time, and normalizing an anchor
|
|
3532
|
+
* between the two runs caller accessors, so the value `validate` is handed need not be the one that
|
|
3533
|
+
* passed here. All four are validate options and none is build-only, so the overlay adds no name
|
|
3534
|
+
* the loop above would have skipped. */
|
|
3535
|
+
intrinsic.forEach(intrinsic.keys(forwardedSwitches), function (k) {
|
|
3536
|
+
forwarded[k] = forwardedSwitches[k];
|
|
3522
3537
|
});
|
|
3523
3538
|
if (buildPermRaw != null) forwarded.initialPermittedSubtrees = buildPermSeeds;
|
|
3524
3539
|
if (buildExclRaw != null) forwarded.initialExcludedSubtrees = buildExclSeeds;
|
|
@@ -3533,6 +3548,10 @@ async function build(leaf, opts) {
|
|
|
3533
3548
|
var considered = 0;
|
|
3534
3549
|
var anyChainAssembled = false;
|
|
3535
3550
|
var bestFail = null;
|
|
3551
|
+
/** @internal Whether the depth bound stopped an expansion that had candidates waiting. A reached
|
|
3552
|
+
* bound is "stopped looking", not "the chain is bad", and the two left this verb returning the
|
|
3553
|
+
* same `valid:false`. */
|
|
3554
|
+
var depthStopped = false;
|
|
3536
3555
|
var success = null;
|
|
3537
3556
|
_assertUserInitialPolicySet(opts.userInitialPolicySet, "build");
|
|
3538
3557
|
var uipsDistinct = null, selectByPolicy = false;
|
|
@@ -3570,7 +3589,6 @@ async function build(leaf, opts) {
|
|
|
3570
3589
|
guard.list.append(accepted, racc);
|
|
3571
3590
|
} else if (!bestFail) bestFail = { path: rpath, trustAnchor: rframe.anchor, result: rres };
|
|
3572
3591
|
}
|
|
3573
|
-
if (rframe.hop >= maxDepth) continue;
|
|
3574
3592
|
var rscored = [];
|
|
3575
3593
|
for (var rpi = 0; rpi < pool.length; rpi++) {
|
|
3576
3594
|
if (nameMatchSoft(pool[rpi].subject.rdns, rframe.anchor.name.rdns) &&
|
|
@@ -3579,6 +3597,13 @@ async function build(leaf, opts) {
|
|
|
3579
3597
|
guard.list.append(rscored, { cand: pool[rpi], score: scoreCandidateReverse(pool[rpi], frontierCert, leafCert, opts.time) });
|
|
3580
3598
|
}
|
|
3581
3599
|
}
|
|
3600
|
+
/** @internal The anchor-first search answers the same question the forward one does, for the
|
|
3601
|
+
* same reason: candidates scored before the bound is consulted, so reaching it can say whether
|
|
3602
|
+
* it stopped anything. */
|
|
3603
|
+
if (rframe.hop >= maxDepth) {
|
|
3604
|
+
if (_anyExpandable(rscored, rframe)) depthStopped = true;
|
|
3605
|
+
continue;
|
|
3606
|
+
}
|
|
3582
3607
|
considered += _pushCandidates(rframe, rscored, rstack, counter, true);
|
|
3583
3608
|
}
|
|
3584
3609
|
} else {
|
|
@@ -3635,7 +3660,7 @@ async function build(leaf, opts) {
|
|
|
3635
3660
|
guard.list.append(accepted, acc);
|
|
3636
3661
|
} else if (!bestFail) bestFail = { path: frame.chain.slice(), trustAnchor: anchors[ai], result: res };
|
|
3637
3662
|
}
|
|
3638
|
-
if (success
|
|
3663
|
+
if (success) continue;
|
|
3639
3664
|
|
|
3640
3665
|
var childAki = childAkiKeyId(current);
|
|
3641
3666
|
var scored = [];
|
|
@@ -3645,6 +3670,16 @@ async function build(leaf, opts) {
|
|
|
3645
3670
|
ineligible: _forwardPathLenIneligible(pool[pi], frame.chain) });
|
|
3646
3671
|
}
|
|
3647
3672
|
}
|
|
3673
|
+
/** @internal The candidates are scored BEFORE the depth bound is consulted, so reaching the
|
|
3674
|
+
* bound can say whether it stopped anything. A chain whose last hop lands on the bound with
|
|
3675
|
+
* nothing left to expand was explored to its end, and a chain with candidates waiting was
|
|
3676
|
+
* TRUNCATED: the two produce the same `valid:false` otherwise, and a caller reading that cannot
|
|
3677
|
+
* tell a refused chain from one the search never assembled. Nothing is pushed either way, so the
|
|
3678
|
+
* reorder costs one pool pass on a frame at the bound and changes no outcome. */
|
|
3679
|
+
if (frame.hop >= maxDepth) {
|
|
3680
|
+
if (_anyExpandable(scored, frame)) depthStopped = true;
|
|
3681
|
+
continue;
|
|
3682
|
+
}
|
|
3648
3683
|
if (aiaCtx && frame.hop < maxDepth && aiaCtx.fetches < aiaCtx.maxFetches) {
|
|
3649
3684
|
guard.list.append(deferredAia, { chain: frame.chain, hop: frame.hop, keys: frame.keys, poolMark: pool.length, seq: deferSeq, fetched: false });
|
|
3650
3685
|
deferSeq += 1;
|
|
@@ -3666,11 +3701,29 @@ async function build(leaf, opts) {
|
|
|
3666
3701
|
return guard.verdict.of({ path: success.path, trustAnchor: success.trustAnchor, candidatesConsidered: considered, aiaFetches: aiaFetches });
|
|
3667
3702
|
}
|
|
3668
3703
|
if (anyChainAssembled) {
|
|
3669
|
-
|
|
3704
|
+
/** @internal `depthLimited` is carried only on the FAILING return, and only when the bound
|
|
3705
|
+
* stopped an expansion. On a success the bound hid nothing that mattered, and a caller reading
|
|
3706
|
+
* the field on every verdict would have to ask what it meant for a valid path. */
|
|
3707
|
+
var failed = { valid: false, path: bestFail.path, trustAnchor: bestFail.trustAnchor, result: bestFail.result, candidatesConsidered: considered, aiaFetches: aiaFetches };
|
|
3708
|
+
if (depthStopped) failed.depthLimited = true;
|
|
3709
|
+
return guard.verdict.of(failed);
|
|
3670
3710
|
}
|
|
3671
3711
|
throw E("path/no-path", "build: no certification path from the leaf to any configured trust anchor could be assembled");
|
|
3672
3712
|
}
|
|
3673
3713
|
|
|
3714
|
+
/** @internal Whether raising the depth cap could reach anything through these candidates. A
|
|
3715
|
+
* candidate already on the chain is skipped by `_pushCandidates` on the identity key, so a deeper
|
|
3716
|
+
* search would skip it too: counting it reports a truncated search where the search is exhausted,
|
|
3717
|
+
* which turns a chain this toolkit refuses into one it says it never finished reading. The identity
|
|
3718
|
+
* key and the frame's visited set are the same two the push uses, so the two cannot disagree about
|
|
3719
|
+
* which candidate is reachable. */
|
|
3720
|
+
function _anyExpandable(scored, frame) {
|
|
3721
|
+
for (var i = 0; i < scored.length; i++) {
|
|
3722
|
+
if (!intrinsic.setHas(frame.keys, identityKey(scored[i].cand))) return true;
|
|
3723
|
+
}
|
|
3724
|
+
return false;
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3674
3727
|
/** @internal The fetching checker composes the two gates below, so it loads after this module
|
|
3675
3728
|
* rather than beside it. The require sits in the call so the cycle resolves at first use, which is
|
|
3676
3729
|
* the documented reason an inline require is permitted. */
|
package/lib/sigstore.js
CHANGED
|
@@ -1417,8 +1417,11 @@ async function _verifyChain(leaf, chainDers, roots, timeMs) {
|
|
|
1417
1417
|
* reported `chain-invalid`. `build` ranks candidates by the RFC 4158 sec. 3.5 heuristics, the key
|
|
1418
1418
|
* identifier among them, EXCLUDING none, and bounds the search by total candidate expansions rather
|
|
1419
1419
|
* than by how many paths it has collected, so a later candidate stays reachable and an exhausted
|
|
1420
|
-
* budget is reported as `path/build-limit` rather than as a verdict about the chain
|
|
1421
|
-
*
|
|
1420
|
+
* budget is reported as `path/build-limit` rather than as a verdict about the chain, and a depth bound
|
|
1421
|
+
* that stopped an expansion as `depthLimited` on the failing return. Both reach the incomplete code
|
|
1422
|
+
* below, because a bound the search reached says nothing about the certificates it never read. Every
|
|
1423
|
+
* assembled path still flows through `pki.path.validate`, so this delegates the search and duplicates
|
|
1424
|
+
* no check.
|
|
1422
1425
|
* The anchors go into the pool as well as the trust store, because a cross-signed root appears in a
|
|
1423
1426
|
* chain as an intermediate under another anchor. */
|
|
1424
1427
|
var anchorTuples = _map(inWindow, function (r) {
|
|
@@ -1462,6 +1465,14 @@ async function _verifyChain(leaf, chainDers, roots, timeMs) {
|
|
|
1462
1465
|
throw _err("sigstore/chain-invalid", "the Fulcio certificate chain failed validation: " + e.message, e);
|
|
1463
1466
|
}
|
|
1464
1467
|
if (!built.valid) {
|
|
1468
|
+
/** @internal The third outcome that means "stopped looking". `build` reports `depthLimited` when
|
|
1469
|
+
* its depth bound stopped an expansion that had candidates waiting, so the chain it hands back
|
|
1470
|
+
* is the best one it ASSEMBLED rather than the best one that exists. Reporting that as invalid
|
|
1471
|
+
* denies a bundle whose path the search never reached, which is the same denial the candidate
|
|
1472
|
+
* bound above avoids, so it answers with the same incomplete code. */
|
|
1473
|
+
if (built.depthLimited) {
|
|
1474
|
+
throw _err("sigstore/chain-incomplete", "no chain to a caller-supplied Fulcio anchor was established before the path search reached its depth bound");
|
|
1475
|
+
}
|
|
1465
1476
|
throw _err("sigstore/chain-invalid", "the Fulcio certificate chain is not valid as of the log time");
|
|
1466
1477
|
}
|
|
1467
1478
|
/** @internal `path` is anchor-proximal first and leaf last with the anchor excluded, so the certificate
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:f7650379-6444-462e-bd4b-7cfbdc879bc7",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-10-
|
|
8
|
+
"timestamp": "2026-10-06T04:32:24.064Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.8.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.8.55",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.55",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.8.55",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.8.
|
|
57
|
+
"ref": "@blamejs/pki@0.8.55",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|