@blamejs/pki 0.8.53 → 0.8.54
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 +17 -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/guard-der.js +94 -31
- package/lib/guard-encoding.js +2 -1
- package/lib/guard-identifier.js +2 -1
- package/lib/guard-name.js +6 -3
- package/lib/guard-text.js +6 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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.54 — 2026-10-06
|
|
8
|
+
|
|
9
|
+
Refuse the universal tags X.680 assigns to no type, and tell a caller which those are.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `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.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `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.
|
|
18
|
+
- `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.
|
|
19
|
+
- `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.
|
|
20
|
+
- `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`.
|
|
21
|
+
- 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.
|
|
22
|
+
- 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.
|
|
23
|
+
|
|
7
24
|
## v0.8.53 — 2026-10-06
|
|
8
25
|
|
|
9
26
|
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/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
|
@@ -634,7 +634,8 @@ function _showSwitch(value) {
|
|
|
634
634
|
* the object, so an accessor further up the chain cannot answer either.
|
|
635
635
|
*
|
|
636
636
|
* @enforced-by guard-shape-reinlined
|
|
637
|
-
* @guard-shape
|
|
637
|
+
* @guard-shape \.\s*blocksPrivateAddresses\s*===\s*true
|
|
638
|
+
*/
|
|
638
639
|
function declaresOwn(obj, name) {
|
|
639
640
|
if (obj === null || obj === undefined) return false;
|
|
640
641
|
var d;
|
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/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:4fbfd882-9aa0-491d-b309-378e286e0976",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-10-
|
|
8
|
+
"timestamp": "2026-10-06T03:34:15.912Z",
|
|
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.54",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.54",
|
|
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.54",
|
|
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.54",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|