@blamejs/pki 0.5.31 → 0.5.33
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 +25 -1
- package/README.md +3 -3
- package/lib/acme.js +347 -90
- package/lib/asn1-der.js +93 -33
- package/lib/cmc-build.js +19 -8
- package/lib/cmc-verify.js +4 -4
- package/lib/cmp-build.js +36 -6
- package/lib/cmp-session.js +5 -5
- package/lib/cmp-verify.js +86 -16
- package/lib/cms-compress.js +2 -2
- package/lib/cms-decrypt.js +3 -3
- package/lib/cms-encrypt.js +1 -1
- package/lib/cms-sign.js +214 -93
- package/lib/cms-verify.js +363 -237
- package/lib/crl-sign.js +4 -4
- package/lib/crmf-sign.js +2 -1
- package/lib/csr-sign.js +1 -4
- package/lib/ct.js +8 -4
- package/lib/est.js +234 -34
- package/lib/framework-error.js +20 -6
- package/lib/guard-bytes.js +56 -4
- package/lib/guard-intrinsic.js +13 -0
- package/lib/http-digest.js +69 -16
- package/lib/http-retry-after.js +68 -24
- package/lib/http-transport.js +60 -6
- package/lib/inspect.js +13 -2
- package/lib/key.js +2 -2
- package/lib/lint.js +1 -1
- package/lib/mime.js +27 -4
- package/lib/ocsp.js +7 -7
- package/lib/oid.js +16 -0
- package/lib/path-validate.js +76 -18
- package/lib/pbes2.js +2 -2
- package/lib/pkcs12-build.js +2 -2
- package/lib/pki-build.js +6 -6
- package/lib/rfc3339.js +75 -17
- package/lib/schema-c509.js +36 -9
- package/lib/schema-cmc.js +3 -3
- package/lib/schema-pkix.js +174 -25
- package/lib/schema-tsp.js +10 -1
- package/lib/sign-scheme.js +1 -1
- package/lib/sigstore.js +52 -9
- package/lib/smime.js +86 -6
- package/lib/trust.js +114 -14
- package/lib/tsp-sign.js +39 -27
- package/lib/validator-tpm.js +2 -2
- package/lib/webauthn-mds.js +24 -6
- package/lib/webauthn.js +3 -3
- package/lib/webcrypto.js +80 -1
- package/lib/x509-sign.js +1 -1
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
package/lib/schema-cmc.js
CHANGED
|
@@ -685,7 +685,7 @@ function _finishBody(body, where) {
|
|
|
685
685
|
* d.controls.length; // 0 -- present and empty, which is legal
|
|
686
686
|
*/
|
|
687
687
|
function parsePkiData(input) {
|
|
688
|
-
var root = asn1.decode(
|
|
688
|
+
var root = asn1.decode(guard.bytes.isByteSource(input) ? guard.bytes.source(input, E, "cmc/bad-input", "the CMC DER") : Buffer.from(input));
|
|
689
689
|
var body = schema.walk(PKI_DATA, root, NS).result;
|
|
690
690
|
body.bytes = root.bytes;
|
|
691
691
|
return _finishBody(body, "pkiData");
|
|
@@ -711,7 +711,7 @@ function parsePkiData(input) {
|
|
|
711
711
|
* r.statuses.length; // 0 -- no status control means success is assumed
|
|
712
712
|
*/
|
|
713
713
|
function parsePkiResponse(input) {
|
|
714
|
-
var root = asn1.decode(
|
|
714
|
+
var root = asn1.decode(guard.bytes.isByteSource(input) ? guard.bytes.source(input, E, "cmc/bad-input", "the CMC DER") : Buffer.from(input));
|
|
715
715
|
var body = schema.walk(PKI_RESPONSE, root, NS).result;
|
|
716
716
|
body.bytes = root.bytes;
|
|
717
717
|
return _finishBody(body, "pkiResponse");
|
|
@@ -752,7 +752,7 @@ function parse(input) {
|
|
|
752
752
|
// encapContentInfo is the field this function goes on to read, so requiring it
|
|
753
753
|
// is the same question the code below already asks.
|
|
754
754
|
var parsedCms = (input && typeof input === "object" && !ArrayBuffer.isView(input) &&
|
|
755
|
-
!(input
|
|
755
|
+
!guard.bytes.isByteSource(input) && input.encapContentInfo != null)
|
|
756
756
|
? input : cms.parse(input);
|
|
757
757
|
// The CARRIER is part of the identity, not just the encapsulated content type.
|
|
758
758
|
// RFC 5272 sec. 3.2 / 4.2 admit a SignedData or an AuthenticatedData; other CMS
|
package/lib/schema-pkix.js
CHANGED
|
@@ -42,10 +42,8 @@ var _strIndexOf = intrinsic.stringIndexOf;
|
|
|
42
42
|
var _bufToString = intrinsic.uncurry(Buffer.prototype.toString);
|
|
43
43
|
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
44
44
|
var _charAt = intrinsic.uncurry(String.prototype.charAt);
|
|
45
|
-
var
|
|
45
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
46
46
|
var _split = intrinsic.uncurry(String.prototype.split);
|
|
47
|
-
var _reExec = intrinsic.uncurry(RegExp.prototype.exec);
|
|
48
|
-
var _reTest = intrinsic.uncurry(RegExp.prototype.test);
|
|
49
47
|
var _create = intrinsic.create;
|
|
50
48
|
var _isUint8Array = intrinsic.types.isUint8Array;
|
|
51
49
|
var _bigIntToString = intrinsic.bigIntToString;
|
|
@@ -54,12 +52,96 @@ var _weakSet = intrinsic.weakSet;
|
|
|
54
52
|
var _WeakMapI = intrinsic.WeakMap;
|
|
55
53
|
var _arrayFrom = Array.from;
|
|
56
54
|
var _getUTCFullYear = intrinsic.uncurry(Date.prototype.getUTCFullYear);
|
|
55
|
+
|
|
56
|
+
// The JavaScript regex `\s` class, decided by code point, so the whitespace tests below match
|
|
57
|
+
// exactly what `/\s/` and `/\S/` matched with no caller-replaceable match protocol standing in for
|
|
58
|
+
// them. Set = ECMAScript WhiteSpace + LineTerminator: TAB LF VT FF CR SP, U+00A0, U+1680,
|
|
59
|
+
// U+2000..U+200A, U+2028, U+2029, U+202F, U+205F, U+3000, U+FEFF.
|
|
60
|
+
function _isJsWhitespace(c) {
|
|
61
|
+
return c === 0x09 || c === 0x0a || c === 0x0b || c === 0x0c || c === 0x0d || c === 0x20 ||
|
|
62
|
+
c === 0xa0 || c === 0x1680 || (c >= 0x2000 && c <= 0x200a) || c === 0x2028 || c === 0x2029 ||
|
|
63
|
+
c === 0x202f || c === 0x205f || c === 0x3000 || c === 0xfeff;
|
|
64
|
+
}
|
|
65
|
+
function _hasWhitespace(s) {
|
|
66
|
+
for (var i = 0; i < s.length; i++) { if (_isJsWhitespace(_charCodeAt(s, i))) return true; }
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
function _hasNonWhitespace(s) {
|
|
70
|
+
for (var i = 0; i < s.length; i++) { if (!_isJsWhitespace(_charCodeAt(s, i))) return true; }
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
// Charset validation by character scan (the shared home for the `/^[chars]+$/`-family regexes format
|
|
74
|
+
// modules used to carry): `charTable` builds a code-point membership table from a string of the allowed
|
|
75
|
+
// characters, and `allCharsIn` checks that a NON-EMPTY string is composed only of them (`+`, not `*`).
|
|
76
|
+
// One data-table loop, not a per-charset inlined character-class chain.
|
|
77
|
+
function _charTable(chars) { var t = []; for (var i = 0; i < chars.length; i++) t[_charCodeAt(chars, i)] = true; return t; }
|
|
78
|
+
function _allCharsIn(s, table) {
|
|
79
|
+
if (s.length === 0) return false;
|
|
80
|
+
for (var i = 0; i < s.length; i++) { if (!table[_charCodeAt(s, i)]) return false; }
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
57
83
|
// ct is a leaf format module (requires only the codec / oid / constants / error
|
|
58
84
|
// core, never pkix) so the SCT-list extension VALUE decoder registers here as a
|
|
59
85
|
// registry row, not a switch. The one-directional edge keeps the layering acyclic.
|
|
60
86
|
var ct = require("./ct");
|
|
61
87
|
|
|
62
|
-
|
|
88
|
+
// PEM armor markers, matched literally so no regex -- with its live, replaceable match protocol and
|
|
89
|
+
// the backreference the END repeats -- stands between the parser and the wire.
|
|
90
|
+
var _PEM_BEGIN = "-----BEGIN ";
|
|
91
|
+
var _PEM_END = "-----END ";
|
|
92
|
+
var _PEM_DASHES = "-----";
|
|
93
|
+
|
|
94
|
+
// The next PEM block at or after `from`: "-----BEGIN <label>-----<body>-----END <label>-----" where
|
|
95
|
+
// the label is one or more of [A-Z0-9 ] (RFC 7468 label octets, spaces included) and the END label
|
|
96
|
+
// repeats the BEGIN label exactly (the backreference). Returns { index, label, body, end } -- `index`
|
|
97
|
+
// the BEGIN offset, `end` the offset past the closing dashes -- or null when no such block remains.
|
|
98
|
+
// A "-----BEGIN " with no valid label or no matching END is skipped, mirroring the regex trying each
|
|
99
|
+
// start position; the body is the shortest span to the first matching END (the lazy quantifier).
|
|
100
|
+
function _findPemBlock(text, from) {
|
|
101
|
+
var searchFrom = from;
|
|
102
|
+
for (;;) {
|
|
103
|
+
var bi = _strIndexOf(text, _PEM_BEGIN, searchFrom);
|
|
104
|
+
if (bi === -1) return null;
|
|
105
|
+
var p = bi + _PEM_BEGIN.length, ls = p;
|
|
106
|
+
while (p < text.length) {
|
|
107
|
+
var c = _charCodeAt(text, p);
|
|
108
|
+
if (!((c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c === 32)) break; // A-Z 0-9 space
|
|
109
|
+
p += 1;
|
|
110
|
+
}
|
|
111
|
+
if (p === ls || _strSlice(text, p, p + 5) !== _PEM_DASHES) { searchFrom = bi + 1; continue; }
|
|
112
|
+
var label = _strSlice(text, ls, p);
|
|
113
|
+
var bodyStart = p + 5;
|
|
114
|
+
var endMarker = _PEM_END + label + _PEM_DASHES;
|
|
115
|
+
var ei = _strIndexOf(text, endMarker, bodyStart);
|
|
116
|
+
if (ei === -1) { searchFrom = bi + 1; continue; }
|
|
117
|
+
return { index: bi, label: label, body: _strSlice(text, bodyStart, ei), end: ei + endMarker.length };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// All ASCII CR / LF / TAB / SPACE removed (the base64 body's permitted layout whitespace).
|
|
122
|
+
function _stripB64Whitespace(s) {
|
|
123
|
+
var out = "", runStart = 0, i;
|
|
124
|
+
for (i = 0; i < s.length; i++) {
|
|
125
|
+
var c = _charCodeAt(s, i);
|
|
126
|
+
if (c === 13 || c === 10 || c === 9 || c === 32) {
|
|
127
|
+
if (i > runStart) out += _strSlice(s, runStart, i);
|
|
128
|
+
runStart = i + 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (i > runStart) out += _strSlice(s, runStart, i);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A base64 string wrapped to 64-column lines joined by LF, no trailing LF (matching (.{64})->"$1\n"
|
|
136
|
+
// then a trailing-newline strip; base64 output carries no newline of its own to strip).
|
|
137
|
+
function _wrap64(s) {
|
|
138
|
+
var out = "";
|
|
139
|
+
for (var i = 0; i < s.length; i += 64) {
|
|
140
|
+
if (i > 0) out += "\n";
|
|
141
|
+
out += _strSlice(s, i, i + 64);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
63
145
|
|
|
64
146
|
// ---- shared parse-entry ----------------------------------------------
|
|
65
147
|
// Input handling, PEM unwrapping (with the size cap), DER-decode wrapping, and
|
|
@@ -80,10 +162,10 @@ function pemDecode(text, label, PemError) {
|
|
|
80
162
|
text = guard.text.decode(text, constants.LIMITS.PEM_MAX_BYTES, PemError, {
|
|
81
163
|
charset: "latin1", tooLarge: "pem/too-large", badInput: "pem/bad-input", label: "PEM input",
|
|
82
164
|
});
|
|
83
|
-
var m =
|
|
165
|
+
var m = _findPemBlock(text, 0);
|
|
84
166
|
if (!m) throw new PemError("pem/no-block", "no PEM block found");
|
|
85
|
-
if (label && m
|
|
86
|
-
var b64 =
|
|
167
|
+
if (label && m.label !== label) throw new PemError("pem/label-mismatch", "expected " + _stringify(label) + " block, got " + _stringify(m.label));
|
|
168
|
+
var b64 = _stripB64Whitespace(m.body);
|
|
87
169
|
// Strict canonical base64 (RFC 4648 sec. 3.5) via the shared encoding guard.
|
|
88
170
|
return guard.encoding.base64(b64, null, function (c, msg) { return new PemError(c, msg); }, "pem/bad-base64", "PEM base64 body");
|
|
89
171
|
}
|
|
@@ -99,51 +181,99 @@ function pemDecodeAll(text, label, PemError) {
|
|
|
99
181
|
text = guard.text.decode(text, constants.LIMITS.PEM_MAX_BYTES, PemError, {
|
|
100
182
|
charset: "latin1", tooLarge: "pem/too-large", badInput: "pem/bad-input", label: "PEM input",
|
|
101
183
|
});
|
|
102
|
-
var re = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/g;
|
|
103
184
|
var blocks = [];
|
|
104
185
|
var lastEnd = 0, m;
|
|
105
|
-
while ((m =
|
|
106
|
-
if (
|
|
107
|
-
if (m
|
|
108
|
-
var b64 =
|
|
186
|
+
while ((m = _findPemBlock(text, lastEnd)) !== null) {
|
|
187
|
+
if (_hasNonWhitespace(_strSlice(text, lastEnd, m.index))) throw new PemError("pem/explanatory-text", "explanatory text is not permitted around PEM blocks (RFC 8555 sec. 9.1)");
|
|
188
|
+
if (m.label !== label) throw new PemError("pem/label-mismatch", "expected " + _stringify(label) + " block, got " + _stringify(m.label));
|
|
189
|
+
var b64 = _stripB64Whitespace(m.body);
|
|
109
190
|
_push(blocks, guard.encoding.base64(b64, null, function (c, msg) { return new PemError(c, msg); }, "pem/bad-base64", "PEM base64 body"));
|
|
110
|
-
lastEnd =
|
|
191
|
+
lastEnd = m.end;
|
|
111
192
|
}
|
|
112
193
|
if (blocks.length === 0) throw new PemError("pem/no-block", "no PEM block found");
|
|
113
|
-
if (
|
|
194
|
+
if (_hasNonWhitespace(_strSlice(text, lastEnd))) throw new PemError("pem/explanatory-text", "explanatory text is not permitted after the PEM chain (RFC 8555 sec. 9.1)");
|
|
114
195
|
return blocks;
|
|
115
196
|
}
|
|
116
197
|
|
|
117
|
-
// The label must be re-readable by this module's own
|
|
198
|
+
// The label must be re-readable by this module's own PEM label scan (uppercase
|
|
118
199
|
// alphanumeric words separated by single spaces, a subset of the RFC 7468
|
|
119
200
|
// sec. 3 label grammar). Anything else (lowercase, a leading/trailing space,
|
|
120
201
|
// an embedded "-----" or newline) would emit armor pemDecode itself rejects
|
|
121
202
|
// or corrupt the armor grammar: a config-time authoring fault, thrown here.
|
|
203
|
+
// The armor label grammar, decided character by character (no regex protocol on the emit path):
|
|
204
|
+
// one or more A-Z0-9 words joined by single spaces -- so no leading/trailing space and no doubled
|
|
205
|
+
// space, and every other character in [A-Z0-9].
|
|
206
|
+
function _isPemLabel(s) {
|
|
207
|
+
var n = s.length;
|
|
208
|
+
if (n === 0) return false;
|
|
209
|
+
for (var i = 0; i < n; i++) {
|
|
210
|
+
var ch = _charAt(s, i);
|
|
211
|
+
if (ch === " ") {
|
|
212
|
+
if (i === 0 || i === n - 1 || _charAt(s, i - 1) === " ") return false;
|
|
213
|
+
} else if (!((ch >= "A" && ch <= "Z") || (ch >= "0" && ch <= "9"))) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
122
220
|
function pemEncode(der, label, PemError) {
|
|
123
|
-
if (typeof label !== "string" || !
|
|
221
|
+
if (typeof label !== "string" || !_isPemLabel(label)) {
|
|
124
222
|
throw new PemError("pem/bad-label", "pemEncode requires an uppercase A-Z0-9 label with single spaces");
|
|
125
223
|
}
|
|
126
224
|
// Re-view through the byte guard: a string input would silently utf8-armor into
|
|
127
225
|
// a bogus PEM, and a detached-backed Buffer would armor an empty body; both
|
|
128
226
|
// fail closed (a Uint8Array / Buffer of real DER re-views cleanly).
|
|
129
227
|
var buf = guard.bytes.view(der, PemError, "pem/bad-input", "pemEncode DER input");
|
|
130
|
-
var b64 =
|
|
228
|
+
var b64 = _wrap64(_bufToString(buf, "base64"));
|
|
131
229
|
return "-----BEGIN " + label + "-----\n" + b64 + "\n-----END " + label + "-----\n";
|
|
132
230
|
}
|
|
133
231
|
|
|
232
|
+
// Only the base64 alphabet (A-Z a-z 0-9 + / =) kept, every other character dropped -- the lenient
|
|
233
|
+
// filter of the historic signer-certificate path, by character scan.
|
|
234
|
+
function _keepBase64(s) {
|
|
235
|
+
var out = "", runStart = 0, i;
|
|
236
|
+
for (i = 0; i < s.length; i++) {
|
|
237
|
+
var c = _charCodeAt(s, i);
|
|
238
|
+
if (!((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 43 || c === 47 || c === 61)) {
|
|
239
|
+
if (i > runStart) out += _strSlice(s, runStart, i);
|
|
240
|
+
runStart = i + 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (i > runStart) out += _strSlice(s, runStart, i);
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// The @internal LENIENT PEM decode of a fixed-`label` block (the historic signer-certificate path,
|
|
248
|
+
// distinct from the strict canonical `pemDecode` above): the body of the first
|
|
249
|
+
// "-----BEGIN <label>-----...-----END <label>-----" block with any non-base64 character stripped,
|
|
250
|
+
// decoded through Node's lenient base64. Returns the DER Buffer, or null when no such block exists so
|
|
251
|
+
// the caller keeps its own domain error. NOT for verified input -- pemDecode is the fail-closed door.
|
|
252
|
+
function pemDecodeLenient(text, label) {
|
|
253
|
+
var begin = _PEM_BEGIN + label + _PEM_DASHES;
|
|
254
|
+
var end = _PEM_END + label + _PEM_DASHES;
|
|
255
|
+
var bi = _strIndexOf(text, begin);
|
|
256
|
+
if (bi === -1) return null;
|
|
257
|
+
var bodyStart = bi + begin.length;
|
|
258
|
+
var ei = _strIndexOf(text, end, bodyStart);
|
|
259
|
+
if (ei === -1) return null;
|
|
260
|
+
return _bufferFrom(_keepBase64(_strSlice(text, bodyStart, ei)), "base64");
|
|
261
|
+
}
|
|
262
|
+
|
|
134
263
|
// Coerce parse input to DER bytes. A PEM string OR a PEM Buffer (a .pem file
|
|
135
264
|
// read with fs.readFileSync) is unwrapped with opts.pemLabel; a DER Buffer or
|
|
136
265
|
// Uint8Array is taken as bytes. opts: { pemLabel, PemError, ErrorClass, prefix }.
|
|
137
266
|
function coerceToDer(input, opts) {
|
|
138
267
|
if (typeof input === "string") return pemDecode(input, opts.pemLabel, opts.PemError);
|
|
139
|
-
if (
|
|
140
|
-
// Route
|
|
141
|
-
// detached backing store fails closed here for every format that composes
|
|
142
|
-
//
|
|
143
|
-
|
|
268
|
+
if (guard.bytes.isByteSource(input)) {
|
|
269
|
+
// Route any DER BufferSource (Buffer, typed array, DataView, or ArrayBuffer) through the shared byte
|
|
270
|
+
// guard so a detached backing store fails closed here for every format that composes this boundary
|
|
271
|
+
// (x509/crl/csr/pkcs8/cms/pkcs12), not per-format, and its byte view is read from the slot -- a
|
|
272
|
+
// cross-realm ArrayBuffer or an offset DataView reaches the decoder exactly as a Buffer does.
|
|
273
|
+
var buf = guard.bytes.source(input, opts.ErrorClass, opts.prefix + "/bad-input", "parse");
|
|
144
274
|
return _isPemArmor(buf) ? pemDecode(buf, opts.pemLabel, opts.PemError) : buf;
|
|
145
275
|
}
|
|
146
|
-
throw new opts.ErrorClass(opts.prefix + "/bad-input", "parse expects a DER Buffer or a PEM string");
|
|
276
|
+
throw new opts.ErrorClass(opts.prefix + "/bad-input", "parse expects a DER BufferSource (Buffer, typed array, DataView, or ArrayBuffer) or a PEM string");
|
|
147
277
|
}
|
|
148
278
|
|
|
149
279
|
// Does a Buffer carry PEM armor (a .pem read with fs.readFileSync) instead of
|
|
@@ -1671,10 +1801,24 @@ function signedEnvelope(ns, tbsSchema, opts) {
|
|
|
1671
1801
|
// whitespace, no leading/trailing dot, no empty label, no underscore, LDH labels (1-63 octets, no leading /
|
|
1672
1802
|
// trailing hyphen), <= 253 octets, an optional leftmost "*" wildcard. Returns a reason string, or null when
|
|
1673
1803
|
// well-formed. The shared home so the linter AND an identity comparator (case-fold only a valid dNSName) agree.
|
|
1804
|
+
// One LDH label (RFC 1034 preferred syntax), decided character by character: every character an
|
|
1805
|
+
// ASCII letter/digit/hyphen, and a hyphen neither first nor last. The caller has already rejected
|
|
1806
|
+
// any label carrying a dot, whitespace, or underscore, so only the LDH shape remains to check.
|
|
1807
|
+
function _isLdhLabel(s) {
|
|
1808
|
+
var n = s.length;
|
|
1809
|
+
if (n === 0) return false;
|
|
1810
|
+
for (var i = 0; i < n; i++) {
|
|
1811
|
+
var ch = _charAt(s, i);
|
|
1812
|
+
if ((ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z") || (ch >= "0" && ch <= "9")) continue;
|
|
1813
|
+
if (ch === "-") { if (i === 0 || i === n - 1) return false; } else return false;
|
|
1814
|
+
}
|
|
1815
|
+
return true;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1674
1818
|
function dnsNameProblem(s) {
|
|
1675
1819
|
if (typeof s !== "string" || !s.length) return "empty";
|
|
1676
1820
|
if (s.length > 253) return "exceeds 253 octets";
|
|
1677
|
-
if (
|
|
1821
|
+
if (_hasWhitespace(s)) return "whitespace";
|
|
1678
1822
|
if (_charAt(s, 0) === "." || _charAt(s, s.length - 1) === ".") return "leading/trailing dot";
|
|
1679
1823
|
if (_strIndexOf(s, "_") !== -1) return "underscore forbidden in dNSName";
|
|
1680
1824
|
var labels = _split(s, ".");
|
|
@@ -1686,7 +1830,7 @@ function dnsNameProblem(s) {
|
|
|
1686
1830
|
if (labels.length < 2) return "bare wildcard";
|
|
1687
1831
|
continue;
|
|
1688
1832
|
}
|
|
1689
|
-
if (!
|
|
1833
|
+
if (!_isLdhLabel(label)) return "invalid label syntax";
|
|
1690
1834
|
}
|
|
1691
1835
|
return null;
|
|
1692
1836
|
}
|
|
@@ -1695,7 +1839,12 @@ module.exports = {
|
|
|
1695
1839
|
dnsNameProblem: dnsNameProblem,
|
|
1696
1840
|
pemDecode: pemDecode,
|
|
1697
1841
|
pemDecodeAll: pemDecodeAll,
|
|
1842
|
+
pemDecodeLenient: pemDecodeLenient,
|
|
1698
1843
|
pemEncode: pemEncode,
|
|
1844
|
+
isJsWhitespace: _isJsWhitespace, // the shared JS regex \s code-point predicate for char-scan parsers
|
|
1845
|
+
stripBase64Whitespace: _stripB64Whitespace, // shared CR/LF/TAB/SP strip for base64 bodies (PEM, MIME)
|
|
1846
|
+
charTable: _charTable, // build a code-point membership table from a string of allowed characters
|
|
1847
|
+
allCharsIn: _allCharsIn, // is a NON-EMPTY string composed only of table members (the /^[chars]+$/ family)
|
|
1699
1848
|
coerceToDer: coerceToDer,
|
|
1700
1849
|
decodeRoot: decodeRoot,
|
|
1701
1850
|
runParse: runParse,
|
package/lib/schema-tsp.js
CHANGED
|
@@ -177,7 +177,16 @@ var TST_INFO = schema.seq([
|
|
|
177
177
|
// genTime is surfaced as a millisecond-precision Date; genTimeFraction is the
|
|
178
178
|
// exact fractional-seconds digits (lossless for sub-millisecond precision), or
|
|
179
179
|
// null when genTime carries no fraction.
|
|
180
|
-
genTimeFraction: (function () {
|
|
180
|
+
genTimeFraction: (function () {
|
|
181
|
+
// /\.(\d+)Z$/ : the fractional-seconds digits before a trailing "Z" (the "." and "Z" excluded), or null.
|
|
182
|
+
var s = m.fields.genTime.node.content.toString("latin1"), n = s.length;
|
|
183
|
+
if (n < 3 || s.charAt(n - 1) !== "Z") return null;
|
|
184
|
+
var dot = s.lastIndexOf(".");
|
|
185
|
+
if (dot < 0 || dot > n - 3) return null;
|
|
186
|
+
var frac = s.slice(dot + 1, n - 1);
|
|
187
|
+
for (var fi = 0; fi < frac.length; fi++) { var cc = frac.charCodeAt(fi); if (cc < 48 || cc > 57) return null; }
|
|
188
|
+
return frac;
|
|
189
|
+
})(),
|
|
181
190
|
accuracy: m.fields.accuracy.present ? m.fields.accuracy.value.result : null,
|
|
182
191
|
ordering: m.fields.ordering.present ? m.fields.ordering.value : false,
|
|
183
192
|
nonce: m.fields.nonce.present ? m.fields.nonce.value : null,
|
package/lib/sign-scheme.js
CHANGED
|
@@ -242,7 +242,7 @@ function _normPkcs8(k, label, E) {
|
|
|
242
242
|
throw E("bad-input", label + " must be a PKCS#8 DER Buffer, Uint8Array, or PEM string");
|
|
243
243
|
}
|
|
244
244
|
function _normCompositeKeys(key, comp, E) {
|
|
245
|
-
if (!key || typeof key !== "object" || _isBuffer(key) || _isU8(key) || key.mldsa == null || key.trad == null) {
|
|
245
|
+
if (!key || typeof key !== "object" || _isBuffer(key) || _isU8(key) || key.mldsa == null || key.trad == null) { // allow:byte-source-narrow -- composite-key discrimination: a raw-key BufferSource (incl an ArrayBuffer, which is neither) is correctly classed non-composite and normalized by _normPkcs8
|
|
246
246
|
throw E("bad-input", "a composite " + comp.name + " signer key must be { mldsa: <PKCS#8>, trad: <PKCS#8> }");
|
|
247
247
|
}
|
|
248
248
|
return { mldsa: _normPkcs8(key.mldsa, "the composite ML-DSA component key", E), trad: _normPkcs8(key.trad, "the composite traditional component key", E) };
|
package/lib/sigstore.js
CHANGED
|
@@ -31,6 +31,7 @@ var constants = require("./constants");
|
|
|
31
31
|
var asn1 = require("./asn1-der");
|
|
32
32
|
var guard = require("./guard-all");
|
|
33
33
|
var x509 = require("./schema-x509");
|
|
34
|
+
var pkix = require("./schema-pkix");
|
|
34
35
|
var pathValidate = require("./path-validate");
|
|
35
36
|
var merkle = require("./merkle");
|
|
36
37
|
var oid = require("./oid");
|
|
@@ -43,9 +44,51 @@ function _err(code, message, cause) { return new SigstoreError(code, message, ca
|
|
|
43
44
|
var JSON_MAX = C.LIMITS.JSON_MAX_BYTES;
|
|
44
45
|
var SP = Buffer.from(" ", "ascii");
|
|
45
46
|
var DSSEV1 = Buffer.from("DSSEv1", "ascii");
|
|
46
|
-
// The checkpoint signature-line marker is EM DASH (U+2014) + space; built at
|
|
47
|
-
//
|
|
48
|
-
|
|
47
|
+
// The checkpoint signature-line marker is EM DASH (U+2014) + space; built at runtime so the source
|
|
48
|
+
// file stays pure ASCII. The line is parsed by character scan (no regex): "<EM DASH> <keyId> <sig>",
|
|
49
|
+
// two whitespace-separated non-empty tokens, nothing after.
|
|
50
|
+
var _EM_DASH = String.fromCharCode(0x2014);
|
|
51
|
+
|
|
52
|
+
// The \S token scan uses the shared JS regex \s code-point predicate (pkix.isJsWhitespace) so the
|
|
53
|
+
// note-line grammar stays exactly the /\S/ complement without a second copy of the code-point set.
|
|
54
|
+
function _parseNoteSig(line) {
|
|
55
|
+
var prefix = _EM_DASH + " ";
|
|
56
|
+
if (line.slice(0, prefix.length) !== prefix) return null;
|
|
57
|
+
var p = prefix.length, n = line.length;
|
|
58
|
+
var t1s = p;
|
|
59
|
+
while (p < n && !pkix.isJsWhitespace(line.charCodeAt(p))) p += 1;
|
|
60
|
+
if (p === t1s) return null; // \S+ requires at least one character
|
|
61
|
+
var keyId = line.slice(t1s, p);
|
|
62
|
+
if (line.charAt(p) !== " ") return null; // the literal single space between the tokens
|
|
63
|
+
p += 1;
|
|
64
|
+
var t2s = p;
|
|
65
|
+
while (p < n && !pkix.isJsWhitespace(line.charCodeAt(p))) p += 1;
|
|
66
|
+
if (p === t2s) return null;
|
|
67
|
+
var sig = line.slice(t2s, p);
|
|
68
|
+
if (p !== n) return null; // $ -- nothing after the second token
|
|
69
|
+
return { keyId: keyId, sig: sig };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A run of trailing "=" removed (the /=+$/ base64-padding strip).
|
|
73
|
+
function _stripTrailingEquals(s) {
|
|
74
|
+
var i = s.length;
|
|
75
|
+
while (i > 0 && s.charAt(i - 1) === "=") i -= 1;
|
|
76
|
+
return s.slice(0, i);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The recognized bundle media types (draft: the inline ".v0.N+json" form and the
|
|
80
|
+
// "+json;version=0.N" form, N in 1..3) -- an explicit set in place of the anchored alternation regex.
|
|
81
|
+
var _BUNDLE_MEDIA_TYPES = {
|
|
82
|
+
"application/vnd.dev.sigstore.bundle.v0.1+json": 1,
|
|
83
|
+
"application/vnd.dev.sigstore.bundle.v0.2+json": 1,
|
|
84
|
+
"application/vnd.dev.sigstore.bundle.v0.3+json": 1,
|
|
85
|
+
"application/vnd.dev.sigstore.bundle+json;version=0.1": 1,
|
|
86
|
+
"application/vnd.dev.sigstore.bundle+json;version=0.2": 1,
|
|
87
|
+
"application/vnd.dev.sigstore.bundle+json;version=0.3": 1
|
|
88
|
+
};
|
|
89
|
+
function _isBundleMediaType(mt) {
|
|
90
|
+
return typeof mt === "string" && Object.prototype.hasOwnProperty.call(_BUNDLE_MEDIA_TYPES, mt);
|
|
91
|
+
}
|
|
49
92
|
|
|
50
93
|
// Parse untrusted JSON through the bounded, duplicate-member-rejecting guard.
|
|
51
94
|
function _jsonParse(input, code, label) {
|
|
@@ -92,10 +135,10 @@ function pae(payloadType, payloadBytes) {
|
|
|
92
135
|
// either, but reject a non-canonical re-encoding (an encoding-malleability guard).
|
|
93
136
|
function _b64(s, label) {
|
|
94
137
|
if (typeof s !== "string") throw _err("sigstore/bad-bundle", label + " must be a base64 string");
|
|
95
|
-
var urlSafe =
|
|
138
|
+
var urlSafe = s.indexOf("-") !== -1 || s.indexOf("_") !== -1;
|
|
96
139
|
var enc = urlSafe ? "base64url" : "base64";
|
|
97
140
|
var buf = Buffer.from(s, enc);
|
|
98
|
-
if (buf.toString(enc)
|
|
141
|
+
if (_stripTrailingEquals(buf.toString(enc)) !== _stripTrailingEquals(s)) {
|
|
99
142
|
throw _err("sigstore/bad-bundle", label + " is not canonical base64");
|
|
100
143
|
}
|
|
101
144
|
return buf;
|
|
@@ -137,7 +180,7 @@ function parseBundle(input) {
|
|
|
137
180
|
// a newer version is recognized-and-deferred, never accepted by an over-broad
|
|
138
181
|
// match (the inline `.v0.N+json` form and the `+json;version=0.N` form).
|
|
139
182
|
var mt = obj.mediaType;
|
|
140
|
-
if (
|
|
183
|
+
if (!_isBundleMediaType(mt)) {
|
|
141
184
|
throw _err("sigstore/bad-bundle-version", "unsupported or unknown bundle media type: " + (mt === undefined ? "(none)" : mt));
|
|
142
185
|
}
|
|
143
186
|
if (!obj.verificationMaterial || typeof obj.verificationMaterial !== "object") {
|
|
@@ -265,9 +308,9 @@ function _verifyCheckpoint(envelope, ip, rekorKeys, timeMs) {
|
|
|
265
308
|
}
|
|
266
309
|
var sigBlock = buf.subarray(sep + 2).toString("utf8").split("\n");
|
|
267
310
|
for (var i = 0; i < sigBlock.length; i++) {
|
|
268
|
-
var
|
|
269
|
-
if (!
|
|
270
|
-
var blob = _b64(
|
|
311
|
+
var note = _parseNoteSig(sigBlock[i]);
|
|
312
|
+
if (!note) continue;
|
|
313
|
+
var blob = _b64(note.sig, "checkpoint signature");
|
|
271
314
|
if (blob.length < 5) continue;
|
|
272
315
|
var hint = blob.subarray(0, 4), derSig = blob.subarray(4);
|
|
273
316
|
var k = _rekorKey(rekorKeys, null, hint, timeMs);
|
package/lib/smime.js
CHANGED
|
@@ -245,26 +245,95 @@ function _outerPrefix(list) {
|
|
|
245
245
|
// non-ASCII protected value round-trips intact -- a latin1 string maps 1:1 to bytes, recovering the emitted UTF-8.
|
|
246
246
|
function _utf8Header(s) { return Buffer.from(s, "latin1").toString("utf8"); }
|
|
247
247
|
|
|
248
|
+
// ---- header parsing by character scan (no regex) ----------------------------------------------
|
|
249
|
+
// ASCII lowercase of a code point (A-Z -> a-z), for the case-insensitive "content-type" literal match.
|
|
250
|
+
function _asciiLower(c) { return (c >= 65 && c <= 90) ? c + 32 : c; }
|
|
251
|
+
var _CONTENT_TYPE = "content-type";
|
|
252
|
+
|
|
253
|
+
// RFC 5322 header unfolding: a CRLF followed by one-or-more SP/HTAB (a folded continuation) collapses
|
|
254
|
+
// to a single space (matching /\r\n[ \t]+/g -> " ").
|
|
255
|
+
function _unfoldHeaderFolds(s) {
|
|
256
|
+
var out = "", i = 0, n = s.length;
|
|
257
|
+
while (i < n) {
|
|
258
|
+
if (s.charCodeAt(i) === 0x0d && i + 1 < n && s.charCodeAt(i + 1) === 0x0a &&
|
|
259
|
+
i + 2 < n && (s.charCodeAt(i + 2) === 0x20 || s.charCodeAt(i + 2) === 0x09)) {
|
|
260
|
+
out += " ";
|
|
261
|
+
i += 2;
|
|
262
|
+
while (i < n && (s.charCodeAt(i) === 0x20 || s.charCodeAt(i) === 0x09)) i += 1;
|
|
263
|
+
} else {
|
|
264
|
+
out += s.charAt(i);
|
|
265
|
+
i += 1;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// If the line beginning at `start` is a Content-Type field -- ^[ \t]*content-type[ \t]*: -- return the
|
|
272
|
+
// whole line up to (not including) the next CR or LF (the [^\r\n]* tail); else null. Case-insensitive
|
|
273
|
+
// on the literal, matching the /i flag over the ASCII field name.
|
|
274
|
+
function _contentTypeLineAt(s, start) {
|
|
275
|
+
var n = s.length, p = start;
|
|
276
|
+
while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
|
|
277
|
+
if (p + _CONTENT_TYPE.length > n) return null;
|
|
278
|
+
for (var k = 0; k < _CONTENT_TYPE.length; k++) {
|
|
279
|
+
if (_asciiLower(s.charCodeAt(p + k)) !== _CONTENT_TYPE.charCodeAt(k)) return null;
|
|
280
|
+
}
|
|
281
|
+
p += _CONTENT_TYPE.length;
|
|
282
|
+
while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
|
|
283
|
+
if (p >= n || s.charCodeAt(p) !== 0x3a) return null; // ":"
|
|
284
|
+
p += 1;
|
|
285
|
+
while (p < n && s.charCodeAt(p) !== 0x0d && s.charCodeAt(p) !== 0x0a) p += 1;
|
|
286
|
+
return s.slice(start, p);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Every Content-Type field in the header block (the /gim match, /m making ^ match each line start):
|
|
290
|
+
// scan the block start and each position after an LF. Returns the matched lines, or null when none.
|
|
291
|
+
function _contentTypeLines(s) {
|
|
292
|
+
var lines = [], lineStart = 0;
|
|
293
|
+
while (lineStart <= s.length) {
|
|
294
|
+
var line = _contentTypeLineAt(s, lineStart);
|
|
295
|
+
if (line !== null) lines.push(line);
|
|
296
|
+
var nl = s.indexOf("\n", lineStart);
|
|
297
|
+
if (nl === -1) break;
|
|
298
|
+
lineStart = nl + 1;
|
|
299
|
+
}
|
|
300
|
+
return lines.length ? lines : null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// The ^[ \t]*content-type[ \t]*: prefix removed from a matched Content-Type line (the value remains).
|
|
304
|
+
function _contentTypeValue(s) {
|
|
305
|
+
var n = s.length, p = 0;
|
|
306
|
+
while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
|
|
307
|
+
if (p + _CONTENT_TYPE.length > n) return s;
|
|
308
|
+
for (var k = 0; k < _CONTENT_TYPE.length; k++) {
|
|
309
|
+
if (_asciiLower(s.charCodeAt(p + k)) !== _CONTENT_TYPE.charCodeAt(k)) return s;
|
|
310
|
+
}
|
|
311
|
+
p += _CONTENT_TYPE.length;
|
|
312
|
+
while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
|
|
313
|
+
if (p >= n || s.charCodeAt(p) !== 0x3a) return s;
|
|
314
|
+
return s.slice(p + 1);
|
|
315
|
+
}
|
|
316
|
+
|
|
248
317
|
function _declaresHp(content) {
|
|
249
318
|
// content is the CANONICAL entity bytes -- the caller (_hpSurface) runs mime.canonicalizeText ONCE so
|
|
250
319
|
// detection here and the subsequent mime.parse see the identical normalized form (bare CR / LF -> CRLF).
|
|
251
320
|
// Detecting hp on the canonical form -- the bytes the signature covers -- is what stops a fold/line-ending
|
|
252
321
|
// transport rewrite from stripping the hp signal from a still-valid signed message.
|
|
253
322
|
var s = content.toString("latin1");
|
|
254
|
-
var
|
|
255
|
-
var head = (
|
|
323
|
+
var sep = s.indexOf("\r\n\r\n");
|
|
324
|
+
var head = _unfoldHeaderFolds(sep !== -1 ? s.slice(0, sep) : s);
|
|
256
325
|
// Detect hp by composing mime.paramCount, the same comment- + quoted-string-aware tokenizer the parser
|
|
257
326
|
// uses -- over each Content-Type field, so the probe never diverges from mime.parse: a leading-whitespace
|
|
258
327
|
// or before-colon field name is matched (the parser trims it), while an hp= inside a MIME comment or a
|
|
259
328
|
// quoted value is not a parameter (so an ordinary Content-Type cannot opt a message into HP processing). A
|
|
260
329
|
// real hp on ANY Content-Type line is detected; a duplicate Content-Type is rejected downstream.
|
|
261
|
-
var cts = head
|
|
330
|
+
var cts = _contentTypeLines(head);
|
|
262
331
|
if (!cts) return false;
|
|
263
332
|
for (var i = 0; i < cts.length; i++) {
|
|
264
333
|
// hasParam (not paramCount): a payload that names the hp attribute without a value ("...; hp") still
|
|
265
334
|
// CLAIMS header protection -- detect it so _hpSurface fails closed on the malformed declaration rather
|
|
266
335
|
// than silently downgrading it to unprotected.
|
|
267
|
-
if (mime.hasParam(cts[i]
|
|
336
|
+
if (mime.hasParam(_contentTypeValue(cts[i]), "hp")) return true;
|
|
268
337
|
}
|
|
269
338
|
return false;
|
|
270
339
|
}
|
|
@@ -714,8 +783,19 @@ function _micalgOf(p7Der) {
|
|
|
714
783
|
// A fresh multipart boundary that cannot collide with the content (random, dashed, unique per call).
|
|
715
784
|
function _boundary() { return "----=_pki_smime_" + nodeCrypto.randomBytes(18).toString("hex"); }
|
|
716
785
|
|
|
786
|
+
// Base64 wrapped to 64-column lines joined by CRLF, no trailing CRLF (matching (.{64})->"$1\r\n"
|
|
787
|
+
// then a trailing-CRLF strip; base64 output carries no CRLF of its own).
|
|
788
|
+
function _wrap64Crlf(s) {
|
|
789
|
+
var out = "";
|
|
790
|
+
for (var i = 0; i < s.length; i += 64) {
|
|
791
|
+
if (i > 0) out += "\r\n";
|
|
792
|
+
out += s.slice(i, i + 64);
|
|
793
|
+
}
|
|
794
|
+
return out;
|
|
795
|
+
}
|
|
796
|
+
|
|
717
797
|
function _base64Body(der) {
|
|
718
|
-
return Buffer.from(der.toString("base64")
|
|
798
|
+
return Buffer.from(_wrap64Crlf(der.toString("base64")), "latin1");
|
|
719
799
|
}
|
|
720
800
|
|
|
721
801
|
/**
|
|
@@ -1180,7 +1260,7 @@ async function decompress(message, opts) {
|
|
|
1180
1260
|
function _decodeCms(ent) {
|
|
1181
1261
|
// guard.encoding.base64 is strict (no whitespace) and rejects via a (code,msg) FACTORY -- strip the
|
|
1182
1262
|
// MIME line-wrapping whitespace first, and pass _err (not the SmimeError class).
|
|
1183
|
-
if (ent.cte === "base64") return guard.encoding.base64(ent.body.toString("latin1")
|
|
1263
|
+
if (ent.cte === "base64") return guard.encoding.base64(pkix.stripBase64Whitespace(ent.body.toString("latin1")), C.LIMITS.MIME_MAX_BYTES, _err, "smime/bad-mime", "the CMS body");
|
|
1184
1264
|
return ent.body;
|
|
1185
1265
|
}
|
|
1186
1266
|
|