@blamejs/pki 0.4.3 → 0.4.4
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 +14 -0
- package/README.md +1 -1
- package/lib/oid.js +5 -1
- package/lib/schema-c509.js +484 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ 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.4.4 — 2026-08-08
|
|
8
|
+
|
|
9
|
+
pki.schema.c509 encodes and decodes the RFC 3779 resource-delegation extensions -- a C509 certificate carrying IP address blocks or AS identifiers now parses at all, where before it was refused outright, and its addresses ride the compact form the specification defines.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.c509 encodes and decodes the RFC 3779 IPAddrBlocks and ASIdentifiers extensions in their compact value form, together with their RFC 8360 v2 twins, which the specification encodes identically. Previously these extensions had no registry entry, so a conformant C509 certificate carrying one was refused rather than falling back -- a C509 resource certificate could not be read at all. An address family carries its address-family identifier and optional sub-identifier, and its addresses as either the delta-coded integer form or the byte-string form; the prefix length rides the unused-bit count, so a prefix ending in zero bits survives exactly. Both directions reproduce the worked example published in the specification's own appendix, byte for byte.
|
|
14
|
+
- Which address form applies is fixed by the specification, not chosen by the sender: the byte-string form applies to a whole address family as soon as any one of its addresses exceeds eight octets, and the integer form applies otherwise. The decoder enforces that, so a family that used the wrong form -- or mixed the two -- is refused instead of giving one certificate two valid encodings.
|
|
15
|
+
- The compact form is used only for a certificate already in the canonical order RFC 3779 requires, at both levels. Within an address family the entries must be sorted, non-overlapping, and with any two contiguous entries already combined into one; the same three rules apply to AS identifiers. Across families, each address family may appear only once and they must ascend by their identifying octets, with a family carrying no sub-identifier preceding the one sharing its identifier. A certificate breaking any of these keeps its original bytes, because compacting it would give one resource set a second encoding when it already has a canonical one -- and because such a certificate is one an independent validator rejects, so re-encoding it would quietly turn a refused certificate into an accepted one. An address wider than its family allows, or one whose declared unused bits are not zero, is likewise refused. Every rule is enforced in both directions, so the two halves of the codec accept exactly the same certificates.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- A certificate whose version is not v3 is now refused with the reason, rather than reported as one this encoder could not reconstruct. Both C509 certificate types are defined over X.509 v3 and the encoding carries no version field, so a v1 or v2 certificate is outside the format; it previously fell through to the byte-exactness self-check, whose verdict reads as a defect in the encoder rather than a certificate the format does not cover. A v3 certificate whose extensions field is omitted was and remains fully supported -- the specification encodes that as an empty array.
|
|
20
|
+
|
|
7
21
|
## v0.4.3 — 2026-08-08
|
|
8
22
|
|
|
9
23
|
pki.tls encodes and decodes RFC 8879 compressed certificate messages -- the largest payload a TLS handshake carries, and the one post-quantum chains grow by kilobytes -- with the two-sided decompression bound the specification requires. Alongside it, SHAKE128 and SHAKE256 join the digest surface, which brings the Ed448 composite signature arm into service.
|
package/README.md
CHANGED
|
@@ -205,7 +205,7 @@ is callable today; nothing below is a stub.
|
|
|
205
205
|
| `pki.tls` | RFC 8879 TLS certificate compression — `decompressCertificate` / `compressCertificate` decode and build a `CompressedCertificate` (zlib / brotli / zstd, each offered only where the runtime decompresses it safely) with the two-sided bound RFC 8879 §5 requires (capped at the message's own declared length, then compared to it exactly), and `parseCertificateMessage` decodes the RFC 8446 §4.4.2 Certificate message to per-entry certificate DER |
|
|
206
206
|
| `pki.schema` | The schema family — `parse` detects which PKI format DER / PEM encodes and routes to the right parser, `all` enumerates the registered formats, and the engine + per-format members are grouped here |
|
|
207
207
|
| `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields, with named + partly-decoded extensions — including the RFC 3739 / ETSI EN 319 412-5 qualified-certificate `qcStatements` (EU-qualified declaration, reliance limit, QSCD flag, certificate type, retention, PDS URLs, country of qualification; unknown statements preserved opaque) and the Microsoft Active Directory Certificate Services enrollment extensions (certificate template, CA version, previous-CA-certificate hash, application policies), fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
208
|
-
| `pki.schema.c509` | Parse **and encode** C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed). `encode(input)` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte (so the original signature still verifies), or a `parse` result re-emits its native array — canonical deterministic CBOR with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms — the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, and more), the general-name-bearing extensions (subjectAltName, issuer alt name, name constraints, CRL distribution points, authority/subject information access, and the full authority key identifier) over one shared GeneralNames value codec, certificate policies (registry-integer or OID policy identifiers with their CPS-URI and UserNotice qualifiers),
|
|
208
|
+
| `pki.schema.c509` | Parse **and encode** C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed). `encode(input)` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte (so the original signature still verifies), or a `parse` result re-emits its native array — canonical deterministic CBOR with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms — the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, and more), the general-name-bearing extensions (subjectAltName, issuer alt name, name constraints, CRL distribution points, authority/subject information access, and the full authority key identifier) over one shared GeneralNames value codec, certificate policies (registry-integer or OID policy identifiers with their CPS-URI and UserNotice qualifiers), policy mappings and policy constraints, subject directory attributes, and the RFC 3779 resource-delegation extensions (IP address blocks and AS identifiers, plus their RFC 8360 v2 twins) whose addresses ride either the delta-coded integer form or the byte-string form the specification mandates once an address exceeds eight octets; a value the compact form cannot carry exactly falls back to the byte-string form with its bytes intact, and a certificate outside the invertible set throws a typed `C509Error` |
|
|
209
209
|
| `pki.schema.crl` | Parse DER / PEM X.509 CRLs per RFC 5280 §5 — revoked serials with real-`Date` revocation times, named + partly-decoded extensions, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
210
210
|
| `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
211
211
|
| `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
|
package/lib/oid.js
CHANGED
|
@@ -87,7 +87,11 @@ var FAMILIES = {
|
|
|
87
87
|
// id-pe-tlsfeature (RFC 7633) -- the TLS Feature (formerly "must-staple") extension.
|
|
88
88
|
tlsFeature: 24,
|
|
89
89
|
// id-pe-qcStatements (RFC 3739 sec. 3.2.6) -- the qualified-certificate-statements extension.
|
|
90
|
-
qcStatements: 3
|
|
90
|
+
qcStatements: 3,
|
|
91
|
+
// RFC 3779 -- the IP-address and AS-number delegation extensions that carry a resource
|
|
92
|
+
// certificate's authority (RPKI). Each has an RFC 8360 "v2" twin with identical syntax and
|
|
93
|
+
// stricter path-validation semantics, so both are registered here.
|
|
94
|
+
ipAddrBlocks: 7, autonomousSysIds: 8, ipAddrBlocksV2: 28, autonomousSysIdsV2: 29 } },
|
|
91
95
|
|
|
92
96
|
// RFC 3739 sec. 3.2.6.1 id-qcs -- the PKIX QCStatement statementIds carrying SemanticsInformation.
|
|
93
97
|
pkixQcSyntax: { base: [1, 3, 6, 1, 5, 5, 7, 11], of: { qcsPkixQCSyntaxV1: 1, qcsPkixQCSyntaxV2: 2 } },
|
package/lib/schema-c509.js
CHANGED
|
@@ -97,6 +97,12 @@ var EXT_BY_INT = {
|
|
|
97
97
|
29: _name("freshestCRL"),
|
|
98
98
|
30: _name("inhibitAnyPolicy"),
|
|
99
99
|
31: _name("subjectInfoAccess"),
|
|
100
|
+
// RFC 3779 resource-delegation extensions, and their RFC 8360 "v2" twins, which the draft
|
|
101
|
+
// encodes "exactly like" the originals -- same two codecs, four registry rows (sec. 8.8).
|
|
102
|
+
32: _name("ipAddrBlocks"),
|
|
103
|
+
33: _name("autonomousSysIds"),
|
|
104
|
+
34: _name("ipAddrBlocksV2"),
|
|
105
|
+
35: _name("autonomousSysIdsV2"),
|
|
100
106
|
36: _name("ocspNoCheck"),
|
|
101
107
|
38: _name("tlsFeature"),
|
|
102
108
|
};
|
|
@@ -108,6 +114,7 @@ var EXT_COMPACT = {
|
|
|
108
114
|
subjectAltName: 1, issuerAltName: 1, nameConstraints: 1, cRLDistributionPoints: 1,
|
|
109
115
|
freshestCRL: 1, authorityInfoAccess: 1, subjectInfoAccess: 1, certificatePolicies: 1,
|
|
110
116
|
policyMappings: 1, policyConstraints: 1, subjectDirectoryAttributes: 1,
|
|
117
|
+
ipAddrBlocks: 1, autonomousSysIds: 1, ipAddrBlocksV2: 1, autonomousSysIdsV2: 1,
|
|
111
118
|
};
|
|
112
119
|
// sec. 8.12 Extended Key Usages registry (C509 int -> registered id-kp purpose name). A KeyPurposeId
|
|
113
120
|
// outside this set encodes as an unwrapped ~oid; a C509 int outside it fails closed on decode.
|
|
@@ -555,6 +562,332 @@ function _ncIpFromDer(buf) {
|
|
|
555
562
|
return Buffer.concat([buf.subarray(0, addrLen), Buffer.from([prefixLen])]);
|
|
556
563
|
}
|
|
557
564
|
|
|
565
|
+
// ---- RFC 3779 IPAddrBlocks / ASIdentifiers (draft sec. 3.3, ext ints 32-35) -----------------
|
|
566
|
+
//
|
|
567
|
+
// An RFC 3779 IPAddress is a BIT STRING whose unused-bit count carries the prefix length, so the
|
|
568
|
+
// draft maps it to the byte sequence `unusedBits || value`, which "preserves the exact information
|
|
569
|
+
// contained in the ASN.1 BIT STRING" -- lossless even for a prefix ending in zero bits. Per
|
|
570
|
+
// IPAddressFamily the draft then picks ONE of two forms and makes the choice a SHALL: if any of the
|
|
571
|
+
// family's byte sequences exceeds 8 octets the whole family uses the bytes form, otherwise the int
|
|
572
|
+
// form. Accepting the wrong one would give one DER two CBOR encodings, so decode enforces it.
|
|
573
|
+
//
|
|
574
|
+
// The int form is the big-endian integer of `(unusedBits + 1) || value` -- the +1 guarantees a
|
|
575
|
+
// non-zero leading octet, which is what makes the minimal big-endian representation unambiguous --
|
|
576
|
+
// and every IPAddress after the first is stored as the DIFFERENCE from its predecessor. The chain
|
|
577
|
+
// runs flat over each address in order (a range contributes min then max) and RESETS at each
|
|
578
|
+
// family, so a family always opens with an absolute value.
|
|
579
|
+
//
|
|
580
|
+
// These integers reach 2^64-1: an IPv6 /48 already exceeds 2^53, and the draft's own A.5 vector
|
|
581
|
+
// lands there. Everything below is BigInt end to end and bounds through guard.range.uint64 (the
|
|
582
|
+
// BigInt-preserving guard) -- never the uint31 counter bound, which would reject a valid prefix.
|
|
583
|
+
|
|
584
|
+
// An asn1 node exposes tagClass + tagNumber (never a combined `.tag`), so a universal-type test
|
|
585
|
+
// has to check both -- a context-tagged [16] must not read as a SEQUENCE.
|
|
586
|
+
function _isUniversal(n, tagNumber) { return !!n && n.tagClass === "universal" && n.tagNumber === tagNumber; }
|
|
587
|
+
|
|
588
|
+
// `unusedBits || value` -> the (unusedBits+1)||value integer. Returns null if the sequence cannot
|
|
589
|
+
// be one (empty, or an unused-bit count DER would not accept).
|
|
590
|
+
function _ipSeqToInt(seq) {
|
|
591
|
+
if (!seq.length || seq[0] > 7) return null;
|
|
592
|
+
// One-shot base-256 parse of (unusedBits+1)||value. A per-byte shift-accumulate is quadratic in
|
|
593
|
+
// the operand width, so the toolkit builds a BigInt from its hex in a single call instead.
|
|
594
|
+
var head = Buffer.from([seq[0] + 1]);
|
|
595
|
+
return BigInt("0x" + Buffer.concat([head, seq.subarray(1)]).toString("hex"));
|
|
596
|
+
}
|
|
597
|
+
// The inverse: the minimal big-endian octets of `n`, with the leading octet decremented back to the
|
|
598
|
+
// unused-bit count. Returns null when `n` cannot be an encoded IPAddress -- a leading octet outside
|
|
599
|
+
// 1..8 is not a DER unused-bit count, and past 8 octets the family was required to use the bytes form.
|
|
600
|
+
function _ipIntToSeq(n) {
|
|
601
|
+
if (n < 1n) return null;
|
|
602
|
+
var out = [];
|
|
603
|
+
for (var v = n; v > 0n; v >>= 8n) out.unshift(Number(v & 0xffn));
|
|
604
|
+
if (out.length > 9 || out[0] < 1 || out[0] > 8) return null;
|
|
605
|
+
out[0] -= 1;
|
|
606
|
+
return Buffer.from(out);
|
|
607
|
+
}
|
|
608
|
+
// The address width in octets of an address family, from its AFI (RFC 3779 sec. 2.2.3.3). Only a
|
|
609
|
+
// family whose width is known can have its canonical form checked, so an unrecognized AFI yields
|
|
610
|
+
// null and the caller declines to compact rather than compacting something it cannot verify.
|
|
611
|
+
var _IP_WIDTH = { 1: 4, 2: 16 };
|
|
612
|
+
|
|
613
|
+
// The lowest address an `unusedBits || value` sequence denotes, as `width` big-endian octets. DER
|
|
614
|
+
// forces the unused bits to zero, so the value IS its own lowest address once zero-extended.
|
|
615
|
+
function _ipLow(seq, width) {
|
|
616
|
+
var v = Buffer.alloc(width);
|
|
617
|
+
seq.subarray(1).copy(v);
|
|
618
|
+
return v;
|
|
619
|
+
}
|
|
620
|
+
// The highest address it denotes: the same prefix with every host bit set (RFC 3779 sec. 2.2.3.8 --
|
|
621
|
+
// a range's max is likewise the prefix with its trailing bits taken as ones).
|
|
622
|
+
function _ipHigh(seq, width) {
|
|
623
|
+
var v = _ipLow(seq, width);
|
|
624
|
+
var bits = (seq.length - 1) * 8 - seq[0];
|
|
625
|
+
for (var i = bits; i < width * 8; i++) v[i >> 3] |= 0x80 >> (i & 7);
|
|
626
|
+
return v;
|
|
627
|
+
}
|
|
628
|
+
// Big-endian octet-string compare, and "is `b` the immediate successor of `a`" -- the test that
|
|
629
|
+
// distinguishes a legal gap from a contiguous pair the RFC requires be merged.
|
|
630
|
+
function _ipOctCmp(a, b) {
|
|
631
|
+
for (var i = 0; i < a.length; i++) { if (a[i] !== b[i]) return a[i] - b[i]; }
|
|
632
|
+
return 0;
|
|
633
|
+
}
|
|
634
|
+
function _ipIsSuccessor(a, b) {
|
|
635
|
+
var carry = 1, inc = Buffer.from(a);
|
|
636
|
+
for (var i = inc.length - 1; i >= 0 && carry; i--) { var s = inc[i] + carry; inc[i] = s & 0xff; carry = s >> 8; }
|
|
637
|
+
if (carry) return false; // `a` was already the maximum address
|
|
638
|
+
return _ipOctCmp(inc, b) === 0;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// RFC 3779 sec. 2.2.3.7: "any range of addresses that can be encoded as a prefix MUST be encoded
|
|
642
|
+
// using an IPAddress element", with the choice fixed by the spec's own pseudocode -- let N be the
|
|
643
|
+
// count of matching leading bits of the low and high addresses; if every remaining bit of the low
|
|
644
|
+
// is zero AND every remaining bit of the high is one, the span IS the N-bit prefix and the range
|
|
645
|
+
// form is forbidden. Two encodings of one address span would otherwise both be legal.
|
|
646
|
+
function _ipRangeIsPrefix(lo, hi) {
|
|
647
|
+
var bits = lo.length * 8, n = 0;
|
|
648
|
+
while (n < bits) {
|
|
649
|
+
var byteAt = n >> 3, mask = 0x80 >> (n & 7);
|
|
650
|
+
if ((lo[byteAt] & mask) !== (hi[byteAt] & mask)) break;
|
|
651
|
+
n++;
|
|
652
|
+
}
|
|
653
|
+
for (var i = n; i < bits; i++) {
|
|
654
|
+
var bt = i >> 3, mk = 0x80 >> (i & 7);
|
|
655
|
+
if ((lo[bt] & mk) !== 0) return false; // a low-address host bit is set
|
|
656
|
+
if ((hi[bt] & mk) === 0) return false; // a high-address host bit is clear
|
|
657
|
+
}
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// RFC 3779 sec. 2.2.3.3 orders the families themselves: "There MUST be only one IPAddressFamily
|
|
662
|
+
// SEQUENCE per unique combination of AFI and SAFI. Each SEQUENCE MUST be ordered by ascending
|
|
663
|
+
// addressFamily values (treating the octets as unsigned quantities). An addressFamily without a
|
|
664
|
+
// SAFI MUST precede one that contains an SAFI." A plain unsigned octet-string compare gives all
|
|
665
|
+
// three at once, because a two-octet family is a PREFIX of the three-octet one sharing its AFI and
|
|
666
|
+
// a prefix sorts first. Returns < 0, 0 or > 0; 0 means the same AFI/SAFI appeared twice.
|
|
667
|
+
function _famOctCmp(a, b) {
|
|
668
|
+
var n = Math.min(a.length, b.length);
|
|
669
|
+
for (var i = 0; i < n; i++) { if (a[i] !== b[i]) return a[i] - b[i]; }
|
|
670
|
+
return a.length - b.length;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Is `unusedBits || value` a sequence DER would accept as a BIT STRING? The declared unused low
|
|
674
|
+
// bits MUST be zero (RFC 3779 sec. 2.2.3.8 restates the DER rule), and a value with no octets can
|
|
675
|
+
// only declare zero unused bits. Checked HERE rather than left to the BIT STRING builder, because
|
|
676
|
+
// the builder's fault is an asn1/* error surfacing out of a CBOR-layer decode -- a caller handed a
|
|
677
|
+
// malformed compact value should see this module's own verdict, not the ASN.1 layer's.
|
|
678
|
+
function _ipSeqBitsClear(seq) {
|
|
679
|
+
var unused = seq[0], value = seq.subarray(1);
|
|
680
|
+
if (!value.length) return unused === 0;
|
|
681
|
+
if (unused === 0) return true;
|
|
682
|
+
return (value[value.length - 1] & ((1 << unused) - 1)) === 0;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// RFC 3779 sec. 2.2.3.6 fixes the canonical form of an address list: entries sorted on
|
|
686
|
+
// `<lowest address> | <prefix length>` (which is neither the DER byte order nor the compact integer
|
|
687
|
+
// order -- the RFC warns about the first and the second sorts the same wrong way), no pair
|
|
688
|
+
// overlapping, and any contiguous pair combined into one entry. All three bind together: a list
|
|
689
|
+
// violating any of them is not the one canonical encoding of its address set, so this codec
|
|
690
|
+
// declines to compact it and the extension keeps its original bytes.
|
|
691
|
+
// Two comparisons carry all three rules. `cur.lo > prev.hi` is the no-overlap rule AND the sort
|
|
692
|
+
// rule at once: entries are already known to have lo <= hi, so an entry that started at or before
|
|
693
|
+
// its predecessor's low address would also start at or before its high one. A separate ascending
|
|
694
|
+
// test would therefore be a branch nothing can reach.
|
|
695
|
+
function _ipRangesCanonical(bounds) {
|
|
696
|
+
for (var i = 1; i < bounds.length; i++) {
|
|
697
|
+
var prev = bounds[i - 1], cur = bounds[i];
|
|
698
|
+
if (_ipOctCmp(cur.lo, prev.hi) <= 0) return false; // out of order, or overlapping
|
|
699
|
+
if (_ipIsSuccessor(prev.hi, cur.lo)) return false; // contiguous: MUST have been merged
|
|
700
|
+
}
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// One family's IntIPAddressChoice / IPAddressChoice -> the DER IPAddressOrRange SEQUENCE list.
|
|
705
|
+
// The int form stores each address after the first as a DIFFERENCE from its predecessor, flat over
|
|
706
|
+
// every address in order (a range contributes min then max); the chain resets at each family, which
|
|
707
|
+
// is why `prev` starts null here rather than being threaded across families. The reconstructed
|
|
708
|
+
// ABSOLUTE is what gets bounded -- the delta itself is an arbitrary CBOR int and bounding it would
|
|
709
|
+
// miss a chain that walks out of range in steps.
|
|
710
|
+
function _ipChoiceToDer(items, afi) {
|
|
711
|
+
var out = [], prev = null, sawBytes = false, sawInt = false, seqs = [];
|
|
712
|
+
// An address may not be wider than its family: RFC 3779 sec. 2.2.3.8 sizes an IPAddress by the
|
|
713
|
+
// family, so a 5-octet address under AFI 1 is not an IPv4 address at all. Without this, a native
|
|
714
|
+
// C509 would reconstruct into a DER carrying an over-wide address -- one the RFC forbids and an
|
|
715
|
+
// independent validator refuses -- from CBOR this codec had accepted. The mirror check lives on
|
|
716
|
+
// the encode side; both directions have to hold or the pair is not a bijection.
|
|
717
|
+
// Without the family's address width none of the RFC 3779 rules below can be evaluated: the
|
|
718
|
+
// width bound has nothing to compare against, and the low/high bounds that drive the order,
|
|
719
|
+
// overlap, adjacency and endpoint checks cannot be computed at all. Accepting such a family
|
|
720
|
+
// would therefore wave every one of those checks through -- so a family this codec cannot
|
|
721
|
+
// measure is refused outright, matching the encode side, which declines to compact it. An
|
|
722
|
+
// `inherit` family is unaffected: it carries no addresses and never reaches here.
|
|
723
|
+
var width = _IP_WIDTH[afi];
|
|
724
|
+
if (!width) throw _err("c509/bad-extensions", "address family " + afi + " has no known address width, so its addresses cannot be checked (RFC 3779 sec. 2.2.3.3)");
|
|
725
|
+
function widthOk(seq) { return seq.length - 1 <= width; }
|
|
726
|
+
function absolute(node) {
|
|
727
|
+
var seq;
|
|
728
|
+
if (node.majorType === 2) { // bytes form: the sequence verbatim
|
|
729
|
+
sawBytes = true;
|
|
730
|
+
if (node.content.length === 0) throw _err("c509/bad-extensions", "an IPAddress byte sequence must be non-empty");
|
|
731
|
+
if (node.content[0] > 7) throw _err("c509/bad-extensions", "an IPAddress unused-bit count must be 0..7 (DER)");
|
|
732
|
+
seq = node.content;
|
|
733
|
+
} else {
|
|
734
|
+
sawInt = true;
|
|
735
|
+
var d = _cborIntVal(node, "an IPAddress");
|
|
736
|
+
var abs = prev === null ? d : prev + d;
|
|
737
|
+
prev = abs;
|
|
738
|
+
// Bound the reconstructed absolute through the BigInt-preserving guard: this domain reaches
|
|
739
|
+
// 2^64-1, so narrowing to Number would corrupt the value being guarded.
|
|
740
|
+
guard.range.uint64(abs, _err, "c509/bad-extensions", "an IPAddress");
|
|
741
|
+
seq = _ipIntToSeq(abs);
|
|
742
|
+
if (!seq) throw _err("c509/bad-extensions", "an IPAddress integer does not encode a DER BIT STRING (sec. 3.3)");
|
|
743
|
+
}
|
|
744
|
+
if (!widthOk(seq)) {
|
|
745
|
+
throw _err("c509/bad-extensions", "an IPAddress is wider than address family " + afi + " permits (RFC 3779 sec. 2.2.3.8)");
|
|
746
|
+
}
|
|
747
|
+
if (!_ipSeqBitsClear(seq)) {
|
|
748
|
+
throw _err("c509/bad-extensions", "an IPAddress must leave its declared unused bits zero (RFC 3779 sec. 2.2.3.8)");
|
|
749
|
+
}
|
|
750
|
+
return seq;
|
|
751
|
+
}
|
|
752
|
+
var bounds = [];
|
|
753
|
+
for (var i = 0; i < items.length; i++) {
|
|
754
|
+
var it = items[i];
|
|
755
|
+
if (it.majorType === 4) { // [min, max] -> an addressRange
|
|
756
|
+
if (!it.children || it.children.length !== 2) throw _err("c509/bad-extensions", "an IPAddress range must be exactly [min, max]");
|
|
757
|
+
var lo = absolute(it.children[0]), hi = absolute(it.children[1]);
|
|
758
|
+
seqs.push(lo); seqs.push(hi);
|
|
759
|
+
var rlo = _ipLow(lo, width), rhi = _ipHigh(hi, width);
|
|
760
|
+
{
|
|
761
|
+
// A span expressible as a prefix MUST use the prefix form (RFC 3779 sec. 2.2.3.7), or one
|
|
762
|
+
// address span would have two legal encodings.
|
|
763
|
+
if (_ipRangeIsPrefix(rlo, rhi)) throw _err("c509/bad-extensions", "an address range that is exactly a prefix must use the prefix form (RFC 3779 sec. 2.2.3.7)");
|
|
764
|
+
bounds.push({ lo: rlo, hi: rhi });
|
|
765
|
+
}
|
|
766
|
+
out.push(b.sequence([b.bitString(lo.subarray(1), lo[0]), b.bitString(hi.subarray(1), hi[0])]));
|
|
767
|
+
} else { // a single addressPrefix
|
|
768
|
+
var pfx = absolute(it);
|
|
769
|
+
seqs.push(pfx);
|
|
770
|
+
bounds.push({ lo: _ipLow(pfx, width), hi: _ipHigh(pfx, width) });
|
|
771
|
+
out.push(b.bitString(pfx.subarray(1), pfx[0]));
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
// The form choice is a SHALL, so the wrong one would give one DER two CBOR encodings. A family
|
|
775
|
+
// mixing the two arms is not a choice either admits; a bytes-form family whose every sequence
|
|
776
|
+
// fits 8 octets was required to use the int form.
|
|
777
|
+
if (sawBytes && sawInt) throw _err("c509/bad-extensions", "an IPAddressFamily must use one address form throughout (sec. 3.3)");
|
|
778
|
+
if (sawBytes && !seqs.some(function (s) { return s.length > 8; })) {
|
|
779
|
+
throw _err("c509/bad-extensions", "an IPAddressFamily whose addresses all fit 8 octets must use the integer form (sec. 3.3)");
|
|
780
|
+
}
|
|
781
|
+
// The SAME RFC 3779 sec. 2.2.3.6 canonical form the encode side requires, enforced here too.
|
|
782
|
+
// draft sec. 3.3 says "The limitations specified in [RFC3779] apply here as well", so a compact
|
|
783
|
+
// list that is unsorted, overlapping, or unmerged is malformed C509 -- and reconstructing it
|
|
784
|
+
// would emit a certificate an independent validator refuses, from CBOR this codec had accepted.
|
|
785
|
+
// Both directions must hold or the pair is not a bijection.
|
|
786
|
+
for (var r = 0; r < bounds.length; r++) {
|
|
787
|
+
if (_ipOctCmp(bounds[r].lo, bounds[r].hi) > 0) throw _err("c509/bad-extensions", "an IPAddress range must not end below its start (RFC 3779 sec. 2.2.3.9)");
|
|
788
|
+
}
|
|
789
|
+
if (!_ipRangesCanonical(bounds)) {
|
|
790
|
+
throw _err("c509/bad-extensions", "an IPAddressFamily must be sorted, non-overlapping and maximally merged (RFC 3779 sec. 2.2.3.6)");
|
|
791
|
+
}
|
|
792
|
+
return out;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// DER IPAddressChoice (a SEQUENCE of BIT STRING / SEQUENCE-of-two-BIT-STRING) -> the compact CBOR
|
|
796
|
+
// array. The form is chosen per family and is a SHALL: the bytes form applies to the WHOLE family
|
|
797
|
+
// as soon as any one sequence exceeds 8 octets, otherwise every member takes the delta-coded int
|
|
798
|
+
// form. Returns null on any shape the compact form cannot carry exactly.
|
|
799
|
+
function _ipChoiceFromDer(ch, afi) {
|
|
800
|
+
if (!_isUniversal(ch, asn1.TAGS.SEQUENCE) || !ch.children || ch.children.length === 0) return null;
|
|
801
|
+
// Collect each address as its `unusedBits || value` sequence, keeping the range grouping.
|
|
802
|
+
var groups = [], flat = [];
|
|
803
|
+
for (var i = 0; i < ch.children.length; i++) {
|
|
804
|
+
var el = ch.children[i], pair;
|
|
805
|
+
if (_isUniversal(el, asn1.TAGS.BIT_STRING)) {
|
|
806
|
+
var bs = asn1.read.bitString(el);
|
|
807
|
+
pair = [Buffer.concat([Buffer.from([bs.unusedBits]), bs.bytes])];
|
|
808
|
+
} else if (_isUniversal(el, asn1.TAGS.SEQUENCE) && el.children && el.children.length === 2 &&
|
|
809
|
+
_isUniversal(el.children[0], asn1.TAGS.BIT_STRING) && _isUniversal(el.children[1], asn1.TAGS.BIT_STRING)) {
|
|
810
|
+
var lo = asn1.read.bitString(el.children[0]), hi = asn1.read.bitString(el.children[1]);
|
|
811
|
+
pair = [Buffer.concat([Buffer.from([lo.unusedBits]), lo.bytes]),
|
|
812
|
+
Buffer.concat([Buffer.from([hi.unusedBits]), hi.bytes])];
|
|
813
|
+
} else return null;
|
|
814
|
+
groups.push(pair);
|
|
815
|
+
for (var k = 0; k < pair.length; k++) flat.push(pair[k]);
|
|
816
|
+
}
|
|
817
|
+
// The list must be in RFC 3779 sec. 2.2.3.6 canonical form -- sorted, non-overlapping, and with
|
|
818
|
+
// every contiguous pair already merged. A list that is not is not the one canonical encoding of
|
|
819
|
+
// its address set, so it is NOT re-encoded into a conforming one: this returns null and the
|
|
820
|
+
// extension rides the byte-string form, keeping its exact bytes and leaving the defect visible to
|
|
821
|
+
// a validator. Checking overlap and adjacency needs the family's address width, so a family whose
|
|
822
|
+
// AFI this codec does not know declines too, rather than compacting what it cannot verify.
|
|
823
|
+
// Defense-in-depth, and deliberately explicit: without the width the bound arithmetic below would
|
|
824
|
+
// fault and the caller's catch would produce the same fallback, so this line changes no observable
|
|
825
|
+
// verdict and no vector can isolate it. It stays because a fallback that depends on an exception
|
|
826
|
+
// being raised somewhere downstream is one refactor away from becoming an accepted value.
|
|
827
|
+
var width = _IP_WIDTH[afi];
|
|
828
|
+
if (!width) return null;
|
|
829
|
+
var bounds = [];
|
|
830
|
+
for (var g = 0; g < groups.length; g++) {
|
|
831
|
+
var bg = groups[g];
|
|
832
|
+
if (bg[0].length - 1 > width || bg[bg.length - 1].length - 1 > width) return null; // longer than the family's addresses
|
|
833
|
+
var blo = _ipLow(bg[0], width), bhi = _ipHigh(bg[bg.length - 1], width);
|
|
834
|
+
// The mirror of the decode-side rule: a range that is exactly a prefix had to be written as one.
|
|
835
|
+
// No vector can isolate this line today, because the encode path's round-trip self-verify
|
|
836
|
+
// re-parses what it produced and the decode rule rejects it there, yielding the same fallback.
|
|
837
|
+
// It stays explicit because that makes encode's correctness depend on decode continuing to
|
|
838
|
+
// throw -- soften the decode rule and this path would silently start compacting again.
|
|
839
|
+
if (bg.length === 2 && _ipRangeIsPrefix(blo, bhi)) return null;
|
|
840
|
+
bounds.push({ lo: blo, hi: bhi });
|
|
841
|
+
}
|
|
842
|
+
// A range's own endpoints must ascend too, which a single-entry list has no chance to violate.
|
|
843
|
+
for (var r = 0; r < bounds.length; r++) {
|
|
844
|
+
if (_ipOctCmp(bounds[r].lo, bounds[r].hi) > 0) return null;
|
|
845
|
+
}
|
|
846
|
+
if (!_ipRangesCanonical(bounds)) return null;
|
|
847
|
+
var useBytes = flat.some(function (s) { return s.length > 8; });
|
|
848
|
+
var out = [];
|
|
849
|
+
if (useBytes) {
|
|
850
|
+
groups.forEach(function (p) {
|
|
851
|
+
out.push(p.length === 1 ? cbor.build.byteString(p[0])
|
|
852
|
+
: cbor.build.array([cbor.build.byteString(p[0]), cbor.build.byteString(p[1])]));
|
|
853
|
+
});
|
|
854
|
+
return cbor.build.array(out);
|
|
855
|
+
}
|
|
856
|
+
var prev = null;
|
|
857
|
+
function delta(seq) {
|
|
858
|
+
var n = _ipSeqToInt(seq);
|
|
859
|
+
if (n === null) return null;
|
|
860
|
+
var d = prev === null ? n : n - prev;
|
|
861
|
+
prev = n;
|
|
862
|
+
return cbor.build.int(d);
|
|
863
|
+
}
|
|
864
|
+
for (var gi = 0; gi < groups.length; gi++) {
|
|
865
|
+
var grp = groups[gi];
|
|
866
|
+
if (grp.length === 1) {
|
|
867
|
+
var one = delta(grp[0]);
|
|
868
|
+
if (!one) return null;
|
|
869
|
+
out.push(one);
|
|
870
|
+
} else {
|
|
871
|
+
var dlo = delta(grp[0]);
|
|
872
|
+
var dhi = dlo ? delta(grp[1]) : null;
|
|
873
|
+
if (!dlo || !dhi) return null;
|
|
874
|
+
out.push(cbor.build.array([dlo, dhi]));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return cbor.build.array(out);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// One ASId in the delta chain -> its absolute value, bounded to the RFC 3779 32-bit ASId domain.
|
|
881
|
+
// Safe to narrow here (2^32-1 < 2^53), unlike the IP domain.
|
|
882
|
+
function _asDelta(node, prev) {
|
|
883
|
+
var d = _cborIntVal(node, "an ASIdentifier");
|
|
884
|
+
var abs = prev === null ? d : prev + d;
|
|
885
|
+
// Bound only -- the narrowed Number the guard returns is deliberately discarded, because the
|
|
886
|
+
// chain's next step adds a BigInt delta to this value and mixing the two throws.
|
|
887
|
+
guard.range.int(abs, 0n, 4294967295n, _err, "c509/bad-extensions", "an ASIdentifier");
|
|
888
|
+
return abs;
|
|
889
|
+
}
|
|
890
|
+
|
|
558
891
|
// GeneralSubtrees = [ + GeneralName ] (the flat int/value array) <-> the concatenated GeneralSubtree
|
|
559
892
|
// SEQUENCEs (RFC 5280 sec. 4.2.1.10: SEQUENCE { base, minimum [0] DEFAULT 0, maximum [1] OPTIONAL }); the
|
|
560
893
|
// C509 profile omits minimum/maximum, so each GeneralSubtree is base-only.
|
|
@@ -799,6 +1132,82 @@ function _extValueToDer(name, node, isNative) {
|
|
|
799
1132
|
if (node.majorType === 3) return b.sequence([b.sequence([b.explicit(0, b.contextConstructed(0, b.contextPrimitive(6, _ia5Bytes(node, 6))))])]);
|
|
800
1133
|
if (node.majorType !== 4 || !node.children || node.children.length < 1) throw _err("c509/bad-extensions", "a " + name + " value must be a CBOR array of DistributionPoints or a bare URI text (sec. 3.3)");
|
|
801
1134
|
return b.sequence(node.children.map(function (dp) { return _dpToDer(dp, isNative); }));
|
|
1135
|
+
// IPAddrBlocks (and its RFC 8360 v2 twin): a FLAT array of (AFI, SAFI, choice) triples --
|
|
1136
|
+
// IPAddressFamily is a parenthesized CDDL group, so it splices rather than nesting.
|
|
1137
|
+
case "ipAddrBlocks":
|
|
1138
|
+
case "ipAddrBlocksV2": {
|
|
1139
|
+
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "an IPAddrBlocks value must be a CBOR array");
|
|
1140
|
+
var ipKids = node.children;
|
|
1141
|
+
if (ipKids.length === 0 || ipKids.length % 3 !== 0) throw _err("c509/bad-extensions", "an IPAddrBlocks array must be non-empty (AFI, SAFI, addresses) triples (sec. 3.3)");
|
|
1142
|
+
var families = [], prevFam = null;
|
|
1143
|
+
for (var fi = 0; fi + 2 < ipKids.length; fi += 3) {
|
|
1144
|
+
var afi = _cborUint(ipKids[fi], "an IPAddrBlocks AFI");
|
|
1145
|
+
if (afi > 0xffffn) throw _err("c509/bad-extensions", "an IPAddrBlocks AFI must fit two octets (RFC 3779 sec. 2.2.3.3)");
|
|
1146
|
+
var safiNode = ipKids[fi + 1], famBytes = [Number(afi >> 8n) & 0xff, Number(afi & 0xffn)];
|
|
1147
|
+
if (!_isCborNull(safiNode)) {
|
|
1148
|
+
var safi = _cborUint(safiNode, "an IPAddrBlocks SAFI");
|
|
1149
|
+
if (safi > 0xffn) throw _err("c509/bad-extensions", "an IPAddrBlocks SAFI must fit one octet (RFC 3779 sec. 2.2.3.3)");
|
|
1150
|
+
famBytes.push(Number(safi));
|
|
1151
|
+
}
|
|
1152
|
+
// The families themselves are ordered and unique (RFC 3779 sec. 2.2.3.3), the same way the
|
|
1153
|
+
// addresses inside one are. Without this a value could name AFI 2 before AFI 1, or repeat a
|
|
1154
|
+
// family, and reconstruct a certificate an independent validator refuses.
|
|
1155
|
+
var famOct = Buffer.from(famBytes);
|
|
1156
|
+
if (prevFam !== null && _famOctCmp(prevFam, famOct) >= 0) {
|
|
1157
|
+
throw _err("c509/bad-extensions", "IPAddrBlocks address families must be unique and in ascending addressFamily order (RFC 3779 sec. 2.2.3.3)");
|
|
1158
|
+
}
|
|
1159
|
+
prevFam = famOct;
|
|
1160
|
+
var choice = ipKids[fi + 2], famFields = [b.octetString(famOct)];
|
|
1161
|
+
if (_isCborNull(choice)) { // null -> inherit
|
|
1162
|
+
famFields.push(b.nullValue());
|
|
1163
|
+
} else {
|
|
1164
|
+
if (choice.majorType !== 4 || !choice.children || choice.children.length === 0) {
|
|
1165
|
+
throw _err("c509/bad-extensions", "an IPAddrBlocks address choice must be null (inherit) or a non-empty CBOR array");
|
|
1166
|
+
}
|
|
1167
|
+
famFields.push(b.sequence(_ipChoiceToDer(choice.children, Number(afi))));
|
|
1168
|
+
}
|
|
1169
|
+
families.push(b.sequence(famFields));
|
|
1170
|
+
}
|
|
1171
|
+
return b.sequence(families);
|
|
1172
|
+
}
|
|
1173
|
+
// ASIdentifiers (and its v2 twin): null = inherit, else a flat array of uint / [min,max].
|
|
1174
|
+
// Only the asnum field is representable -- a present rdi has no compact form (sec. 3.3), so a
|
|
1175
|
+
// certificate carrying one rides the ~oid byte-string form and never reaches this arm.
|
|
1176
|
+
case "autonomousSysIds":
|
|
1177
|
+
case "autonomousSysIdsV2": {
|
|
1178
|
+
if (_isCborNull(node)) return b.sequence([b.explicit(0, b.nullValue())]); // asnum inherit
|
|
1179
|
+
if (node.majorType !== 4 || !node.children || node.children.length === 0) {
|
|
1180
|
+
throw _err("c509/bad-extensions", "an ASIdentifiers value must be null (inherit) or a non-empty CBOR array");
|
|
1181
|
+
}
|
|
1182
|
+
// The deltas are `uint` in the draft's CDDL precisely because RFC 3779 sec. 3.2.3.4 sorts AS
|
|
1183
|
+
// ids by increasing value: a negative delta would walk the chain backwards. That section also
|
|
1184
|
+
// forbids a pair overlapping and requires a contiguous series to be one range -- the same
|
|
1185
|
+
// three rules the encode side applies, tracked here across members through `asPrevHigh`.
|
|
1186
|
+
// A negative delta needs no separate test: on the first entry it drives the absolute below
|
|
1187
|
+
// zero and the range guard refuses it, and on any later entry it lands at or below the
|
|
1188
|
+
// previous high, which the canonical test already refuses. A separate uint check would be a
|
|
1189
|
+
// branch nothing can reach.
|
|
1190
|
+
var asKids = node.children, asDers = [], asPrev = null, asPrevHigh = null;
|
|
1191
|
+
for (var asi2 = 0; asi2 < asKids.length; asi2++) {
|
|
1192
|
+
var it = asKids[asi2];
|
|
1193
|
+
if (it.majorType === 4) { // [min, max] -> ASRange
|
|
1194
|
+
if (!it.children || it.children.length !== 2) throw _err("c509/bad-extensions", "an ASIdentifiers range must be exactly [min, max]");
|
|
1195
|
+
var amin = _asDelta(it.children[0], asPrev), amax = _asDelta(it.children[1], amin);
|
|
1196
|
+
if (amax <= amin) throw _err("c509/bad-extensions", "an ASIdentifiers range must be ascending (RFC 3779 sec. 3.2.3.6)");
|
|
1197
|
+
if (asPrevHigh !== null && amin <= asPrevHigh + 1n) throw _err("c509/bad-extensions", "ASIdentifiers must be sorted, non-overlapping and maximally merged (RFC 3779 sec. 3.2.3.4)");
|
|
1198
|
+
asDers.push(b.sequence([b.integer(amin), b.integer(amax)]));
|
|
1199
|
+
asPrev = amax;
|
|
1200
|
+
asPrevHigh = amax;
|
|
1201
|
+
} else { // uint -> ASId
|
|
1202
|
+
var aid = _asDelta(it, asPrev);
|
|
1203
|
+
if (asPrevHigh !== null && aid <= asPrevHigh + 1n) throw _err("c509/bad-extensions", "ASIdentifiers must be sorted, non-overlapping and maximally merged (RFC 3779 sec. 3.2.3.4)");
|
|
1204
|
+
asDers.push(b.integer(aid));
|
|
1205
|
+
asPrev = aid;
|
|
1206
|
+
asPrevHigh = aid;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return b.sequence([b.explicit(0, b.sequence(asDers))]);
|
|
1210
|
+
}
|
|
802
1211
|
case "certificatePolicies": { // [ pid, [ *(qid, qtext) ], ... ] -> SEQUENCE OF PolicyInformation
|
|
803
1212
|
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a certificatePolicies value must be a CBOR array");
|
|
804
1213
|
var cpKids = node.children;
|
|
@@ -960,6 +1369,73 @@ function _extValueFromDer(name, der) {
|
|
|
960
1369
|
if (dpResults.length === 1 && dpResults[0].oneUri != null && dpResults[0].noReasons && dpResults[0].noIssuer) return cbor.build.textString(dpResults[0].oneUri);
|
|
961
1370
|
return cbor.build.array(dpResults.map(function (r) { return r.triple; }));
|
|
962
1371
|
}
|
|
1372
|
+
// IPAddrBlocks -> the flat (AFI, SAFI, choice) array. Any shape the compact form cannot
|
|
1373
|
+
// represent exactly returns null, so the extension rides the ~oid byte-string form intact.
|
|
1374
|
+
case "ipAddrBlocks":
|
|
1375
|
+
case "ipAddrBlocksV2": {
|
|
1376
|
+
if (!_isUniversal(node, asn1.TAGS.SEQUENCE) || !node.children || node.children.length === 0) return null;
|
|
1377
|
+
var ipOut = [], prevFamOct = null;
|
|
1378
|
+
for (var ifi = 0; ifi < node.children.length; ifi++) {
|
|
1379
|
+
var fam = node.children[ifi];
|
|
1380
|
+
if (!_isUniversal(fam, asn1.TAGS.SEQUENCE) || !fam.children || fam.children.length !== 2) return null;
|
|
1381
|
+
var famOct = asn1.read.octetString(fam.children[0]);
|
|
1382
|
+
if (famOct.length !== 2 && famOct.length !== 3) return null; // OCTET STRING (SIZE (2..3))
|
|
1383
|
+
// Families ordered and unique (RFC 3779 sec. 2.2.3.3) -- the mirror of the decode side.
|
|
1384
|
+
if (prevFamOct !== null && _famOctCmp(prevFamOct, famOct) >= 0) return null;
|
|
1385
|
+
prevFamOct = famOct;
|
|
1386
|
+
var afiVal = (famOct[0] << 8) | famOct[1];
|
|
1387
|
+
ipOut.push(cbor.build.uint(BigInt(afiVal)));
|
|
1388
|
+
ipOut.push(famOct.length === 3 ? cbor.build.uint(BigInt(famOct[2])) : cbor.build.nullValue());
|
|
1389
|
+
var ch = fam.children[1];
|
|
1390
|
+
if (_isUniversal(ch, asn1.TAGS.NULL)) { ipOut.push(cbor.build.nullValue()); continue; } // inherit
|
|
1391
|
+
var chOut = _ipChoiceFromDer(ch, afiVal);
|
|
1392
|
+
if (!chOut) return null;
|
|
1393
|
+
ipOut.push(chOut);
|
|
1394
|
+
}
|
|
1395
|
+
return cbor.build.array(ipOut);
|
|
1396
|
+
}
|
|
1397
|
+
// ASIdentifiers -> null (inherit) or the flat delta array. Only asnum is representable: a
|
|
1398
|
+
// present rdi has no compact form (sec. 3.3), so such a certificate returns null here.
|
|
1399
|
+
case "autonomousSysIds":
|
|
1400
|
+
case "autonomousSysIdsV2": {
|
|
1401
|
+
if (!_isUniversal(node, asn1.TAGS.SEQUENCE) || !node.children || node.children.length !== 1) return null;
|
|
1402
|
+
var asnum = node.children[0];
|
|
1403
|
+
if (asnum.tagClass !== "context" || asnum.tagNumber !== 0 || !asnum.children || asnum.children.length !== 1) return null;
|
|
1404
|
+
var inner = asnum.children[0];
|
|
1405
|
+
if (_isUniversal(inner, asn1.TAGS.NULL)) return cbor.build.nullValue(); // asnum inherit
|
|
1406
|
+
if (!_isUniversal(inner, asn1.TAGS.SEQUENCE) || !inner.children || inner.children.length === 0) return null;
|
|
1407
|
+
// RFC 3779 sec. 3.2.3.4 fixes the canonical form the same way sec. 2.2.3.6 does for addresses:
|
|
1408
|
+
// sorted by increasing value, no pair overlapping, and any contiguous series already merged
|
|
1409
|
+
// into one range. `asPrevHigh` carries the previous entry's upper bound so all three hold
|
|
1410
|
+
// ACROSS members -- checking only within a range would let a descending or adjacent pair
|
|
1411
|
+
// through. A list that is not canonical is left uncompacted with its bytes intact.
|
|
1412
|
+
var asOut = [], asPrevOut = null, asPrevHigh = null;
|
|
1413
|
+
for (var aoi = 0; aoi < inner.children.length; aoi++) {
|
|
1414
|
+
var el = inner.children[aoi], elLo, elHi;
|
|
1415
|
+
if (_isUniversal(el, asn1.TAGS.INTEGER)) {
|
|
1416
|
+
elLo = asn1.read.integer(el);
|
|
1417
|
+
elHi = elLo;
|
|
1418
|
+
if (elLo < 0n || elLo > 4294967295n) return null;
|
|
1419
|
+
} else if (_isUniversal(el, asn1.TAGS.SEQUENCE) && el.children && el.children.length === 2) {
|
|
1420
|
+
elLo = asn1.read.integer(el.children[0]);
|
|
1421
|
+
elHi = asn1.read.integer(el.children[1]);
|
|
1422
|
+
if (elLo < 0n || elHi > 4294967295n || elHi <= elLo) return null;
|
|
1423
|
+
} else return null;
|
|
1424
|
+
if (asPrevHigh !== null && elLo <= asPrevHigh + 1n) return null; // descending, overlapping, or contiguous
|
|
1425
|
+
if (el.tagNumber === asn1.TAGS.INTEGER) {
|
|
1426
|
+
asOut.push(cbor.build.int(asPrevOut === null ? elLo : elLo - asPrevOut));
|
|
1427
|
+
asPrevOut = elLo;
|
|
1428
|
+
} else {
|
|
1429
|
+
asOut.push(cbor.build.array([
|
|
1430
|
+
cbor.build.int(asPrevOut === null ? elLo : elLo - asPrevOut),
|
|
1431
|
+
cbor.build.int(elHi - elLo),
|
|
1432
|
+
]));
|
|
1433
|
+
asPrevOut = elHi;
|
|
1434
|
+
}
|
|
1435
|
+
asPrevHigh = elHi;
|
|
1436
|
+
}
|
|
1437
|
+
return cbor.build.array(asOut);
|
|
1438
|
+
}
|
|
963
1439
|
case "certificatePolicies": { // SEQUENCE OF PolicyInformation -> [ pid, [ *(qid, qtext) ], ... ]
|
|
964
1440
|
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length < 1) return null;
|
|
965
1441
|
var cpOut = [];
|
|
@@ -1879,6 +2355,14 @@ function _compressEcPoint(point, coordLen) {
|
|
|
1879
2355
|
_derToType3 = function (input, opts) {
|
|
1880
2356
|
var c;
|
|
1881
2357
|
try { c = x509.parse(input); } catch (e) { throw _err("c509/bad-input", "the input is not a valid X.509 certificate", e); }
|
|
2358
|
+
// Both C509 certificate types are defined over X.509 v3 (draft-ietf-cose-cbor-encoded-cert
|
|
2359
|
+
// sec. 1), and the encoding carries no version field -- reconstruction always emits v3. So a v1
|
|
2360
|
+
// or v2 certificate is outside the format, not a codec limitation, and saying so here keeps it
|
|
2361
|
+
// from falling through to the byte-compare below, whose "does not reconstruct byte-for-byte"
|
|
2362
|
+
// reads as a defect in this encoder rather than a certificate the format does not cover.
|
|
2363
|
+
// (A v3 certificate with the extensions field OMITTED is fully supported: sec. 3.1.10 encodes
|
|
2364
|
+
// an omitted 'extensions' field as an empty CBOR array.)
|
|
2365
|
+
if (c.version !== 3) throw _err("c509/non-invertible", "C509 covers X.509 v3 certificates; got v" + c.version);
|
|
1882
2366
|
if (!/^ecdsa/i.test(c.signatureAlgorithm.name || "")) throw _err("c509/non-invertible", "type-3 C509 encoding covers only ECDSA-signed certificates; got " + (c.signatureAlgorithm.name || "an unregistered algorithm"));
|
|
1883
2367
|
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"));
|
|
1884
2368
|
var curveOid = asn1.read.oid(asn1.decode(c.subjectPublicKeyInfo.algorithm.parameters));
|
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:1be17160-9a0e-4c1f-b171-d5234287a696",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-08T17:10:39.565Z",
|
|
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.4.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.4.4",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.4.
|
|
25
|
+
"version": "0.4.4",
|
|
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.4.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.4.4",
|
|
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.4.
|
|
57
|
+
"ref": "@blamejs/pki@0.4.4",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|