@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/mime.js
CHANGED
|
@@ -36,15 +36,38 @@ function _splitPoint(bytes) {
|
|
|
36
36
|
return null;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Line-ending + whitespace helpers, decided character by character (no regex): _toLf collapses any
|
|
40
|
+
// CRLF / bare CR / bare LF to a single LF; _toCrlf then expands every LF to CRLF; _stripLeadingHtab
|
|
41
|
+
// drops a leading run of SP/HTAB. They match the /\r\n/g, /\r/g, /\n/g and /^[ \t]+/ replacements.
|
|
42
|
+
function _toLf(s) {
|
|
43
|
+
var out = "";
|
|
44
|
+
for (var i = 0; i < s.length; i++) {
|
|
45
|
+
var c = s.charAt(i);
|
|
46
|
+
if (c === "\r") { if (s.charAt(i + 1) === "\n") continue; out += "\n"; } // CRLF -> its LF next; bare CR -> LF
|
|
47
|
+
else out += c;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function _toCrlf(s) {
|
|
52
|
+
var lf = _toLf(s), out = "";
|
|
53
|
+
for (var i = 0; i < lf.length; i++) { var c = lf.charAt(i); out += (c === "\n") ? "\r\n" : c; }
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
function _stripLeadingHtab(s) {
|
|
57
|
+
var i = 0;
|
|
58
|
+
while (i < s.length && (s.charAt(i) === " " || s.charAt(i) === "\t")) i += 1;
|
|
59
|
+
return s.slice(i);
|
|
60
|
+
}
|
|
61
|
+
|
|
39
62
|
// Split a header block into unfolded logical lines (RFC 5322 folding: a continuation line begins with
|
|
40
63
|
// SP/HTAB and joins the previous). Returns the raw header strings (each "Name: value").
|
|
41
64
|
function _unfoldHeaders(headerText) {
|
|
42
|
-
var lines = headerText
|
|
65
|
+
var lines = _toLf(headerText).split("\n");
|
|
43
66
|
var out = [];
|
|
44
67
|
for (var i = 0; i < lines.length; i++) {
|
|
45
68
|
var ln = lines[i];
|
|
46
69
|
if (ln === "") continue;
|
|
47
|
-
if ((ln[0] === " " || ln[0] === "\t") && out.length) out[out.length - 1] += " " + ln
|
|
70
|
+
if ((ln[0] === " " || ln[0] === "\t") && out.length) out[out.length - 1] += " " + _stripLeadingHtab(ln);
|
|
48
71
|
else out.push(ln);
|
|
49
72
|
}
|
|
50
73
|
return out;
|
|
@@ -130,7 +153,7 @@ function parse(input, E, code) {
|
|
|
130
153
|
// with only the single conventional ": " delimiter space removed. Leading/trailing whitespace inside
|
|
131
154
|
// the body is preserved, so a consumer that must reproduce the exact authenticated field body (S/MIME
|
|
132
155
|
// header protection) is not handed a lossily-trimmed value.
|
|
133
|
-
headers.push({ name: nm, lname: nm.toLowerCase(), value: body.trim(), rawValue: body.
|
|
156
|
+
headers.push({ name: nm, lname: nm.toLowerCase(), value: body.trim(), rawValue: body.charAt(0) === " " ? body.slice(1) : body });
|
|
134
157
|
}
|
|
135
158
|
function header(name) { var l = name.toLowerCase(); for (var j = 0; j < headers.length; j++) if (headers[j].lname === l) return headers[j].value; return null; }
|
|
136
159
|
var ctv = header("content-type");
|
|
@@ -146,7 +169,7 @@ function parse(input, E, code) {
|
|
|
146
169
|
// Normalize a text body's line endings to the RFC 8551 sec. 3.1.1 canonical CRLF pair (any bare CR or
|
|
147
170
|
// LF becomes CRLF; an existing CRLF is preserved).
|
|
148
171
|
function canonicalizeText(bodyBytes) {
|
|
149
|
-
var s = bodyBytes.toString("latin1")
|
|
172
|
+
var s = _toCrlf(bodyBytes.toString("latin1"));
|
|
150
173
|
return Buffer.from(s, "latin1");
|
|
151
174
|
}
|
|
152
175
|
|
package/lib/ocsp.js
CHANGED
|
@@ -84,7 +84,7 @@ function _responseFromBytes(response) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function _toDer(input, what) {
|
|
87
|
-
if (
|
|
87
|
+
if (guard.bytes.isByteSource(input)) return guard.bytes.snapshotSource(input, OcspError, "ocsp/bad-input", what || "input");
|
|
88
88
|
if (typeof input === "string") { try { return ocspSchema.pemDecode(input); } catch (e) { throw _err("ocsp/bad-input", (what || "input") + " PEM could not be decoded", e); } }
|
|
89
89
|
throw _err("ocsp/bad-input", (what || "input") + " must be a DER Buffer, Uint8Array, or PEM string");
|
|
90
90
|
}
|
|
@@ -221,16 +221,16 @@ function _buildRequest(query, opts) {
|
|
|
221
221
|
}
|
|
222
222
|
function _emitReq(der, opts) { return opts.pem ? ocspSchema.pemEncode(der, "OCSP REQUEST") : der; }
|
|
223
223
|
function _nameDer(name) {
|
|
224
|
-
if (
|
|
224
|
+
if (guard.bytes.isByteSource(name)) return guard.bytes.snapshotSource(name, OcspError, "ocsp/bad-input", "requestorName");
|
|
225
225
|
if (name && name.bytes) return name.bytes; // a parsed Name
|
|
226
|
-
throw _err("ocsp/bad-input", "requestorName must be a DER Name
|
|
226
|
+
throw _err("ocsp/bad-input", "requestorName must be a DER Name BufferSource or a parsed Name");
|
|
227
227
|
}
|
|
228
228
|
// The RAW DER of a certificate that will be embedded verbatim (the responder cert in a
|
|
229
229
|
// BasicOCSPResponse, the signer cert in a signed OCSPRequest). It must be supplied as bytes:
|
|
230
230
|
// a parsed certificate does not retain its full DER encoding, and re-encoding it would risk
|
|
231
231
|
// byte drift from the signed original, so a parsed cert is rejected rather than reconstructed.
|
|
232
232
|
function _normCertDer(cert, what) {
|
|
233
|
-
if (
|
|
233
|
+
if (guard.bytes.isByteSource(cert)) return guard.bytes.snapshotSource(cert, OcspError, "ocsp/bad-input", what || "a certificate");
|
|
234
234
|
if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("ocsp/bad-input", (what || "a certificate") + " PEM could not be decoded", e); } }
|
|
235
235
|
if (cert && cert.tbsBytes && cert.subjectPublicKeyInfo) {
|
|
236
236
|
throw _err("ocsp/bad-input", (what || "a certificate") + " to embed must be supplied as DER bytes or a PEM string, not a parsed certificate (the parser does not retain the full DER encoding needed to embed it verbatim)");
|
|
@@ -378,7 +378,7 @@ function _asDate(d) {
|
|
|
378
378
|
// the aliasing window is wiped rather than left to the garbage collector. Only our own
|
|
379
379
|
// allocations are listed: the caller's key is never written to.
|
|
380
380
|
function _snapshotSignerKey(key, owned) {
|
|
381
|
-
if (Buffer.isBuffer(key) || key instanceof Uint8Array) {
|
|
381
|
+
if (Buffer.isBuffer(key) || key instanceof Uint8Array) { // allow:byte-source-narrow -- responder private key on the key-ownership contract (Buffer / Uint8Array / PEM / CryptoKey); widening keys is a separate root decision
|
|
382
382
|
var copy = guard.bytes.snapshot(key, OcspError, "ocsp/bad-input", "the responder key");
|
|
383
383
|
owned.push(copy);
|
|
384
384
|
return copy;
|
|
@@ -550,8 +550,8 @@ function verify(response, opts) {
|
|
|
550
550
|
// A client that sent a nonce binds it (RFC 9654 / RFC 5019 sec. 4): a missing or mismatched
|
|
551
551
|
// response nonce fails the verdict closed, even if the status/signature were otherwise good.
|
|
552
552
|
var respNonce = _responseNonce(parsed);
|
|
553
|
-
var reqNonce =
|
|
554
|
-
? guard.bytes.
|
|
553
|
+
var reqNonce = guard.bytes.isByteSource(opts.requestNonce)
|
|
554
|
+
? guard.bytes.snapshotSource(opts.requestNonce, OcspError, "ocsp/bad-input", "opts.requestNonce") : null;
|
|
555
555
|
var matched = respNonce != null && reqNonce != null && guard.crypto.constantTimeEqual(respNonce, reqNonce);
|
|
556
556
|
var out = Object.assign({}, verdict, { nonceMatched: matched });
|
|
557
557
|
// The downgrade applies to `good` ONLY. `unknown` is the closed direction for a
|
package/lib/oid.js
CHANGED
|
@@ -652,6 +652,21 @@ function byName(n) {
|
|
|
652
652
|
return _byName.get(n);
|
|
653
653
|
}
|
|
654
654
|
|
|
655
|
+
// Does `s` look like a dotted-decimal OID -- one or more digit groups joined by single dots (at least two
|
|
656
|
+
// groups, no leading/trailing/doubled dot)? Distinguishes a numeric OID from a symbolic name so a caller may
|
|
657
|
+
// pass either. Decided by character scan (no regex); the arc-value bounds are the encoder's authority.
|
|
658
|
+
function isDottedDecimal(s) {
|
|
659
|
+
if (typeof s !== "string" || s.length === 0) return false;
|
|
660
|
+
var i, c, sawDot = false, atomEmpty = true;
|
|
661
|
+
for (i = 0; i < s.length; i += 1) {
|
|
662
|
+
c = s.charCodeAt(i);
|
|
663
|
+
if (c === 46) { if (atomEmpty) return false; sawDot = true; atomEmpty = true; } // "."
|
|
664
|
+
else if (c >= 48 && c <= 57) { atomEmpty = false; } // 0-9
|
|
665
|
+
else return false;
|
|
666
|
+
}
|
|
667
|
+
return sawDot && !atomEmpty;
|
|
668
|
+
}
|
|
669
|
+
|
|
655
670
|
function has(dotted) {
|
|
656
671
|
_assertDotted(dotted, "has");
|
|
657
672
|
return _byOid.has(dotted);
|
|
@@ -876,6 +891,7 @@ function kemParams(oidOrName) { return KEM_PARAMS[oidOrName]; }
|
|
|
876
891
|
module.exports = {
|
|
877
892
|
name: name,
|
|
878
893
|
byName: byName,
|
|
894
|
+
isDottedDecimal: isDottedDecimal,
|
|
879
895
|
has: has,
|
|
880
896
|
paramsMustBeAbsent: paramsMustBeAbsent,
|
|
881
897
|
kemParams: kemParams,
|
package/lib/path-validate.js
CHANGED
|
@@ -49,6 +49,7 @@ var cmsVerify = require("./cms-verify");
|
|
|
49
49
|
var cmpSession = require("./cmp-session");
|
|
50
50
|
var guard = require("./guard-all");
|
|
51
51
|
var intrinsic = require("./guard-intrinsic");
|
|
52
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
52
53
|
var constants = require("./constants");
|
|
53
54
|
var validator = require("./validator-all");
|
|
54
55
|
var cms = require("./schema-cms");
|
|
@@ -568,11 +569,54 @@ function hostConstraintMatch(constraint, host) {
|
|
|
568
569
|
// or a value carrying non-hostname characters (a scheme "://", a path "/", a
|
|
569
570
|
// port ":") is not a FQDN and cannot be matched against a domain-suffix
|
|
570
571
|
// constraint. Only [A-Za-z0-9.-] with at least one dot qualifies.
|
|
572
|
+
// A code-point membership table built from a string of the allowed characters, and the shared scan that
|
|
573
|
+
// checks every character of a non-empty string against one -- replacing the hostname-charset regexes
|
|
574
|
+
// through one loop (no per-predicate character-class chain).
|
|
575
|
+
function _charTable(chars) { var t = []; for (var i = 0; i < chars.length; i++) t[_charCodeAt(chars, i)] = true; return t; }
|
|
576
|
+
var _FQDN_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-"); // /^[a-z0-9.-]+$/i
|
|
577
|
+
var _DIGDOT_TABLE = _charTable("0123456789."); // /^[0-9.]+$/
|
|
578
|
+
function _allCharsIn(s, table) {
|
|
579
|
+
if (s.length === 0) return false;
|
|
580
|
+
for (var i = 0; i < s.length; i++) if (!table[_charCodeAt(s, i)]) return false;
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
// /^[0-9]/ : begins with an ASCII digit (a dotted-OID attempt, vs an EKU / purpose name).
|
|
584
|
+
function _startsWithDigit(s) { if (s.length === 0) return false; var c = _charCodeAt(s, 0); return c >= 48 && c <= 57; }
|
|
585
|
+
|
|
586
|
+
var _SCHEME_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+.-"); // [a-z0-9+.-]
|
|
587
|
+
// /^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)/i : the [start, end) range of the authority (up to the first / ? #
|
|
588
|
+
// or end), or null when the input is not a scheme "//" URI. Returns a range so the caller slices once
|
|
589
|
+
// (the slice that replaces the removed .exec), not a fresh substring here.
|
|
590
|
+
function _uriAuthorityRange(uri) {
|
|
591
|
+
var n = uri.length;
|
|
592
|
+
if (n === 0) return null;
|
|
593
|
+
var c0 = _charCodeAt(uri, 0);
|
|
594
|
+
if (!((c0 >= 65 && c0 <= 90) || (c0 >= 97 && c0 <= 122))) return null; // scheme starts with a letter
|
|
595
|
+
var p = 1;
|
|
596
|
+
while (p < n && _SCHEME_TABLE[_charCodeAt(uri, p)]) p += 1;
|
|
597
|
+
if (_charCodeAt(uri, p) !== 58 || _charCodeAt(uri, p + 1) !== 47 || _charCodeAt(uri, p + 2) !== 47) return null; // "://"
|
|
598
|
+
p += 3;
|
|
599
|
+
var authStart = p;
|
|
600
|
+
while (p < n) { var d = _charCodeAt(uri, p); if (d === 47 || d === 63 || d === 35) break; p += 1; } // [^/?#]*
|
|
601
|
+
return [authStart, p];
|
|
602
|
+
}
|
|
603
|
+
// /:\d+$/ : the index at which a trailing ":" + digits port starts (to slice off), or s.length when absent.
|
|
604
|
+
function _portStripIndex(s) {
|
|
605
|
+
var i = s.length;
|
|
606
|
+
while (i > 0) { var c = _charCodeAt(s, i - 1); if (c >= 48 && c <= 57) i -= 1; else break; }
|
|
607
|
+
return (i < s.length && i > 0 && _charCodeAt(s, i - 1) === 58) ? i - 1 : s.length;
|
|
608
|
+
}
|
|
609
|
+
// /^\[(.*)\]$/ -> "$1" : an IPv6 literal's surrounding "[" "]" removed, else the string unchanged.
|
|
610
|
+
function _stripBrackets(s) {
|
|
611
|
+
var n = s.length;
|
|
612
|
+
return (n >= 2 && _charCodeAt(s, 0) === 91 && _charCodeAt(s, n - 1) === 93) ? s.slice(1, n - 1) : s;
|
|
613
|
+
}
|
|
614
|
+
|
|
571
615
|
function isFqdnHost(host) {
|
|
572
616
|
var h = stripTrailingDot(host);
|
|
573
617
|
if (h === "" || h.indexOf(".") === -1) return false; // empty or single-label (localhost)
|
|
574
|
-
if (
|
|
575
|
-
if (
|
|
618
|
+
if (!_allCharsIn(h, _FQDN_TABLE)) return false; // scheme/path/port/IPv6 chars, "@", etc.
|
|
619
|
+
if (_allCharsIn(h, _DIGDOT_TABLE)) return false; // IPv4 dotted-quad literal
|
|
576
620
|
return true;
|
|
577
621
|
}
|
|
578
622
|
|
|
@@ -595,9 +639,9 @@ function uriMatch(constraint, uri) {
|
|
|
595
639
|
}
|
|
596
640
|
|
|
597
641
|
function uriHost(uri) {
|
|
598
|
-
var
|
|
599
|
-
if (
|
|
600
|
-
var authority =
|
|
642
|
+
var r = _uriAuthorityRange(uri);
|
|
643
|
+
if (r === null) return null;
|
|
644
|
+
var authority = uri.slice(r[0], r[1]);
|
|
601
645
|
// authority = [ userinfo "@" ] host [ ":" port ]. RFC 3986 userinfo does not
|
|
602
646
|
// contain a raw "@", so a conformant authority has at most one "@". With more
|
|
603
647
|
// than one the host cannot be determined reliably, so this fails closed
|
|
@@ -606,7 +650,7 @@ function uriHost(uri) {
|
|
|
606
650
|
var firstAt = authority.indexOf("@");
|
|
607
651
|
if (firstAt !== authority.lastIndexOf("@")) return null;
|
|
608
652
|
if (firstAt !== -1) authority = authority.slice(firstAt + 1);
|
|
609
|
-
var host = authority.
|
|
653
|
+
var host = authority.slice(0, _portStripIndex(authority));
|
|
610
654
|
return host === "" ? null : host; // an empty authority cannot be evaluated -> unsupported
|
|
611
655
|
}
|
|
612
656
|
|
|
@@ -788,8 +832,7 @@ function treeIsEmpty(tree) { return tree.children.length === 0; }
|
|
|
788
832
|
function isSubtreeBaseValid(tag, base) {
|
|
789
833
|
switch (tag) {
|
|
790
834
|
case 1: case 2: case 6: return typeof base === "string"; // rfc822Name / dNSName / URI
|
|
791
|
-
case 7: return
|
|
792
|
-
(base.length === 8 || base.length === 32); // iPAddress: address + mask
|
|
835
|
+
case 7: return Buffer.isBuffer(base) && (base.length === 8 || base.length === 32); // iPAddress: address + mask (any BufferSource is normalized to a Buffer in checkedSubtreeSeeds)
|
|
793
836
|
case 4: return base !== null && typeof base === "object" && !!base.rdns && intrinsic.isArray(base.rdns); // directoryName
|
|
794
837
|
default: return base !== undefined;
|
|
795
838
|
}
|
|
@@ -803,10 +846,19 @@ function checkedSubtreeSeeds(list, optName) {
|
|
|
803
846
|
if (list === undefined || list === null) return [];
|
|
804
847
|
if (!intrinsic.isArray(list)) throw E("path/bad-input", "validate: opts." + optName + " must be an array of { tag, base } subtree entries");
|
|
805
848
|
return list.map(function (st) {
|
|
806
|
-
if (!st || typeof st !== "object" || !Number.isInteger(st.tag) || st.tag < 0 || st.tag > 8
|
|
849
|
+
if (!st || typeof st !== "object" || !Number.isInteger(st.tag) || st.tag < 0 || st.tag > 8) {
|
|
850
|
+
throw E("path/bad-input", "validate: opts." + optName + " entries must be { tag: <GeneralName tag number 0..8>, base: <that form's constraint value> }");
|
|
851
|
+
}
|
|
852
|
+
// An iPAddress base (address+mask) may arrive as any BufferSource; normalize it to a
|
|
853
|
+
// private Buffer so the matcher can index it by byte and a caller cannot mutate it once a
|
|
854
|
+
// microtask has passed. The name forms (string / directoryName) are validated as they stand.
|
|
855
|
+
var base = (st.tag === 7 && guard.bytes.isByteSource(st.base))
|
|
856
|
+
? guard.bytes.snapshotSource(st.base, PathError, "path/bad-input", "an iPAddress subtree base")
|
|
857
|
+
: st.base;
|
|
858
|
+
if (!isSubtreeBaseValid(st.tag, base)) {
|
|
807
859
|
throw E("path/bad-input", "validate: opts." + optName + " entries must be { tag: <GeneralName tag number 0..8>, base: <that form's constraint value> }");
|
|
808
860
|
}
|
|
809
|
-
return { tag: st.tag, base:
|
|
861
|
+
return { tag: st.tag, base: base };
|
|
810
862
|
});
|
|
811
863
|
}
|
|
812
864
|
|
|
@@ -2522,8 +2574,8 @@ function verifyOcspResponse(response, cert, issuerCert, time, opts) {
|
|
|
2522
2574
|
var _OCSP_CLAIM = ["responseStatus", "basicResponse", "tbsResponseDataBytes"];
|
|
2523
2575
|
function _ocspFromBytes(response) {
|
|
2524
2576
|
return guard.parsed.fromTrustedSource(response, "ocspResponse", _OCSP_CLAIM, function (bytes) {
|
|
2525
|
-
if (!
|
|
2526
|
-
throw E("path/bad-input", "verifyOcspResponse: the response must be an OCSP response DER
|
|
2577
|
+
if (!guard.bytes.isByteSource(bytes) && typeof bytes !== "string") {
|
|
2578
|
+
throw E("path/bad-input", "verifyOcspResponse: the response must be an OCSP response DER BufferSource, a PEM string, or a pki.schema.ocsp.parseResponse result");
|
|
2527
2579
|
}
|
|
2528
2580
|
return ocsp.parseResponse(bytes);
|
|
2529
2581
|
}, E, "path/bad-input",
|
|
@@ -2674,7 +2726,7 @@ function resolvePurposeOpts(opts) {
|
|
|
2674
2726
|
// A dotted-form attempt (leads with a digit) must be a canonical OID -- a
|
|
2675
2727
|
// loose regex accepted a leading-zero / out-of-bounds key that would never
|
|
2676
2728
|
// match the canonical EKU the target advertises; anything else is a name.
|
|
2677
|
-
if (
|
|
2729
|
+
if (_startsWithDigit(p)) return guard.identifier.assertCanonicalOid(p, E, "path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p));
|
|
2678
2730
|
var dotted = oid.byName(p);
|
|
2679
2731
|
if (typeof dotted !== "string") throw E("path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p) + " is not a registered OID name");
|
|
2680
2732
|
return dotted;
|
|
@@ -2685,7 +2737,7 @@ function resolvePurposeOpts(opts) {
|
|
|
2685
2737
|
if (typeof opts.checkPurpose !== "string" || opts.checkPurpose.length === 0) {
|
|
2686
2738
|
throw E("path/bad-input", "validate: opts.checkPurpose must be a key-purpose OID name or dotted OID string");
|
|
2687
2739
|
}
|
|
2688
|
-
if (
|
|
2740
|
+
if (_startsWithDigit(opts.checkPurpose)) {
|
|
2689
2741
|
var cpDotted = guard.identifier.assertCanonicalOid(opts.checkPurpose, E, "path/bad-input", "validate: opts.checkPurpose");
|
|
2690
2742
|
checkPurpose = oid.name(cpDotted) || cpDotted; // normalize a dotted purpose OID to its name for the anchor map
|
|
2691
2743
|
} else {
|
|
@@ -2840,7 +2892,7 @@ function toAnchor(entry, verb) {
|
|
|
2840
2892
|
// entry refusal, not a raw exception leaked past the typed guards. `alg` and the raw publicKey are PINNED
|
|
2841
2893
|
// to ONE read here and used throughout, so a stateful accessor that returns a value now and throws (or
|
|
2842
2894
|
// returns a different value) on a later read cannot slip past this guard at the shape check or the snapshot.
|
|
2843
|
-
var isTuple = false, alg = null, pubRaw
|
|
2895
|
+
var isTuple = false, alg = null, pubRaw, nameRaw = null, pk = null, purposesSnap, distrustSnap;
|
|
2844
2896
|
var subjectDerRaw, labelRaw, mozRaw;
|
|
2845
2897
|
// A Proxy anchor is refused outright. Every field read below -- whether by property access or from an own
|
|
2846
2898
|
// data descriptor -- is answered by the anchor's own reflection, and a Proxy's traps let the same read
|
|
@@ -2915,7 +2967,10 @@ function toAnchor(entry, verb) {
|
|
|
2915
2967
|
// it is captured. A non-tuple leaves pk null / isTuple false and falls through to coerceCert below.
|
|
2916
2968
|
pubRaw = entry.publicKey;
|
|
2917
2969
|
if (guard.bytes.isByteSource(pubRaw)) {
|
|
2918
|
-
|
|
2970
|
+
// snapshotSource, not snapshot: isByteSource accepts all four BufferSource forms (Buffer, any view,
|
|
2971
|
+
// DataView, ArrayBuffer), so the normalizer must too -- snapshot accepts only Buffer/Uint8Array and
|
|
2972
|
+
// would accept-then-throw on an ArrayBuffer / DataView anchor publicKey.
|
|
2973
|
+
pk = guard.bytes.snapshotSource(pubRaw, PathError, "path/bad-input", who + ": trustAnchor publicKey");
|
|
2919
2974
|
}
|
|
2920
2975
|
nameRaw = entry.name;
|
|
2921
2976
|
alg = entry.algorithm;
|
|
@@ -3005,7 +3060,10 @@ function toAnchor(entry, verb) {
|
|
|
3005
3060
|
// resolve a polluted Object.prototype.rdns on a name that supplied none (and could invoke an inherited
|
|
3006
3061
|
// throwing getter) -- means a name with no own rdns is refused, never accepted on an inherited array.
|
|
3007
3062
|
var nameHasRdns = intrinsic.hasOwn(nameOut, "rdns") && intrinsic.isArray(nameOut.rdns);
|
|
3008
|
-
|
|
3063
|
+
// Gate on the NORMALIZED key `pk` (a private Buffer from snapshotSource iff publicKey was any of the four
|
|
3064
|
+
// BufferSource forms), not the raw `pubRaw` -- Buffer.isBuffer(pubRaw) would reject a Uint8Array / view /
|
|
3065
|
+
// ArrayBuffer publicKey that isByteSource + snapshotSource just accepted.
|
|
3066
|
+
if (!nameHasRdns || !Buffer.isBuffer(pk) || algStr === null) {
|
|
3009
3067
|
throw E("path/bad-input", who + ": a trustAnchor tuple must be { name: { rdns: [...] }, publicKey: Buffer, algorithm: an OID string or a SubjectPublicKeyInfo algorithm carrying an oid }");
|
|
3010
3068
|
}
|
|
3011
3069
|
// Hold the stated algorithm to a canonical dotted-decimal OID: a shape-valid but non-canonical value
|
|
@@ -3256,7 +3314,7 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
3256
3314
|
// A DNS-NAME host's private RESOLUTION can only be blocked by a transport that filters the resolved address;
|
|
3257
3315
|
// with an UNGUARDED transport (an injected fn that does not vouch blocksPrivateAddresses) fail closed -- fetch
|
|
3258
3316
|
// IP literals only (already validated above), never a hostname whose resolved address we cannot verify.
|
|
3259
|
-
if (net.isIP(u.hostname
|
|
3317
|
+
if (net.isIP(_stripBrackets(u.hostname)) === 0 && !aia.transportGuardsAddresses) continue;
|
|
3260
3318
|
u.hash = ""; // the fragment is never sent on the wire -> not part of the request/dedupe identity
|
|
3261
3319
|
if (intrinsic.setHas(aia.fetchedUrls, u.href) || intrinsic.setHas(seenThisCert, u.href)) continue; // dedupe (build-wide OR same-cert) on the normalized URL
|
|
3262
3320
|
if (seenThisCert.size >= aia.maxPerCert) break; // per-cert DISTINCT-url cap, checked BEFORE appending so maxAiaPerCert:0 collects nothing (no fetch at all)
|
package/lib/pbes2.js
CHANGED
|
@@ -157,7 +157,7 @@ function pbes2Encrypt(pwBytes, plaintext, opts, E, prefix) {
|
|
|
157
157
|
opts = opts || {};
|
|
158
158
|
var cipherName = opts.cipher || "aes256-CBC";
|
|
159
159
|
var keyBits = CONTENT_KEYBITS[O(cipherName)];
|
|
160
|
-
if (!keyBits ||
|
|
160
|
+
if (!keyBits || cipherName.indexOf("CBC") === -1) throw E(prefix + "/bad-input", "unsupported PBES2 cipher " + cipherName + " (AES-CBC only)");
|
|
161
161
|
var prf = opts.prf || "hmacWithSHA256";
|
|
162
162
|
var prfNode = prfNodeByName(prf, E, prefix);
|
|
163
163
|
var iterations = assertIterations(opts.iterations == null ? 2048 : opts.iterations, E, prefix);
|
|
@@ -200,7 +200,7 @@ function pbes2Decrypt(pwBytes, params, ciphertext, opts, E, prefix, budget) {
|
|
|
200
200
|
pb = parsePbkdf2Params(kdf[1].bytes, opts, E, prefix, true); // strictPrf: reject a non-canonical explicit default prf
|
|
201
201
|
var encOid = asn1.read.oid(encScheme[0]);
|
|
202
202
|
keyBits = CONTENT_KEYBITS[encOid];
|
|
203
|
-
if (!keyBits ||
|
|
203
|
+
if (!keyBits || (oid.name(encOid) || "").indexOf("CBC") === -1) throw E(prefix + "/unsupported-algorithm", "unsupported PBES2 encryptionScheme " + (oid.name(encOid) || encOid) + " (AES-CBC only)");
|
|
204
204
|
iv = asn1.read.octetString(encScheme[1]);
|
|
205
205
|
if (iv.length !== 16) throw E(prefix + "/bad-algorithm-parameters", "the AES-CBC IV must be 16 octets");
|
|
206
206
|
} catch (e) {
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -216,7 +216,7 @@ function _p12Encode(pw) {
|
|
|
216
216
|
// A view first, so a detached backing store is a reject and never an empty password; then a copy,
|
|
217
217
|
// because everything this function returns is reported owned and gets zeroized after derivation,
|
|
218
218
|
// and handing back a view of a caller's Uint8Array would wipe the caller's own credential.
|
|
219
|
-
if (Buffer.isBuffer(pw) || pw instanceof Uint8Array) return guard.bytes.snapshot(pw, Pkcs12Error, "pkcs12/bad-input", "the password");
|
|
219
|
+
if (Buffer.isBuffer(pw) || pw instanceof Uint8Array) return guard.bytes.snapshot(pw, Pkcs12Error, "pkcs12/bad-input", "the password"); // allow:byte-source-narrow -- password on the ownership contract (string / Buffer / Uint8Array); widening passwords is a separate root decision
|
|
220
220
|
if (typeof pw !== "string") throw _err("pkcs12/bad-input", _MISSING_PASSWORD);
|
|
221
221
|
var out = Buffer.alloc(pw.length * 2 + 2); // + the 2-byte NULL terminator
|
|
222
222
|
for (var i = 0; i < pw.length; i++) {
|
|
@@ -507,7 +507,7 @@ async function _buildAuthSafeElement(sc, opts) {
|
|
|
507
507
|
var alg = sc.contentEncryptionAlgorithm || "aes-256-cbc";
|
|
508
508
|
// RFC 7292 sec. 4.1 lists only id-envelopedData for a public-key privacy safe (never id-ct-authEnvelopedData);
|
|
509
509
|
// cms.encrypt DEFAULTS to aes-256-gcm (AEAD -> AuthEnvelopedData), so force a CBC content cipher and reject GCM.
|
|
510
|
-
if (
|
|
510
|
+
if (alg !== "aes-128-cbc" && alg !== "aes-192-cbc" && alg !== "aes-256-cbc") throw _err("pkcs12/bad-input", "a public-key privacy safe requires an AES-CBC content cipher (aes-128|192|256-cbc), not " + JSON.stringify(alg) + " -- RFC 7292 sec. 4.1 admits only id-envelopedData, not GCM/AuthEnvelopedData");
|
|
511
511
|
var envCi = await cms.encrypt(safeContentsDer, sc.recipients, { contentEncryptionAlgorithm: alg, contentType: "data" });
|
|
512
512
|
return b.raw(envCi); // cms.encrypt returns the whole id-envelopedData ContentInfo DER -- splice it verbatim
|
|
513
513
|
}
|
package/lib/pki-build.js
CHANGED
|
@@ -355,7 +355,7 @@ function makeBuilder(ctx) {
|
|
|
355
355
|
// A DN spec -> RDNSequence DER. A string is shorthand for a single commonName RDN; a Buffer is raw
|
|
356
356
|
// pre-encoded Name DER (validated through the parser). An empty array yields an empty RDNSequence.
|
|
357
357
|
function encodeName(spec) {
|
|
358
|
-
if (
|
|
358
|
+
if (guard.bytes.isByteSource(spec)) { var _nd = guard.bytes.snapshotSource(spec, ErrorClass, ctx.prefix + "/bad-name", "raw Name DER"); assertValidNameDer(_nd); return _nd; }
|
|
359
359
|
if (typeof spec === "string") spec = [{ commonName: spec }];
|
|
360
360
|
if (!_isArray(spec)) throw E("bad-name", "a name must be a string, an array of RDNs, or raw Name DER");
|
|
361
361
|
return b.sequence(spec.map(encodeRdn));
|
|
@@ -560,14 +560,14 @@ function makeBuilder(ctx) {
|
|
|
560
560
|
return nodeCrypto.createHash("sha1").update(keyBytes).digest();
|
|
561
561
|
}
|
|
562
562
|
function skiKeyId(val, spkiDer) {
|
|
563
|
-
if (
|
|
563
|
+
if (guard.bytes.isByteSource(val)) return guard.bytes.snapshotSource(val, ErrorClass, ctx.prefix + "/bad-input", "subjectKeyIdentifier");
|
|
564
564
|
if (val === true) return spkiKeyId(spkiDer);
|
|
565
|
-
throw E("bad-input", "subjectKeyIdentifier must be true (auto-derive) or a
|
|
565
|
+
throw E("bad-input", "subjectKeyIdentifier must be true (auto-derive) or a BufferSource key id");
|
|
566
566
|
}
|
|
567
567
|
|
|
568
568
|
// ---- embedded-input validators ----
|
|
569
569
|
function reqDer(v, what) {
|
|
570
|
-
if (
|
|
570
|
+
if (guard.bytes.isByteSource(v)) return guard.bytes.snapshotSource(v, ErrorClass, ctx.prefix + "/bad-input", what);
|
|
571
571
|
throw E("bad-input", what + " must be a DER Buffer");
|
|
572
572
|
}
|
|
573
573
|
// Full validation of an embedded SubjectPublicKeyInfo, walked through the parser the decoder uses.
|
|
@@ -663,8 +663,8 @@ function makeBuilder(ctx) {
|
|
|
663
663
|
} else if (typeof serial === "bigint") { v = serial; }
|
|
664
664
|
else if (typeof serial === "number") { if (!Number.isSafeInteger(serial)) throw E("bad-serial", "serialNumber number must be a safe integer (pass a BigInt, hex string, or Buffer for a value above 2^53-1)"); v = BigInt(serial); }
|
|
665
665
|
else if (typeof serial === "string") { try { v = BigInt(serial); } catch (e) { throw E("bad-serial", "serialNumber string must be a decimal or 0x-hex integer", e); } }
|
|
666
|
-
else if (
|
|
667
|
-
else { throw E("bad-serial", "serialNumber must be a BigInt, integer, hex string, or
|
|
666
|
+
else if (guard.bytes.isByteSource(serial)) { var _sb = guard.bytes.source(serial, ErrorClass, ctx.prefix + "/bad-serial", "serialNumber"); v = _sb.length ? BigInt("0x" + _sb.toString("hex")) : 0n; }
|
|
667
|
+
else { throw E("bad-serial", "serialNumber must be a BigInt, integer, hex string, or BufferSource"); }
|
|
668
668
|
if (v <= 0n) throw E("bad-serial", "serialNumber must be a positive integer (RFC 5280 sec. 4.1.2.2)");
|
|
669
669
|
var tlv = b.integer(v);
|
|
670
670
|
if (asn1.decode(tlv).content.length > 20) throw E("bad-serial", "serialNumber must not exceed 20 octets (RFC 5280 sec. 4.1.2.2)");
|
package/lib/rfc3339.js
CHANGED
|
@@ -13,23 +13,72 @@
|
|
|
13
13
|
* domain fault code.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
|
|
16
|
+
// Exactly `count` ASCII digits at `start`, as an integer, or null on any non-digit or short read.
|
|
17
|
+
// The grammar is decided character by character (no regex, whose match protocol re-dispatches through
|
|
18
|
+
// a replaceable `exec`); `\d` in the original is ASCII [0-9] only, matched here as 48..57.
|
|
19
|
+
function _digitsAt(v, start, count) {
|
|
20
|
+
if (start + count > v.length) return null;
|
|
21
|
+
var val = 0;
|
|
22
|
+
for (var i = 0; i < count; i++) {
|
|
23
|
+
var c = v.charCodeAt(start + i);
|
|
24
|
+
if (c < 48 || c > 57) return null;
|
|
25
|
+
val = val * 10 + (c - 48);
|
|
26
|
+
}
|
|
27
|
+
return val;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// RFC 3339 date-time scanned into its components, or null on any grammar violation:
|
|
31
|
+
// YYYY "-" MM "-" DD [Tt] HH ":" MM ":" SS, an optional "." + one-or-more fraction digits, then a
|
|
32
|
+
// zone -- [Zz] or ([+-]) OFFHH ":" OFFMM -- with nothing after. Calendar validity is checked by the
|
|
33
|
+
// caller. `offset` carries the numeric-offset hours/minutes when present, else null.
|
|
34
|
+
function _scanDateTime(v) {
|
|
35
|
+
var n = v.length, p = 0;
|
|
36
|
+
var year = _digitsAt(v, p, 4); if (year === null) return null; p += 4;
|
|
37
|
+
if (v.charAt(p) !== "-") return null; p += 1;
|
|
38
|
+
var month = _digitsAt(v, p, 2); if (month === null) return null; p += 2;
|
|
39
|
+
if (v.charAt(p) !== "-") return null; p += 1;
|
|
40
|
+
var day = _digitsAt(v, p, 2); if (day === null) return null; p += 2;
|
|
41
|
+
var t = v.charAt(p); if (t !== "T" && t !== "t") return null; p += 1;
|
|
42
|
+
var hour = _digitsAt(v, p, 2); if (hour === null) return null; p += 2;
|
|
43
|
+
if (v.charAt(p) !== ":") return null; p += 1;
|
|
44
|
+
var min = _digitsAt(v, p, 2); if (min === null) return null; p += 2;
|
|
45
|
+
if (v.charAt(p) !== ":") return null; p += 1;
|
|
46
|
+
var sec = _digitsAt(v, p, 2); if (sec === null) return null; p += 2;
|
|
47
|
+
if (v.charAt(p) === ".") { // optional fractional second: "." + one or more digits
|
|
48
|
+
p += 1;
|
|
49
|
+
var fracStart = p;
|
|
50
|
+
while (p < n) { var fc = v.charCodeAt(p); if (fc < 48 || fc > 57) break; p += 1; }
|
|
51
|
+
if (p === fracStart) return null;
|
|
52
|
+
}
|
|
53
|
+
var z = v.charAt(p), offset = null;
|
|
54
|
+
if (z === "Z" || z === "z") {
|
|
55
|
+
p += 1;
|
|
56
|
+
} else if (z === "+" || z === "-") {
|
|
57
|
+
p += 1;
|
|
58
|
+
var offHour = _digitsAt(v, p, 2); if (offHour === null) return null; p += 2;
|
|
59
|
+
if (v.charAt(p) !== ":") return null; p += 1;
|
|
60
|
+
var offMin = _digitsAt(v, p, 2); if (offMin === null) return null; p += 2;
|
|
61
|
+
offset = { hour: offHour, min: offMin };
|
|
62
|
+
} else {
|
|
63
|
+
return null; // a zone is required
|
|
64
|
+
}
|
|
65
|
+
if (p !== n) return null; // no trailing characters
|
|
66
|
+
return { year: year, month: month, day: day, hour: hour, min: min, sec: sec, offset: offset };
|
|
67
|
+
}
|
|
18
68
|
|
|
19
69
|
// isValid(v) -> boolean: v is a syntactically well-formed AND calendar-valid RFC 3339 date-time string.
|
|
20
70
|
function isValid(v) {
|
|
21
71
|
if (typeof v !== "string") return false;
|
|
22
|
-
var
|
|
23
|
-
if (!
|
|
24
|
-
|
|
25
|
-
if (month < 1 || month > 12) return false;
|
|
72
|
+
var t = _scanDateTime(v);
|
|
73
|
+
if (!t) return false;
|
|
74
|
+
if (t.month < 1 || t.month > 12) return false;
|
|
26
75
|
// Reject a :60 leap second (Node's Date.parse returns NaN for it, so a comparison on such a value
|
|
27
76
|
// would silently pass) and any hour/minute/second out of range.
|
|
28
|
-
if (hour > 23 || min > 59 || sec > 59) return false;
|
|
29
|
-
var leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
77
|
+
if (t.hour > 23 || t.min > 59 || t.sec > 59) return false;
|
|
78
|
+
var leap = (t.year % 4 === 0 && t.year % 100 !== 0) || t.year % 400 === 0;
|
|
30
79
|
var daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
31
|
-
if (day < 1 || day > daysInMonth[month - 1]) return false;
|
|
32
|
-
if (
|
|
80
|
+
if (t.day < 1 || t.day > daysInMonth[t.month - 1]) return false;
|
|
81
|
+
if (t.offset) { if (t.offset.hour > 23 || t.offset.min > 59) return false; } // a numeric zone offset must itself be valid
|
|
33
82
|
return true;
|
|
34
83
|
}
|
|
35
84
|
|
|
@@ -44,20 +93,29 @@ function parse(v, E, code, label) {
|
|
|
44
93
|
// RFC 3339 sec. 5.6 `full-date`: a calendar date with no time component. A distinct production from
|
|
45
94
|
// the date-time above, and deliberately a separate pair of functions instead of a flag: a consumer
|
|
46
95
|
// that expects one and is handed the other has a malformed value, not a permissible variant.
|
|
47
|
-
|
|
96
|
+
// RFC 3339 full-date scanned into its components (YYYY "-" MM "-" DD, nothing after), or null.
|
|
97
|
+
function _scanFullDate(v) {
|
|
98
|
+
var p = 0;
|
|
99
|
+
var year = _digitsAt(v, p, 4); if (year === null) return null; p += 4;
|
|
100
|
+
if (v.charAt(p) !== "-") return null; p += 1;
|
|
101
|
+
var month = _digitsAt(v, p, 2); if (month === null) return null; p += 2;
|
|
102
|
+
if (v.charAt(p) !== "-") return null; p += 1;
|
|
103
|
+
var day = _digitsAt(v, p, 2); if (day === null) return null; p += 2;
|
|
104
|
+
if (p !== v.length) return null;
|
|
105
|
+
return { year: year, month: month, day: day };
|
|
106
|
+
}
|
|
48
107
|
|
|
49
108
|
// isValidDate(v) -> boolean: v is a syntactically well-formed AND calendar-valid RFC 3339 full-date.
|
|
50
109
|
// Calendar validity matters as much as shape: "2021-02-30" parses as a Date in JS by rolling over
|
|
51
110
|
// into March, so a shape-only check would silently accept a day that does not exist.
|
|
52
111
|
function isValidDate(v) {
|
|
53
112
|
if (typeof v !== "string") return false;
|
|
54
|
-
var
|
|
55
|
-
if (!
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
var leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
113
|
+
var t = _scanFullDate(v);
|
|
114
|
+
if (!t) return false;
|
|
115
|
+
if (t.month < 1 || t.month > 12) return false;
|
|
116
|
+
var leap = (t.year % 4 === 0 && t.year % 100 !== 0) || t.year % 400 === 0;
|
|
59
117
|
var daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
60
|
-
return day >= 1 && day <= daysInMonth[month - 1];
|
|
118
|
+
return t.day >= 1 && t.day <= daysInMonth[t.month - 1];
|
|
61
119
|
}
|
|
62
120
|
|
|
63
121
|
// parseDate(v, E, code, label) -> Date at UTC midnight. Anchored explicitly at UTC ("T00:00:00Z")
|
package/lib/schema-c509.js
CHANGED
|
@@ -263,16 +263,37 @@ function _specialText(node) {
|
|
|
263
263
|
// name one certificate. A natively signed certificate is exempt: sec. 3.1.4 says bytes and tag 48
|
|
264
264
|
// there "do not correspond to any predefined text string encoding and may also be used for other attribute
|
|
265
265
|
// types", so no canonical text spelling exists to hold one to.
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
// Even-length lowercase hex ("HHHH..."), decided by character so no regex match protocol -- a live,
|
|
267
|
+
// replaceable exec -- stands between the canonicalization gate and the value it inspects.
|
|
268
|
+
function _isEvenHexLower(s) {
|
|
269
|
+
var n = s.length;
|
|
270
|
+
if (n < 2 || (n & 1) !== 0) return false;
|
|
271
|
+
for (var i = 0; i < n; i++) {
|
|
272
|
+
var c = s.charCodeAt(i);
|
|
273
|
+
if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 102))) return false; // 0-9 a-f
|
|
274
|
+
}
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
// EUI-64 text form: eight uppercase hex pairs joined by single hyphens ("HH-HH-...-HH", 23 chars).
|
|
278
|
+
function _isEui64Text(s) {
|
|
279
|
+
if (s.length !== 23) return false;
|
|
280
|
+
for (var i = 0; i < 23; i++) {
|
|
281
|
+
if ((i % 3) === 2) { if (s.charCodeAt(i) !== 45) return false; } // "-" at 2,5,...,20
|
|
282
|
+
else {
|
|
283
|
+
var c = s.charCodeAt(i);
|
|
284
|
+
if (!((c >= 48 && c <= 57) || (c >= 65 && c <= 70))) return false; // 0-9 A-F
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
268
289
|
function _assertCanonicalSpecialText(node, isNative, E) {
|
|
269
290
|
if (isNative) return;
|
|
270
291
|
if (node.majorType === 3) {
|
|
271
292
|
var t = cbor.read.textString(node);
|
|
272
|
-
if (t.length >= 2 &&
|
|
293
|
+
if (t.length >= 2 && _isEvenHexLower(t)) {
|
|
273
294
|
throw _err(E, "a text attribute value of even-length hex characters must be encoded as a CBOR byte string (draft sec. 3.1.4)");
|
|
274
295
|
}
|
|
275
|
-
if (
|
|
296
|
+
if (_isEui64Text(t)) {
|
|
276
297
|
throw _err(E, "a text attribute value in EUI-64 form must be encoded as a CBOR tag-48 MAC address (draft sec. 3.1.4)");
|
|
277
298
|
}
|
|
278
299
|
return;
|
|
@@ -1973,7 +1994,7 @@ function _reconstructDer(r, sigNode) {
|
|
|
1973
1994
|
// The signature is re-wrapped as a DER ECDSA-Sig-Value from the fixed-width r||s, so only an ECDSA
|
|
1974
1995
|
// signature algorithm is in the type-3 reconstruction covered set (an RSA/EdDSA signature is raw bytes,
|
|
1975
1996
|
// not r||s, so it is rejected). A wrong-length r||s surfaces the caller's typed code.
|
|
1976
|
-
if (
|
|
1997
|
+
if (String(r.signatureAlgorithm.name || "").toLowerCase().indexOf("ecdsa") !== 0) {
|
|
1977
1998
|
throw _err("c509/non-invertible", "type-3 signature reconstruction covers only ECDSA; got " + r.signatureAlgorithm.name);
|
|
1978
1999
|
}
|
|
1979
2000
|
// The fixed-width r||s must split at a real curve field width: P-256/384/521 = 64/96/132 bytes
|
|
@@ -2143,7 +2164,7 @@ function _encAlgorithm(alg, toInt, key) {
|
|
|
2143
2164
|
function _encSpecialText(rdn) {
|
|
2144
2165
|
if (rdn.eui64) return cbor.build.tag(48, cbor.build.byteString(rdn.eui64));
|
|
2145
2166
|
var v = String(rdn.value);
|
|
2146
|
-
if (v.length >= 2 &&
|
|
2167
|
+
if (v.length >= 2 && _isEvenHexLower(v)) return cbor.build.byteString(Buffer.from(v, "hex"));
|
|
2147
2168
|
return cbor.build.textString(v);
|
|
2148
2169
|
}
|
|
2149
2170
|
// A Name -> CBOR: null (issuer only) | a bare SpecialText single utf8 commonName | an array of RDN pairs.
|
|
@@ -2293,9 +2314,15 @@ function _encodeC509Array(r) {
|
|
|
2293
2314
|
// value (draft-20 sec. 3.2.3): an FF-FE-in-the-middle EUI-64 collapses to its 6-byte EUI-48, else the
|
|
2294
2315
|
// 8-byte EUI-64 verbatim. A non-MAC commonName returns null (encoded as text). The exact inverse of
|
|
2295
2316
|
// _macToEui64String, so the reconstruction rebuilds the identical DER commonName string.
|
|
2317
|
+
// The "-" characters removed from a string, by character scan (no regex replace protocol).
|
|
2318
|
+
function _stripHyphens(s) {
|
|
2319
|
+
var out = "";
|
|
2320
|
+
for (var i = 0; i < s.length; i++) { var ch = s.charAt(i); if (ch !== "-") out += ch; }
|
|
2321
|
+
return out;
|
|
2322
|
+
}
|
|
2296
2323
|
function _euiFromCn(value) {
|
|
2297
|
-
if (
|
|
2298
|
-
var bytes = Buffer.from(value
|
|
2324
|
+
if (!_isEui64Text(value)) return null;
|
|
2325
|
+
var bytes = Buffer.from(_stripHyphens(value), "hex");
|
|
2299
2326
|
if (bytes[3] === 0xff && bytes[4] === 0xfe) return Buffer.concat([bytes.subarray(0, 3), bytes.subarray(5)]);
|
|
2300
2327
|
return bytes;
|
|
2301
2328
|
}
|
|
@@ -2415,7 +2442,7 @@ _derToType3 = function (input, opts) {
|
|
|
2415
2442
|
// (A v3 certificate with the extensions field OMITTED is fully supported: sec. 3.1.10 encodes
|
|
2416
2443
|
// an omitted 'extensions' field as an empty CBOR array.)
|
|
2417
2444
|
if (c.version !== 3) throw _err("c509/non-invertible", "C509 covers X.509 v3 certificates; got v" + c.version);
|
|
2418
|
-
if (
|
|
2445
|
+
if (String(c.signatureAlgorithm.name || "").toLowerCase().indexOf("ecdsa") !== 0) throw _err("c509/non-invertible", "type-3 C509 encoding covers only ECDSA-signed certificates; got " + (c.signatureAlgorithm.name || "an unregistered algorithm"));
|
|
2419
2446
|
if (c.subjectPublicKeyInfo.algorithm.name !== "ecPublicKey") throw _err("c509/non-invertible", "type-3 C509 encoding covers only EC (ecPublicKey) certificates in v1; got " + (c.subjectPublicKeyInfo.algorithm.name || "an unregistered algorithm"));
|
|
2420
2447
|
var curveOid = asn1.read.oid(asn1.decode(c.subjectPublicKeyInfo.algorithm.parameters));
|
|
2421
2448
|
var curve = oid.name(curveOid);
|