@blamejs/pki 0.6.36 → 0.6.38
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 +28 -0
- package/lib/path-validate.js +307 -23
- package/lib/schema-c509.js +70 -20
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@ 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.6.38 — 2026-09-05
|
|
8
|
+
|
|
9
|
+
A trust anchor can name the namespace its root is trusted for, and pki.path.validate enforces it as the initial name-constraint state.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- A trust anchor passed to pki.path.validate or pki.path.build may carry nameConstraints: { permitted, excluded }, each an array of { tag, base } subtrees. They seed the RFC 5280 section 6.1.1(h)(i) initial permitted and excluded state, so a root a program trusts only for a namespace enforces that namespace even though its certificate states no nameConstraints extension. The anchor's subtrees intersect with an intermediate's own extension and with opts.initialPermittedSubtrees; excluded subtrees are a union.
|
|
14
|
+
- The pki.path.validate verdict reports anchorConstraints.nameConstraintsApplied, so a stored verdict distinguishes an anchor that named a namespace from one that did not, alongside the distrustAfterApplied and purposeTrustApplied it already carries.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- pki.path.validate and pki.path.build copy a directoryName subtree base and snapshot an iPAddress one before validating, and read opts.initialPermittedSubtrees and opts.initialExcludedSubtrees once rather than again for each trust anchor or candidate path tried. A caller mutating its own subtree list, entry or base while the path is still verifying signatures no longer changes the namespace the validation enforces; the subtrees that bind are the ones read at entry.
|
|
19
|
+
- A directoryName subtree base whose rdns is an accessor rather than a data property is refused. Its value was read once to check its shape and again to copy it, so an accessor could answer with a restrictive name the first time and an empty one the second, and an empty directoryName constraint matches every name.
|
|
20
|
+
- A subtree list that is a Proxy, that is sparse, or whose entries are accessors is refused, on a trust anchor and on opts.initialPermittedSubtrees and opts.initialExcludedSubtrees alike. Such a list answers its own length and index reads, so it could hold a restrictive subtree while reporting none and leave the namespace unenforced. A directoryName subtree is built from property descriptors alone, so nothing the caller supplies runs while the namespace is read: an accessor, own or inherited, is refused rather than invoked. It is a relative-name sequence of attribute lists and nothing deeper, so an array nested past that, including one that refers to itself, is a typed refusal, as is a hole, an entry the array does not own, an attribute that is not an object, or one lacking its own type and value. Each of those refusals is a configuration error reported wherever the anchor sits among several, rather than a name mismatch a different anchor could mask.
|
|
21
|
+
- A parsed certificate passed as a trust anchor is refused if it carries nameConstraints, the way one carrying purposes or distrustAfter already is. A certificate anchor cannot hold that metadata, so accepting it would have dropped the namespace and validated a leaf the anchor was meant to exclude.
|
|
22
|
+
- An anchor whose nameConstraints names a field other than permitted or excluded is refused rather than read as unconstrained, and its subtrees are checked while the anchor is normalized, so a malformed one is refused wherever that anchor sits among several rather than only once it is reached.
|
|
23
|
+
|
|
24
|
+
## v0.6.37 — 2026-09-05
|
|
25
|
+
|
|
26
|
+
A natively signed C509 certificate is refused when it writes an extension in the generic OID form that the registry can carry as an identifier.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- pki.schema.c509.parse refuses a natively signed certificate (c509CertificateType 2) that writes an extension as an OID and a DER value when a C509 registry identifier carries the same extension value. Draft section 3.7 reserves the generic form for re-encoded certificates, where it stays accepted. An extension whose OID has no registry identifier, and one whose value the compact encodings cannot represent under that certificate type's rules, both keep the generic form.
|
|
31
|
+
- pki.schema.c509.encode chooses an extension's form under the rules of the certificate type it is writing. It previously judged whether a value could be written compactly using the rules for a re-encoded certificate, so a natively signed certificate could be given a compact form its own decoder does not accept.
|
|
32
|
+
- pki.schema.c509.encode resolves an extension named only by its OID to the matching registry identifier. It looked the identifier up by name alone, so an extension supplied without one was written in the generic OID form even where a compact encoding carried it.
|
|
33
|
+
- pki.schema.c509 decides what belongs to the C509 registries by dotted OID rather than by the name pki.oid.register maps that OID to. Registering a different name for a built-in OID previously changed what the toolkit emitted for an unrelated certificate, and for commonName, a curve, ecPublicKey or an ECDSA signature algorithm it stopped a certificate converting to C509 at all.
|
|
34
|
+
|
|
7
35
|
## v0.6.36 — 2026-09-04
|
|
8
36
|
|
|
9
37
|
A natively signed C509 certificate is refused when it spells an algorithm generically instead of using the registry entry that names the same value.
|
package/lib/path-validate.js
CHANGED
|
@@ -553,19 +553,61 @@ function isSubtreeBaseValid(tag, base) {
|
|
|
553
553
|
|
|
554
554
|
function checkedSubtreeSeeds(list, optName) {
|
|
555
555
|
if (list === undefined || list === null) return [];
|
|
556
|
-
if (!intrinsic.isArray(list)) throw E("path/bad-input", "validate: opts." + optName + " must be
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
556
|
+
if (!intrinsic.isArray(list) || intrinsic.types.isProxy(list)) throw E("path/bad-input", "validate: opts." + optName + " must be a plain array of { tag, base } subtree entries");
|
|
557
|
+
for (var li = 0; li < list.length; li++) {
|
|
558
|
+
var ld = intrinsic.getOwnPropertyDescriptor(list, li);
|
|
559
|
+
if (ld === undefined) throw E("path/bad-input", "validate: opts." + optName + " must not be a sparse array; index " + li + " is a hole");
|
|
560
|
+
if (!intrinsic.hasOwn(ld, "value")) throw E("path/bad-input", "validate: opts." + optName + " entry " + li + " must be a data property, not an accessor");
|
|
561
|
+
}
|
|
562
|
+
/** @internal Built with a plain loop into a fresh array: Array.prototype.map consults the caller
|
|
563
|
+
* array's constructor and Symbol.species, which is more caller code running mid-copy. */
|
|
564
|
+
var seedsOut = [];
|
|
565
|
+
for (var si = 0; si < list.length; si++) {
|
|
566
|
+
intrinsic.push(seedsOut, (function (st) {
|
|
567
|
+
var shapeMsg = "validate: opts." + optName + " entries must be { tag: <GeneralName tag number 0..8>, base: <that form's constraint value> }";
|
|
568
|
+
if (!st || typeof st !== "object" || intrinsic.types.isProxy(st)) throw E("path/bad-input", shapeMsg);
|
|
569
|
+
/** @internal Read tag and base once each, from their own data properties: an accessor could
|
|
570
|
+
* answer with a restrictive value to a check and a permissive one to the copy. */
|
|
571
|
+
var tagD = intrinsic.getOwnPropertyDescriptor(st, "tag");
|
|
572
|
+
var baseD = intrinsic.getOwnPropertyDescriptor(st, "base");
|
|
573
|
+
if (tagD !== undefined && !intrinsic.hasOwn(tagD, "value")) throw E("path/bad-input", "validate: opts." + optName + " entry tag must be a data property, not an accessor");
|
|
574
|
+
if (baseD !== undefined && !intrinsic.hasOwn(baseD, "value")) throw E("path/bad-input", "validate: opts." + optName + " entry base must be a data property, not an accessor");
|
|
575
|
+
var tag = tagD === undefined ? undefined : tagD.value;
|
|
576
|
+
var base = baseD === undefined ? undefined : baseD.value;
|
|
577
|
+
if (!Number.isInteger(tag) || tag < 0 || tag > 8) throw E("path/bad-input", shapeMsg);
|
|
578
|
+
if (tag === 7 && guard.bytes.isByteSource(base)) {
|
|
579
|
+
base = guard.bytes.snapshotSource(base, PathError, "path/bad-input", "an iPAddress subtree base");
|
|
560
580
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
581
|
+
if (tag === 4 && base !== null && typeof base === "object") {
|
|
582
|
+
var seedRdns = _capturedRdns(base, "validate: opts." + optName);
|
|
583
|
+
if (seedRdns !== undefined) {
|
|
584
|
+
var dnCopy = {};
|
|
585
|
+
intrinsic.defineProperty(dnCopy, "rdns", { value: seedRdns, enumerable: true, configurable: true, writable: true });
|
|
586
|
+
base = dnCopy;
|
|
587
|
+
}
|
|
566
588
|
}
|
|
567
|
-
|
|
568
|
-
|
|
589
|
+
if (!isSubtreeBaseValid(tag, base)) throw E("path/bad-input", shapeMsg);
|
|
590
|
+
return { tag: tag, base: base };
|
|
591
|
+
})(intrinsic.getOwnPropertyDescriptor(list, si).value));
|
|
592
|
+
}
|
|
593
|
+
return seedsOut;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* @internal RFC 5280 sec. 6.1.1(h)(i): the anchor's own permitted/excluded subtrees, the way a root
|
|
598
|
+
* program restricts a root to a namespace its certificate does not state. Validated through the same
|
|
599
|
+
* entry-point checker the caller's seeds use, so a mis-shaped subtree is refused rather than dropped.
|
|
600
|
+
*/
|
|
601
|
+
function _anchorSubtreeSeeds(anchor) {
|
|
602
|
+
var nc = anchor.nameConstraints;
|
|
603
|
+
if (nc === undefined || nc === null) return { permitted: [], excluded: [] };
|
|
604
|
+
if (typeof nc !== "object" || intrinsic.isArray(nc)) {
|
|
605
|
+
throw E("path/bad-input", "validate: a trustAnchor nameConstraints must be a { permitted, excluded } object of { tag, base } subtree entries");
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
permitted: checkedSubtreeSeeds(nc.permitted, "trustAnchors[].nameConstraints.permitted"),
|
|
609
|
+
excluded: checkedSubtreeSeeds(nc.excluded, "trustAnchors[].nameConstraints.excluded"),
|
|
610
|
+
};
|
|
569
611
|
}
|
|
570
612
|
|
|
571
613
|
function initialize(certs, params, seeds, anchor) {
|
|
@@ -574,7 +616,7 @@ function initialize(certs, params, seeds, anchor) {
|
|
|
574
616
|
validPolicyTree: rootNode(),
|
|
575
617
|
policyNodeCount: 1,
|
|
576
618
|
maxPolicyNodes: params.maxPolicyNodes !== undefined ? params.maxPolicyNodes : constants.LIMITS.PATH_MAX_POLICY_NODES,
|
|
577
|
-
permittedGenerations: seeds.
|
|
619
|
+
permittedGenerations: intrinsic.filter(seeds.permittedGenerations, function (g) { return g.length > 0; }),
|
|
578
620
|
excludedSubtrees: seeds.excluded,
|
|
579
621
|
explicitPolicy: params.initialExplicitPolicy ? 0 : n + 1,
|
|
580
622
|
inhibitAnyPolicy: params.initialAnyPolicyInhibit ? 0 : n + 1,
|
|
@@ -867,10 +909,21 @@ function validateCriticalExtensionStructure(cert) {
|
|
|
867
909
|
*
|
|
868
910
|
* `anchorConstraints` reports what the anchor's own trust metadata decided:
|
|
869
911
|
* the `checkedPurpose` it was judged under, and whether the `distrustAfter`
|
|
870
|
-
* date
|
|
871
|
-
*
|
|
872
|
-
*
|
|
873
|
-
*
|
|
912
|
+
* date, the `purposes` delegator map and the `nameConstraints` namespace each
|
|
913
|
+
* applied. The purpose-keyed metadata is keyed BY key purpose, so an anchor
|
|
914
|
+
* carrying it while `opts.checkPurpose` is absent is a configuration fault
|
|
915
|
+
* (`path/bad-input`). A silently inert constraint would let a root distrusted
|
|
916
|
+
* years ago quietly validate a current leaf.
|
|
917
|
+
*
|
|
918
|
+
* An anchor may also carry `nameConstraints`, a `{ permitted, excluded }` pair
|
|
919
|
+
* of `{ tag, base }` subtrees naming the namespace that root is trusted for.
|
|
920
|
+
* A root program restricts a root this way when the restriction appears in no
|
|
921
|
+
* `nameConstraints` extension the root certificate carries. The subtrees become
|
|
922
|
+
* the RFC 5280 sec. 6.1.1(h)(i) initial permitted and excluded state, so an
|
|
923
|
+
* intermediate's own `nameConstraints` extension intersects with them and a leaf
|
|
924
|
+
* must satisfy both. They are a separate generation from
|
|
925
|
+
* `opts.initialPermittedSubtrees`, so those two intersect as well. A mis-shaped
|
|
926
|
+
* subtree is refused with `path/bad-input` rather than left inert.
|
|
874
927
|
*
|
|
875
928
|
* @example
|
|
876
929
|
* var pair = await pki.key.generate("Ed25519");
|
|
@@ -938,17 +991,35 @@ async function validate(path, opts) {
|
|
|
938
991
|
var certs = path.map(function (c, ci) { return coerceCert(c, "validate: path[" + ci + "]"); });
|
|
939
992
|
var n = certs.length;
|
|
940
993
|
if (n < 1) throw E("path/empty-path", "validate: the certification path is empty");
|
|
994
|
+
/** @internal Captured before any anchor is normalized: an anchor field may be an accessor, and
|
|
995
|
+
* that getter runs caller code which could otherwise clear these lists first. */
|
|
996
|
+
var callerPermitted = checkedSubtreeSeeds(opts.initialPermittedSubtrees, "initialPermittedSubtrees");
|
|
997
|
+
var callerExcluded = checkedSubtreeSeeds(opts.initialExcludedSubtrees, "initialExcludedSubtrees");
|
|
941
998
|
var rawAnchors = opts.trustAnchors;
|
|
942
999
|
var anchorList = intrinsic.isArray(rawAnchors) ? rawAnchors : [rawAnchors];
|
|
943
1000
|
if (rawAnchors == null || anchorList.length === 0) throw E("path/bad-input", "validate: trustAnchors is required (a single anchor or a non-empty array of anchors)");
|
|
1001
|
+
/** @internal Read the caller's list once here, whatever its length, so neither branch below reads
|
|
1002
|
+
* one of its indices twice. */
|
|
1003
|
+
if (intrinsic.isArray(rawAnchors)) anchorList = _ownAnchorEntries(rawAnchors, "validate");
|
|
944
1004
|
if (anchorList.length > 1) {
|
|
1005
|
+
var anchorEntries = anchorList;
|
|
1006
|
+
var preNc = _precaptureAnchorNameConstraints(anchorEntries, "validate");
|
|
945
1007
|
var tuples = [];
|
|
946
|
-
for (var ti = 0; ti <
|
|
1008
|
+
for (var ti = 0; ti < anchorEntries.length; ti++) {
|
|
1009
|
+
tuples[ti] = toAnchor(anchorEntries[ti], "validate");
|
|
1010
|
+
if (preNc[ti] !== undefined) intrinsic.defineProperty(tuples[ti], "nameConstraints", { value: preNc[ti], enumerable: true, configurable: true, writable: true });
|
|
1011
|
+
}
|
|
1012
|
+
/** @internal Each anchor is a separate attempt and the later ones run after an await, so the
|
|
1013
|
+
* caller's own subtree lists are read once here rather than again per attempt. */
|
|
1014
|
+
var permRawOnce = opts.initialPermittedSubtrees;
|
|
1015
|
+
var exclRawOnce = opts.initialExcludedSubtrees;
|
|
947
1016
|
var singleOpts = function (a) {
|
|
948
1017
|
var keys = intrinsic.ownKeys(opts);
|
|
949
1018
|
var o = {};
|
|
950
1019
|
for (var ki = 0; ki < keys.length; ki++) { o[keys[ki]] = opts[keys[ki]]; }
|
|
951
1020
|
o.trustAnchors = a;
|
|
1021
|
+
if (permRawOnce != null) o.initialPermittedSubtrees = callerPermitted;
|
|
1022
|
+
if (exclRawOnce != null) o.initialExcludedSubtrees = callerExcluded;
|
|
952
1023
|
return o;
|
|
953
1024
|
};
|
|
954
1025
|
var lastMatched = null, anyMatched = false;
|
|
@@ -965,10 +1036,12 @@ async function validate(path, opts) {
|
|
|
965
1036
|
guard.time.assertValid(opts.time, E, "path/bad-input", "validate: opts.time (the always-on validity-window check date)");
|
|
966
1037
|
guard.limits.cap(opts.maxPolicyNodes, "validate: opts.maxPolicyNodes", undefined, { E: E, code: "path/bad-input", min: 1 });
|
|
967
1038
|
_assertUserInitialPolicySet(opts.userInitialPolicySet, "validate");
|
|
1039
|
+
var anchorNc = _anchorSubtreeSeeds(anchor);
|
|
968
1040
|
var seeds = {
|
|
969
|
-
|
|
970
|
-
excluded:
|
|
1041
|
+
permittedGenerations: [callerPermitted, anchorNc.permitted],
|
|
1042
|
+
excluded: intrinsic.concat(callerExcluded, anchorNc.excluded),
|
|
971
1043
|
};
|
|
1044
|
+
var anchorNameConstraintsApplied = anchorNc.permitted.length > 0 || anchorNc.excluded.length > 0;
|
|
972
1045
|
var purposeOpts = resolvePurposeOpts(opts);
|
|
973
1046
|
var requiredEku = purposeOpts.requiredEku;
|
|
974
1047
|
var checkPurpose = purposeOpts.checkPurpose;
|
|
@@ -1158,6 +1231,7 @@ async function validate(path, opts) {
|
|
|
1158
1231
|
checkedPurpose: checkPurpose || null,
|
|
1159
1232
|
distrustAfterApplied: anchorDistrustApplied,
|
|
1160
1233
|
purposeTrustApplied: anchorPurposeApplied,
|
|
1234
|
+
nameConstraintsApplied: anchorNameConstraintsApplied,
|
|
1161
1235
|
},
|
|
1162
1236
|
path: certs,
|
|
1163
1237
|
results: state.results,
|
|
@@ -1903,6 +1977,198 @@ function _captureOwnData(entry, key) {
|
|
|
1903
1977
|
return (d && intrinsic.hasOwn(d, "value")) ? d.value : undefined;
|
|
1904
1978
|
}
|
|
1905
1979
|
|
|
1980
|
+
/**
|
|
1981
|
+
* @internal Copy the anchor's subtree lists getter-free, so a caller mutating the entry afterwards
|
|
1982
|
+
* cannot widen the namespace the validation already resolved.
|
|
1983
|
+
*/
|
|
1984
|
+
/**
|
|
1985
|
+
* @internal Read a directoryName base's `rdns` once, from its own data property. Reading it twice,
|
|
1986
|
+
* to test its shape and then to copy it, lets an accessor answer differently each time and hand the
|
|
1987
|
+
* validation an empty DN, which matches every name.
|
|
1988
|
+
*/
|
|
1989
|
+
var _ATV_KEYS = ["type", "value", "name"];
|
|
1990
|
+
/**
|
|
1991
|
+
* @internal Build the directoryName's own copy from property descriptors alone, so no caller code
|
|
1992
|
+
* runs while the namespace is read: a getter, own or inherited, could clear another part of the
|
|
1993
|
+
* namespace before it is captured. A directoryName is a relative-name sequence of attribute lists
|
|
1994
|
+
* and nothing deeper, so exactly those levels are walked and a self-referencing array is refused
|
|
1995
|
+
* rather than followed.
|
|
1996
|
+
*/
|
|
1997
|
+
function _copyDnRdns(rdns, who) {
|
|
1998
|
+
_assertPlainDnRdns(rdns, who);
|
|
1999
|
+
var out = [];
|
|
2000
|
+
for (var i = 0; i < rdns.length; i++) {
|
|
2001
|
+
var rd = intrinsic.getOwnPropertyDescriptor(rdns, i);
|
|
2002
|
+
if (rd === undefined) throw E("path/bad-input", who + " index " + i + " is a hole or is inherited; every entry must be its own");
|
|
2003
|
+
var rdn = rd.value;
|
|
2004
|
+
if (!intrinsic.isArray(rdn)) throw E("path/bad-input", who + "[" + i + "] must be an array of attributes");
|
|
2005
|
+
var rdnOut = [];
|
|
2006
|
+
for (var j = 0; j < rdn.length; j++) {
|
|
2007
|
+
var ad = intrinsic.getOwnPropertyDescriptor(rdn, j);
|
|
2008
|
+
if (ad === undefined) throw E("path/bad-input", who + "[" + i + "] index " + j + " is a hole or is inherited; every entry must be its own");
|
|
2009
|
+
var atv = ad.value;
|
|
2010
|
+
if (atv === null || typeof atv !== "object") throw E("path/bad-input", who + "[" + i + "][" + j + "] must be an attribute object carrying a type and a value");
|
|
2011
|
+
var atvOut = {};
|
|
2012
|
+
for (var k = 0; k < _ATV_KEYS.length; k++) {
|
|
2013
|
+
var fd = intrinsic.getOwnPropertyDescriptor(atv, _ATV_KEYS[k]);
|
|
2014
|
+
if (fd === undefined) continue;
|
|
2015
|
+
intrinsic.defineProperty(atvOut, _ATV_KEYS[k], { value: fd.value, enumerable: true, configurable: true, writable: true });
|
|
2016
|
+
}
|
|
2017
|
+
if (!intrinsic.hasOwn(atvOut, "type") || !intrinsic.hasOwn(atvOut, "value")) {
|
|
2018
|
+
throw E("path/bad-input", who + "[" + i + "][" + j + "] must carry its type and value as own data properties");
|
|
2019
|
+
}
|
|
2020
|
+
intrinsic.push(rdnOut, atvOut);
|
|
2021
|
+
}
|
|
2022
|
+
intrinsic.push(out, rdnOut);
|
|
2023
|
+
}
|
|
2024
|
+
return out;
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* @internal The shape check the copy above relies on: a Proxy answers its own reads and an accessor
|
|
2028
|
+
* runs caller code, so either one at any level of the directoryName is refused before it is copied.
|
|
2029
|
+
*/
|
|
2030
|
+
function _assertPlainDnRdns(rdns, who) {
|
|
2031
|
+
if (intrinsic.types.isProxy(rdns)) throw E("path/bad-input", who + " must be a plain array, not a Proxy");
|
|
2032
|
+
for (var i = 0; i < rdns.length; i++) {
|
|
2033
|
+
var rd = intrinsic.getOwnPropertyDescriptor(rdns, i);
|
|
2034
|
+
if (rd === undefined) continue;
|
|
2035
|
+
if (!intrinsic.hasOwn(rd, "value")) throw E("path/bad-input", who + " entry " + i + " must be a data property, not an accessor");
|
|
2036
|
+
var rdn = rd.value;
|
|
2037
|
+
if (!intrinsic.isArray(rdn)) continue;
|
|
2038
|
+
if (intrinsic.types.isProxy(rdn)) throw E("path/bad-input", who + "[" + i + "] must be a plain array, not a Proxy");
|
|
2039
|
+
for (var j = 0; j < rdn.length; j++) {
|
|
2040
|
+
var ad = intrinsic.getOwnPropertyDescriptor(rdn, j);
|
|
2041
|
+
if (ad === undefined) continue;
|
|
2042
|
+
if (!intrinsic.hasOwn(ad, "value")) throw E("path/bad-input", who + "[" + i + "] entry " + j + " must be a data property, not an accessor");
|
|
2043
|
+
var atv = ad.value;
|
|
2044
|
+
if (atv === null || typeof atv !== "object") continue;
|
|
2045
|
+
if (intrinsic.types.isProxy(atv)) throw E("path/bad-input", who + "[" + i + "][" + j + "] must not be a Proxy");
|
|
2046
|
+
if (intrinsic.isArray(atv)) throw E("path/bad-input", who + "[" + i + "][" + j + "] must be an attribute object, not an array");
|
|
2047
|
+
for (var k = 0; k < _ATV_KEYS.length; k++) {
|
|
2048
|
+
var fd = intrinsic.getOwnPropertyDescriptor(atv, _ATV_KEYS[k]);
|
|
2049
|
+
if (fd !== undefined && !intrinsic.hasOwn(fd, "value")) {
|
|
2050
|
+
throw E("path/bad-input", who + "[" + i + "][" + j + "]." + _ATV_KEYS[k] + " must be a data property, not an accessor");
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
function _capturedRdns(base, who) {
|
|
2057
|
+
if (intrinsic.types.isProxy(base)) throw E("path/bad-input", who + ": a trustAnchor nameConstraints directoryName base must not be a Proxy");
|
|
2058
|
+
/** @internal An inherited Proxy answers `in` through its has trap, which is caller code running
|
|
2059
|
+
* inside what is meant to be a getter-free read. */
|
|
2060
|
+
var baseProto = intrinsic.getPrototypeOf(base);
|
|
2061
|
+
while (baseProto !== null) {
|
|
2062
|
+
if (intrinsic.types.isProxy(baseProto)) throw E("path/bad-input", who + ": a trustAnchor nameConstraints directoryName base must not inherit from a Proxy");
|
|
2063
|
+
baseProto = intrinsic.getPrototypeOf(baseProto);
|
|
2064
|
+
}
|
|
2065
|
+
var d = intrinsic.getOwnPropertyDescriptor(base, "rdns");
|
|
2066
|
+
if (d === undefined) {
|
|
2067
|
+
if ("rdns" in base) throw E("path/bad-input", who + ": a trustAnchor nameConstraints directoryName rdns must be an own data property, not inherited");
|
|
2068
|
+
return undefined;
|
|
2069
|
+
}
|
|
2070
|
+
if (!intrinsic.hasOwn(d, "value")) throw E("path/bad-input", who + ": a trustAnchor nameConstraints directoryName rdns must be a data property, not an accessor");
|
|
2071
|
+
if (!intrinsic.isArray(d.value)) return undefined;
|
|
2072
|
+
return _copyDnRdns(d.value, who + ": a trustAnchor nameConstraints directoryName rdns");
|
|
2073
|
+
}
|
|
2074
|
+
var _NC_SUBTREE_KEYS = ["tag", "base"];
|
|
2075
|
+
var _NC_LIST_KEYS = ["permitted", "excluded"];
|
|
2076
|
+
function _copyAnchorSubtreeList(list, key, who) {
|
|
2077
|
+
if (list === undefined || list === null) return undefined;
|
|
2078
|
+
if (!intrinsic.isArray(list) || intrinsic.types.isProxy(list)) throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " must be a plain array of { tag, base } subtree entries");
|
|
2079
|
+
var copied = [];
|
|
2080
|
+
var len = list.length;
|
|
2081
|
+
for (var i = 0; i < len; i++) {
|
|
2082
|
+
var entryD = intrinsic.getOwnPropertyDescriptor(list, i);
|
|
2083
|
+
if (entryD === undefined) throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " must not be a sparse array; index " + i + " is a hole");
|
|
2084
|
+
if (!intrinsic.hasOwn(entryD, "value")) throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " entry " + i + " must be a data property, not an accessor");
|
|
2085
|
+
var st = entryD.value;
|
|
2086
|
+
if (st === null || typeof st !== "object" || intrinsic.types.isProxy(st)) {
|
|
2087
|
+
throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " entry must be a plain { tag, base } object");
|
|
2088
|
+
}
|
|
2089
|
+
var out = {};
|
|
2090
|
+
for (var k = 0; k < _NC_SUBTREE_KEYS.length; k++) {
|
|
2091
|
+
var field = _NC_SUBTREE_KEYS[k];
|
|
2092
|
+
var fd = intrinsic.getOwnPropertyDescriptor(st, field);
|
|
2093
|
+
if (fd === undefined) continue;
|
|
2094
|
+
if (!intrinsic.hasOwn(fd, "value")) throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " " + field + " must be a data property, not an accessor");
|
|
2095
|
+
var fv = fd.value;
|
|
2096
|
+
if (field === "base" && fv !== null && typeof fv === "object") {
|
|
2097
|
+
var rdnsOwn = _capturedRdns(fv, who);
|
|
2098
|
+
if (rdnsOwn !== undefined) {
|
|
2099
|
+
var dnBase = {};
|
|
2100
|
+
intrinsic.defineProperty(dnBase, "rdns", { value: rdnsOwn, enumerable: true, configurable: true, writable: true });
|
|
2101
|
+
fv = dnBase;
|
|
2102
|
+
} else if (guard.bytes.isByteSource(fv)) {
|
|
2103
|
+
fv = guard.bytes.snapshotSource(fv, PathError, "path/bad-input", who + ": a trustAnchor nameConstraints iPAddress subtree base");
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
intrinsic.defineProperty(out, field, { value: fv, enumerable: true, configurable: true, writable: true });
|
|
2107
|
+
}
|
|
2108
|
+
intrinsic.push(copied, out);
|
|
2109
|
+
}
|
|
2110
|
+
return copied;
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* @internal Anchors are normalized one at a time, and each may expose fields through getters that
|
|
2114
|
+
* run caller code. Capture every anchor's namespace first, so an earlier anchor's getter cannot
|
|
2115
|
+
* clear a later anchor's restriction before that anchor is reached.
|
|
2116
|
+
*/
|
|
2117
|
+
/**
|
|
2118
|
+
* @internal Read a caller's anchor list once, from its own indices, into a fresh array. The list is
|
|
2119
|
+
* walked twice, to capture namespaces and then to normalize, and an accessor index could answer
|
|
2120
|
+
* differently each time.
|
|
2121
|
+
*/
|
|
2122
|
+
function _ownAnchorEntries(list, who) {
|
|
2123
|
+
if (intrinsic.types.isProxy(list)) throw E("path/bad-input", who + ": trustAnchors must be a plain array, not a Proxy");
|
|
2124
|
+
var out = [];
|
|
2125
|
+
for (var i = 0; i < list.length; i++) {
|
|
2126
|
+
var d = intrinsic.getOwnPropertyDescriptor(list, i);
|
|
2127
|
+
if (d === undefined) throw E("path/bad-input", who + ": trustAnchors index " + i + " is a hole or is inherited; every entry must be its own");
|
|
2128
|
+
if (!intrinsic.hasOwn(d, "value")) throw E("path/bad-input", who + ": trustAnchors entry " + i + " must be a data property, not an accessor");
|
|
2129
|
+
intrinsic.push(out, d.value);
|
|
2130
|
+
}
|
|
2131
|
+
return out;
|
|
2132
|
+
}
|
|
2133
|
+
function _precaptureAnchorNameConstraints(list, who) {
|
|
2134
|
+
var out = [];
|
|
2135
|
+
for (var i = 0; i < list.length; i++) {
|
|
2136
|
+
var entry = list[i];
|
|
2137
|
+
var captured;
|
|
2138
|
+
if (entry && typeof entry === "object" && !intrinsic.isBuffer(entry) && !guard.parsed.isRecorded(entry) && !intrinsic.types.isProxy(entry)) {
|
|
2139
|
+
var raw = _captureConstraintMap(entry, "nameConstraints", E, "path/bad-input", who);
|
|
2140
|
+
captured = raw == null ? undefined : _copyAnchorNameConstraints(raw, who);
|
|
2141
|
+
}
|
|
2142
|
+
intrinsic.push(out, captured);
|
|
2143
|
+
}
|
|
2144
|
+
return out;
|
|
2145
|
+
}
|
|
2146
|
+
function _copyAnchorNameConstraints(nc, who) {
|
|
2147
|
+
if (nc === null || typeof nc !== "object" || intrinsic.isArray(nc)) {
|
|
2148
|
+
throw E("path/bad-input", who + ": a trustAnchor nameConstraints must be a { permitted, excluded } object of { tag, base } subtree entries");
|
|
2149
|
+
}
|
|
2150
|
+
guard.identifier.assertPlainRecord(nc, E, "path/bad-input", who + ": a trustAnchor nameConstraints");
|
|
2151
|
+
guard.identifier.assertKnownKeys(nc, { permitted: 1, excluded: 1 }, E, "path/bad-input",
|
|
2152
|
+
who + ": a trustAnchor nameConstraints names its subtree lists `permitted` and `excluded`. The unknown key was: ");
|
|
2153
|
+
var out = {};
|
|
2154
|
+
for (var i = 0; i < _NC_LIST_KEYS.length; i++) {
|
|
2155
|
+
var key = _NC_LIST_KEYS[i];
|
|
2156
|
+
var d = intrinsic.getOwnPropertyDescriptor(nc, key);
|
|
2157
|
+
if (d === undefined) {
|
|
2158
|
+
if (key in nc) throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " must be an own data property, not inherited");
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
2161
|
+
if (!intrinsic.hasOwn(d, "value")) throw E("path/bad-input", who + ": a trustAnchor nameConstraints." + key + " must be a data property, not an accessor");
|
|
2162
|
+
var copiedList = _copyAnchorSubtreeList(d.value, key, who);
|
|
2163
|
+
if (copiedList === undefined) continue;
|
|
2164
|
+
/** @internal Validated while the anchor is normalized, so a malformed subtree is refused
|
|
2165
|
+
* wherever the anchor sits in the list rather than only once that anchor is reached. */
|
|
2166
|
+
var checkedList = checkedSubtreeSeeds(copiedList, "trustAnchors[].nameConstraints." + key);
|
|
2167
|
+
intrinsic.defineProperty(out, key, { value: checkedList, enumerable: true, configurable: true, writable: true });
|
|
2168
|
+
}
|
|
2169
|
+
return out;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
1906
2172
|
function _copyAtv(atv) {
|
|
1907
2173
|
var out = {};
|
|
1908
2174
|
var t = atv.type;
|
|
@@ -1939,7 +2205,7 @@ function _copyRdnsDeep(rdns) {
|
|
|
1939
2205
|
function toAnchor(entry, verb) {
|
|
1940
2206
|
var who = verb || "build";
|
|
1941
2207
|
var isTuple = false, alg = null, pubRaw, nameRaw = null, pk = null, purposesSnap, distrustSnap;
|
|
1942
|
-
var subjectDerRaw, labelRaw, mozRaw;
|
|
2208
|
+
var subjectDerRaw, labelRaw, mozRaw, ncSnap;
|
|
1943
2209
|
if (intrinsic.types.isProxy(entry)) {
|
|
1944
2210
|
throw E("path/bad-input", who + ": a trustAnchor must not be a Proxy");
|
|
1945
2211
|
}
|
|
@@ -1956,6 +2222,8 @@ function toAnchor(entry, verb) {
|
|
|
1956
2222
|
purposesSnap = _normalizeConstraintField(pRaw, E, "path/bad-input", who);
|
|
1957
2223
|
var dRaw = _captureConstraintMap(entry, "distrustAfter", E, "path/bad-input", who);
|
|
1958
2224
|
distrustSnap = _normalizeConstraintField(dRaw, E, "path/bad-input", who);
|
|
2225
|
+
var ncRaw = _captureConstraintMap(entry, "nameConstraints", E, "path/bad-input", who);
|
|
2226
|
+
ncSnap = ncRaw == null ? undefined : _copyAnchorNameConstraints(ncRaw, who);
|
|
1959
2227
|
subjectDerRaw = _captureOwnData(entry, "subjectDer");
|
|
1960
2228
|
labelRaw = _captureOwnData(entry, "label");
|
|
1961
2229
|
mozRaw = _captureOwnData(entry, "mozillaCaPolicy");
|
|
@@ -2023,6 +2291,7 @@ function toAnchor(entry, verb) {
|
|
|
2023
2291
|
intrinsic.defineProperty(flat, "name", { value: nameOut, enumerable: true, configurable: true, writable: true });
|
|
2024
2292
|
if (purposesSnap !== undefined) intrinsic.defineProperty(flat, "purposes", { value: purposesSnap, enumerable: true, configurable: true, writable: true });
|
|
2025
2293
|
if (distrustSnap !== undefined) intrinsic.defineProperty(flat, "distrustAfter", { value: distrustSnap, enumerable: true, configurable: true, writable: true });
|
|
2294
|
+
if (ncSnap !== undefined) intrinsic.defineProperty(flat, "nameConstraints", { value: ncSnap, enumerable: true, configurable: true, writable: true });
|
|
2026
2295
|
if (subjectDerRaw !== undefined) intrinsic.defineProperty(flat, "subjectDer", { value: subjectDerRaw, enumerable: true, configurable: true, writable: true });
|
|
2027
2296
|
if (labelRaw !== undefined) intrinsic.defineProperty(flat, "label", { value: labelRaw, enumerable: true, configurable: true, writable: true });
|
|
2028
2297
|
if (mozRaw !== undefined) intrinsic.defineProperty(flat, "mozillaCaPolicy", { value: mozRaw, enumerable: true, configurable: true, writable: true });
|
|
@@ -2031,8 +2300,8 @@ function toAnchor(entry, verb) {
|
|
|
2031
2300
|
intrinsic.defineProperty(flat, "parameters", { value: spkiAlg.parameters, enumerable: true, configurable: true, writable: true });
|
|
2032
2301
|
return flat;
|
|
2033
2302
|
}
|
|
2034
|
-
if (entry && typeof entry === "object" && (("purposes" in entry) || ("distrustAfter" in entry))) {
|
|
2035
|
-
throw E("path/bad-input", who + ": a trustAnchor certificate must not carry purposes / distrustAfter -- put
|
|
2303
|
+
if (entry && typeof entry === "object" && (("purposes" in entry) || ("distrustAfter" in entry) || ("nameConstraints" in entry))) {
|
|
2304
|
+
throw E("path/bad-input", who + ": a trustAnchor certificate must not carry purposes / distrustAfter / nameConstraints -- put anchor constraints on a { name, publicKey, algorithm, purposes, distrustAfter, nameConstraints } tuple (or a pki.trust anchor), not on a parsed certificate");
|
|
2036
2305
|
}
|
|
2037
2306
|
var cert;
|
|
2038
2307
|
try { cert = coerceCert(entry); }
|
|
@@ -2349,7 +2618,20 @@ async function build(leaf, opts) {
|
|
|
2349
2618
|
for (var pdk = 0; pdk < pool.length; pdk++) intrinsic.setAdd(poolDerKeys, certDerKey(pool[pdk]));
|
|
2350
2619
|
|
|
2351
2620
|
if (!intrinsic.isArray(opts.trustAnchors) || opts.trustAnchors.length === 0) throw E("path/bad-input", "build: opts.trustAnchors must be a non-empty array of anchor tuples or root certificates");
|
|
2352
|
-
|
|
2621
|
+
/** @internal Captured before any anchor is normalized: an anchor field may be an accessor whose
|
|
2622
|
+
* getter runs caller code, and every candidate is validated in its own pass after an await. */
|
|
2623
|
+
var buildPermRaw = opts.initialPermittedSubtrees;
|
|
2624
|
+
var buildExclRaw = opts.initialExcludedSubtrees;
|
|
2625
|
+
var buildPermSeeds = checkedSubtreeSeeds(buildPermRaw, "initialPermittedSubtrees");
|
|
2626
|
+
var buildExclSeeds = checkedSubtreeSeeds(buildExclRaw, "initialExcludedSubtrees");
|
|
2627
|
+
var buildEntries = _ownAnchorEntries(opts.trustAnchors, "build");
|
|
2628
|
+
var buildPreNc = _precaptureAnchorNameConstraints(buildEntries, "build");
|
|
2629
|
+
var anchors = [];
|
|
2630
|
+
for (var bai = 0; bai < buildEntries.length; bai++) {
|
|
2631
|
+
var bAnchor = toAnchor(buildEntries[bai], "build");
|
|
2632
|
+
if (buildPreNc[bai] !== undefined) intrinsic.defineProperty(bAnchor, "nameConstraints", { value: buildPreNc[bai], enumerable: true, configurable: true, writable: true });
|
|
2633
|
+
intrinsic.push(anchors, bAnchor);
|
|
2634
|
+
}
|
|
2353
2635
|
|
|
2354
2636
|
guard.time.assertValid(opts.time, E, "path/bad-input", "build: opts.time (the validity-window check date, forwarded to validate)");
|
|
2355
2637
|
var effectiveMaxCerts = guard.limits.cap(opts.maxPathCerts, "build: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
|
|
@@ -2396,6 +2678,8 @@ async function build(leaf, opts) {
|
|
|
2396
2678
|
if (intrinsic.hasOwn(BUILD_ONLY_OPT, k)) return;
|
|
2397
2679
|
if (k in opts) forwarded[k] = opts[k];
|
|
2398
2680
|
});
|
|
2681
|
+
if (buildPermRaw != null) forwarded.initialPermittedSubtrees = buildPermSeeds;
|
|
2682
|
+
if (buildExclRaw != null) forwarded.initialExcludedSubtrees = buildExclSeeds;
|
|
2399
2683
|
function validateOpts(anchor) {
|
|
2400
2684
|
var vo = Object.create(null);
|
|
2401
2685
|
Object.keys(forwarded).forEach(function (f) { vo[f] = forwarded[f]; });
|
package/lib/schema-c509.js
CHANGED
|
@@ -63,6 +63,11 @@ var PK_ALG_BY_INT = {
|
|
|
63
63
|
3: { alg: _name("ecPublicKey"), curve: _name("secp521r1") },
|
|
64
64
|
};
|
|
65
65
|
var EC_FIELD_BYTES = Object.assign(Object.create(null), { "prime256v1": 32, "secp384r1": 48, "secp521r1": 66 });
|
|
66
|
+
/** @internal Curve identity follows the dotted OID; pki.oid.register can rename one at any time. */
|
|
67
|
+
var EC_CURVE_BY_OID = Object.create(null);
|
|
68
|
+
Object.keys(EC_FIELD_BYTES).forEach(function (n) { EC_CURVE_BY_OID[oid.byName(n)] = n; });
|
|
69
|
+
var SIG_ALG_BY_OID = Object.create(null);
|
|
70
|
+
Object.keys(SIG_ALG_BY_INT).forEach(function (k) { SIG_ALG_BY_OID[oid.byName(SIG_ALG_BY_INT[k])] = SIG_ALG_BY_INT[k]; });
|
|
66
71
|
|
|
67
72
|
var ATTR_BY_INT = {
|
|
68
73
|
0: _name("emailAddress"),
|
|
@@ -1351,7 +1356,9 @@ function _sdaFromDer(node) {
|
|
|
1351
1356
|
var setNode = attr.children[1];
|
|
1352
1357
|
if (setNode.tagClass !== "universal" || setNode.tagNumber !== asn1.TAGS.SET || !setNode.children || setNode.children.length < 1) return null;
|
|
1353
1358
|
var dotted = asn1.read.oid(attr.children[0]);
|
|
1354
|
-
var
|
|
1359
|
+
var canonAttr = ATTR_NAME_BY_OID[dotted];
|
|
1360
|
+
var nm = canonAttr === undefined ? oid.name(dotted) : canonAttr;
|
|
1361
|
+
var tint = canonAttr === undefined ? undefined : ATTR_TO_INT[canonAttr];
|
|
1355
1362
|
var intForm = _sdaTryIntForm(tint, nm, setNode.children);
|
|
1356
1363
|
if (intForm != null) {
|
|
1357
1364
|
out.push(cbor.build.int(BigInt(intForm.sign < 0 ? -tint : tint)));
|
|
@@ -1364,12 +1371,24 @@ function _sdaFromDer(node) {
|
|
|
1364
1371
|
return cbor.build.array(out);
|
|
1365
1372
|
}
|
|
1366
1373
|
|
|
1367
|
-
|
|
1374
|
+
/**
|
|
1375
|
+
* @internal Draft sec. 3.7 for extensions. The specific form was available when the registry names
|
|
1376
|
+
* this extension AND the int form can carry this exact value: a compact-codec extension only when
|
|
1377
|
+
* the codec represents it, and one without a codec always, since its int form carries the same
|
|
1378
|
+
* bytes. An extension the codec cannot represent keeps the generic form on every certificate type.
|
|
1379
|
+
*/
|
|
1380
|
+
function _assertSpecificExtension(dotted, valContent, isNative) {
|
|
1381
|
+
var canon = EXT_NAME_BY_OID[dotted];
|
|
1382
|
+
if (!isNative || canon === undefined) return;
|
|
1383
|
+
if (EXT_COMPACT[canon] && (!Buffer.isBuffer(valContent) || _tryCompactExtValue(canon, valContent, isNative) == null)) return;
|
|
1384
|
+
throw _err("c509/non-specific-encoding", "extension " + canon + " has a C509 registry id that carries this value, and a natively signed certificate uses only the specific CBOR encoding (draft sec. 3.7)");
|
|
1385
|
+
}
|
|
1386
|
+
function _tryCompactExtValue(name, der, isNative) {
|
|
1368
1387
|
var compact = _extValueFromDer(name, der);
|
|
1369
1388
|
if (compact == null) return null;
|
|
1370
1389
|
var recon;
|
|
1371
1390
|
try {
|
|
1372
|
-
recon = _extValueToDer(name, cbor.decode(compact));
|
|
1391
|
+
recon = _extValueToDer(name, cbor.decode(compact), isNative);
|
|
1373
1392
|
} catch (_e) {
|
|
1374
1393
|
return null;
|
|
1375
1394
|
}
|
|
@@ -1418,6 +1437,7 @@ function _extensions(node, isNative) {
|
|
|
1418
1437
|
if (valNode.majorType !== 2) throw _err("c509/bad-extensions", "a non-critical ~oid extension value must be a byte string");
|
|
1419
1438
|
valContent = valNode.content;
|
|
1420
1439
|
}
|
|
1440
|
+
_assertSpecificExtension(extOid, valContent, isNative);
|
|
1421
1441
|
}
|
|
1422
1442
|
out.push({ name: name, oid: extOid, critical: critical, value: valContent || null });
|
|
1423
1443
|
}
|
|
@@ -1637,8 +1657,14 @@ function matches(node) {
|
|
|
1637
1657
|
|
|
1638
1658
|
var ATTR_TO_INT = Object.create(null);
|
|
1639
1659
|
Object.keys(ATTR_BY_INT).forEach(function (k) { ATTR_TO_INT[ATTR_BY_INT[k]] = Number(k); });
|
|
1660
|
+
/** @internal An attribute's C509 registry membership follows its dotted OID, not its display name. */
|
|
1661
|
+
var ATTR_NAME_BY_OID = Object.create(null);
|
|
1662
|
+
Object.keys(ATTR_BY_INT).forEach(function (k) { ATTR_NAME_BY_OID[oid.byName(ATTR_BY_INT[k])] = ATTR_BY_INT[k]; });
|
|
1640
1663
|
var EXT_TO_INT = Object.create(null);
|
|
1641
1664
|
Object.keys(EXT_BY_INT).forEach(function (k) { if (EXT_COMPACT[EXT_BY_INT[k]]) EXT_TO_INT[EXT_BY_INT[k]] = Number(k); });
|
|
1665
|
+
/** @internal An extension's C509 registry membership follows its dotted OID, not its display name. */
|
|
1666
|
+
var EXT_NAME_BY_OID = Object.create(null);
|
|
1667
|
+
Object.keys(EXT_BY_INT).forEach(function (k) { EXT_NAME_BY_OID[oid.byName(EXT_BY_INT[k])] = EXT_BY_INT[k]; });
|
|
1642
1668
|
|
|
1643
1669
|
function _minBytes(n) {
|
|
1644
1670
|
if (typeof n !== "bigint") throw _err("c509/bad-serial", "a ~biguint value must be a BigInt, got " + _describe(n));
|
|
@@ -1696,7 +1722,24 @@ function _encSpk(r) {
|
|
|
1696
1722
|
if (!Buffer.isBuffer(r.subjectPublicKey)) throw _err("c509/bad-spki", "the subjectPublicKey bytes are missing");
|
|
1697
1723
|
return cbor.build.byteString(r.subjectPublicKey);
|
|
1698
1724
|
}
|
|
1699
|
-
|
|
1725
|
+
/** @internal An extension names itself by name, by OID, or by both; the registry is keyed by name. */
|
|
1726
|
+
function _extIdentity(ext) {
|
|
1727
|
+
var dotted = ext.oid || (typeof ext.name === "string" ? oid.byName(ext.name) : null);
|
|
1728
|
+
var canon = dotted ? EXT_NAME_BY_OID[dotted] : undefined;
|
|
1729
|
+
if (canon !== undefined) return { dotted: dotted, name: canon };
|
|
1730
|
+
return { dotted: dotted, name: typeof ext.name === "string" ? ext.name : (dotted ? oid.name(dotted) : null) };
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* @internal The bits a lone keyUsage is written as. Derived from a DER value only when the compact
|
|
1734
|
+
* form reproduces that value byte for byte, since the bare integer covers the nine defined bits and
|
|
1735
|
+
* a wider BIT STRING would lose the rest.
|
|
1736
|
+
*/
|
|
1737
|
+
function _extKeyUsageBits(ext, isNative) {
|
|
1738
|
+
if (typeof ext.keyUsageBits === "number") return ext.keyUsageBits;
|
|
1739
|
+
if (!Buffer.isBuffer(ext.value) || _tryCompactExtValue("keyUsage", ext.value, isNative) == null) return null;
|
|
1740
|
+
return _keyUsageBitsFromDer(ext.value);
|
|
1741
|
+
}
|
|
1742
|
+
function _encExtensions(exts, isNative) {
|
|
1700
1743
|
_assertDense(exts, "c509/bad-input", "the extensions array");
|
|
1701
1744
|
exts.forEach(function (ext) {
|
|
1702
1745
|
if (!ext || typeof ext !== "object") throw _err("c509/bad-input", "each extension must be an object, got " + _describe(ext));
|
|
@@ -1709,18 +1752,20 @@ function _encExtensions(exts) {
|
|
|
1709
1752
|
throw _err("c509/bad-extensions", "keyUsageBits must be a safe integer, got " + _describe(ext.keyUsageBits));
|
|
1710
1753
|
}
|
|
1711
1754
|
});
|
|
1712
|
-
if (exts.length === 1 && exts[0].name === "keyUsage"
|
|
1713
|
-
|
|
1755
|
+
if (exts.length === 1 && _extIdentity(exts[0]).name === "keyUsage") {
|
|
1756
|
+
var onlyBits = _extKeyUsageBits(exts[0], isNative);
|
|
1757
|
+
if (typeof onlyBits === "number") return cbor.build.int(BigInt(exts[0].critical ? -onlyBits : onlyBits));
|
|
1714
1758
|
}
|
|
1715
1759
|
var items = [];
|
|
1716
1760
|
exts.forEach(function (ext) {
|
|
1717
|
-
var
|
|
1718
|
-
var
|
|
1761
|
+
var ident = _extIdentity(ext);
|
|
1762
|
+
var extDotted = ident.dotted, extName = ident.name;
|
|
1763
|
+
var ei = extName == null ? undefined : EXT_TO_INT[extName];
|
|
1764
|
+
var compact = (ei !== undefined && Buffer.isBuffer(ext.value)) ? _tryCompactExtValue(extName, ext.value, isNative) : null;
|
|
1719
1765
|
if (compact != null) {
|
|
1720
1766
|
items.push(cbor.build.int(BigInt(ext.critical ? -ei : ei)));
|
|
1721
1767
|
items.push(compact);
|
|
1722
1768
|
} else {
|
|
1723
|
-
var extDotted = ext.oid || (typeof ext.name === "string" ? oid.byName(ext.name) : null);
|
|
1724
1769
|
if (!extDotted) throw _err("c509/bad-input", "an extension must carry an OID or a name in the OID registry, got " + _describe(ext.name));
|
|
1725
1770
|
items.push(cbor.build.byteString(asn1.encodeOidContent(extDotted)));
|
|
1726
1771
|
if (!Buffer.isBuffer(ext.value)) throw _err("c509/non-invertible", "extension " + (ext.oid || ext.name) + " has no byte-string value to encode");
|
|
@@ -1764,7 +1809,7 @@ function _encodeC509Array(r) {
|
|
|
1764
1809
|
_encName(r.subject, true),
|
|
1765
1810
|
_encAlgorithm(r.subjectPublicKeyAlgorithm, PK_ALG_BY_INT),
|
|
1766
1811
|
_encSpk(r),
|
|
1767
|
-
_encExtensions(r.extensions),
|
|
1812
|
+
_encExtensions(r.extensions, Number(r.certificateType) === 2),
|
|
1768
1813
|
cbor.build.byteString(r.signatureValue),
|
|
1769
1814
|
]);
|
|
1770
1815
|
parse(arr);
|
|
@@ -1828,7 +1873,9 @@ function _c509NameFromDer(nameBytes) {
|
|
|
1828
1873
|
(node.children || []).forEach(function (rdnSet) {
|
|
1829
1874
|
if (!rdnSet.children || rdnSet.children.length !== 1) throw _err("c509/non-invertible", "a C509 Name requires single-attribute RDNs");
|
|
1830
1875
|
var attr = rdnSet.children[0];
|
|
1831
|
-
var
|
|
1876
|
+
var attrDotted = asn1.read.oid(attr.children[0]);
|
|
1877
|
+
var attrCanon = ATTR_NAME_BY_OID[attrDotted];
|
|
1878
|
+
var attrName = attrCanon === undefined ? oid.name(attrDotted) : attrCanon;
|
|
1832
1879
|
if (attrName == null || ATTR_TO_INT[attrName] === undefined) throw _err("c509/non-invertible", "attribute type " + attrName + " has no C509 registry integer");
|
|
1833
1880
|
var valNode = attr.children[1];
|
|
1834
1881
|
var value;
|
|
@@ -1875,19 +1922,19 @@ function _minCurveForMag(mag) {
|
|
|
1875
1922
|
for (var i = 0; i < CURVE_ORDER.length; i++) { if (mag <= WEBCRYPTO_FIELD_BYTES[CURVE_ORDER[i]]) return CURVE_ORDER[i]; }
|
|
1876
1923
|
return null;
|
|
1877
1924
|
}
|
|
1878
|
-
function _resolveIssuerSigCurve(c, opts) {
|
|
1925
|
+
function _resolveIssuerSigCurve(c, opts, sigAlgName) {
|
|
1879
1926
|
var mag = _sigMagWidth(c.signatureValue.bytes), curve;
|
|
1880
1927
|
if (opts && opts.issuerCurve != null) {
|
|
1881
1928
|
curve = String(opts.issuerCurve);
|
|
1882
1929
|
if (NODE_TO_WEBCRYPTO[curve]) curve = NODE_TO_WEBCRYPTO[curve];
|
|
1883
1930
|
if (!WEBCRYPTO_FIELD_BYTES[curve]) throw _err("c509/bad-input", "opts.issuerCurve must be P-256 / P-384 / P-521 (or prime256v1 / secp384r1 / secp521r1); got " + opts.issuerCurve);
|
|
1884
1931
|
} else {
|
|
1885
|
-
curve = SIG_ALG_TO_CURVE[
|
|
1886
|
-
if (!curve) throw _err("c509/non-invertible", "cannot resolve the issuer signing curve for signature algorithm " +
|
|
1932
|
+
curve = SIG_ALG_TO_CURVE[sigAlgName];
|
|
1933
|
+
if (!curve) throw _err("c509/non-invertible", "cannot resolve the issuer signing curve for signature algorithm " + sigAlgName);
|
|
1887
1934
|
var minCurve = _minCurveForMag(mag);
|
|
1888
1935
|
if (!minCurve) throw _err("c509/non-invertible", "the ECDSA signature r/s width " + mag + " exceeds every supported curve field");
|
|
1889
|
-
if (WEBCRYPTO_FIELD_BYTES[curve] < mag) throw _err("c509/non-invertible", "the issuer signed with a non-standard digest/curve pairing (r/s width " + mag + " exceeds the " + curve + " field implied by " +
|
|
1890
|
-
if (WEBCRYPTO_FIELD_BYTES[curve] > WEBCRYPTO_FIELD_BYTES[minCurve]) throw _err("c509/non-invertible", "signature algorithm " +
|
|
1936
|
+
if (WEBCRYPTO_FIELD_BYTES[curve] < mag) throw _err("c509/non-invertible", "the issuer signed with a non-standard digest/curve pairing (r/s width " + mag + " exceeds the " + curve + " field implied by " + sigAlgName + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
|
|
1937
|
+
if (WEBCRYPTO_FIELD_BYTES[curve] > WEBCRYPTO_FIELD_BYTES[minCurve]) throw _err("c509/non-invertible", "signature algorithm " + sigAlgName + " does not uniquely determine the issuer curve (r/s width " + mag + " also fits " + minCurve + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
|
|
1891
1938
|
}
|
|
1892
1939
|
if (mag > WEBCRYPTO_FIELD_BYTES[curve]) throw _err("c509/non-invertible", "the ECDSA signature r/s width " + mag + " does not fit the resolved " + curve + " field");
|
|
1893
1940
|
return curve;
|
|
@@ -1902,13 +1949,16 @@ _derToType3 = function (input, opts) {
|
|
|
1902
1949
|
var c;
|
|
1903
1950
|
try { c = x509.parse(input); } catch (e) { throw _err("c509/bad-input", "the input is not a valid X.509 certificate", e); }
|
|
1904
1951
|
if (c.version !== 3) throw _err("c509/non-invertible", "C509 covers X.509 v3 certificates; got v" + c.version);
|
|
1905
|
-
|
|
1906
|
-
|
|
1952
|
+
var sigCanon = SIG_ALG_BY_OID[c.signatureAlgorithm.oid];
|
|
1953
|
+
var sigAlgName = sigCanon === undefined ? c.signatureAlgorithm.name : sigCanon;
|
|
1954
|
+
if (String(sigAlgName || "").toLowerCase().indexOf("ecdsa") !== 0) throw _err("c509/non-invertible", "type-3 C509 encoding covers only ECDSA-signed certificates; got " + (sigAlgName || "an unregistered algorithm"));
|
|
1955
|
+
if (c.subjectPublicKeyInfo.algorithm.oid !== oid.byName("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"));
|
|
1907
1956
|
var curveOid = asn1.read.oid(asn1.decode(c.subjectPublicKeyInfo.algorithm.parameters));
|
|
1908
|
-
var
|
|
1957
|
+
var curveCanon = EC_CURVE_BY_OID[curveOid];
|
|
1958
|
+
var curve = curveCanon === undefined ? oid.name(curveOid) : curveCanon;
|
|
1909
1959
|
var coordLen = EC_FIELD_BYTES[curve];
|
|
1910
1960
|
if (!coordLen) throw _err("c509/non-invertible", "unsupported EC subject curve " + (curve || curveOid));
|
|
1911
|
-
var sigCurve = _resolveIssuerSigCurve(c, opts);
|
|
1961
|
+
var sigCurve = _resolveIssuerSigCurve(c, opts, sigAlgName);
|
|
1912
1962
|
var spkiNode = asn1.decode(c.subjectPublicKeyInfo.bytes);
|
|
1913
1963
|
return {
|
|
1914
1964
|
certificateType: 3,
|
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:adf5e979-6804-475f-a0d0-8aef04be0317",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-05T07:57:13.454Z",
|
|
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.6.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.6.38",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.6.
|
|
25
|
+
"version": "0.6.38",
|
|
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.6.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.6.38",
|
|
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.6.
|
|
57
|
+
"ref": "@blamejs/pki@0.6.38",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|