@blamejs/pki 0.3.3 → 0.3.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 +9 -0
- package/README.md +1 -1
- package/lib/cbor-det.js +103 -0
- package/lib/schema-all.js +1 -1
- package/lib/schema-c509.js +334 -9
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ 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.3.4 — 2026-07-17
|
|
8
|
+
|
|
9
|
+
pki.schema.c509.encode produces C509 CBOR certificates -- a DER X.509 certificate compresses to a compact, byte-exact-invertible type-3 C509, and a deterministic-CBOR encoder joins pki.cbor.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.c509.encode(input) encodes a C509 certificate to deterministic-CBOR bytes -- a DER X.509 v3 certificate to a compact type-3 C509 (byte-exact-invertible, so the original signature verifies), or a pki.schema.c509.parse result re-emitted to its native array. Canonical deterministic CBOR with the registry integer shorthands and the C509 compressions; a certificate outside the invertible covered set throws a typed C509Error. Certificate parsing remains pki.schema.c509.parse.
|
|
14
|
+
- pki.cbor.build is a deterministic-CBOR encoder (RFC 8949 section 4.2) -- the byte-exact inverse of pki.cbor.decode: shortest-form heads, definite lengths, sorted and unique map keys, over unsigned and negative integers, byte and text strings, arrays, maps, tags, and the tagged bignum / epoch-time / object-identifier leaves. Encoded output always re-decodes through the strict decoder.
|
|
15
|
+
|
|
7
16
|
## v0.3.3 — 2026-07-17
|
|
8
17
|
|
|
9
18
|
pki.crmf.build issues RFC 4211 certificate request messages -- a CertReqMessages with a signature proof of possession, over every algorithm the toolkit supports.
|
package/README.md
CHANGED
|
@@ -204,7 +204,7 @@ is callable today; nothing below is a stub.
|
|
|
204
204
|
| `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation and certificate/PKCS#8 import — the RFC 9935 seed / expandedKey / both private-key CHOICE is validated fail-closed, so an OpenSSL-legacy bare-seed or an internally inconsistent key is rejected with a typed error (KEM encapsulation lands with CMS KEM-decrypt). Zero-dependency, OpenSSL-interoperable |
|
|
205
205
|
| `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 |
|
|
206
206
|
| `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` |
|
|
207
|
-
| `pki.schema.c509` | Parse 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) |
|
|
207
|
+
| `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 and the C509 compressions; a certificate outside the invertible set throws a typed `C509Error` |
|
|
208
208
|
| `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` |
|
|
209
209
|
| `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` |
|
|
210
210
|
| `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/cbor-det.js
CHANGED
|
@@ -624,8 +624,111 @@ function readOid(node) {
|
|
|
624
624
|
return asn1.decodeOidContent(inner.content);
|
|
625
625
|
}
|
|
626
626
|
|
|
627
|
+
// ---- deterministic-CBOR encoder (RFC 8949 sec. 4.2) -- the byte-exact inverse of decode ----
|
|
628
|
+
// Every emitted head is the shortest form (the exact mirror of decode's cbor/non-minimal-argument reject),
|
|
629
|
+
// every length is definite, and map keys are sorted bytewise + unique -- so decode(build.x(...)) round-trips
|
|
630
|
+
// or the builder produced a shape its own strict decoder rejects. Nested items are pre-encoded CBOR item
|
|
631
|
+
// Buffers (the asn1.build pattern); the CBOR-encoded PKI surfaces (C509, COSE) compose these leaves.
|
|
632
|
+
function _cbItem(x, who) {
|
|
633
|
+
if (!Buffer.isBuffer(x)) throw new CborError("cbor/not-buffer", who + ": expected a pre-encoded CBOR item Buffer");
|
|
634
|
+
return x;
|
|
635
|
+
}
|
|
636
|
+
// Validate an assembled container decodes: well-formed nested items (no malformed head, indefinite length,
|
|
637
|
+
// stray break, or trailing bytes), AND within the decoder's depth cap -- a container adds ONE nesting
|
|
638
|
+
// level, so a child AT the cap would overflow the output. Decoding the whole output in one pass catches
|
|
639
|
+
// both, so the encoder never produces bytes its own strict decoder rejects.
|
|
640
|
+
function _assertDecodes(out, who) {
|
|
641
|
+
try { decode(out); } catch (e) { throw new CborError("cbor/bad-item", who + ": produced bytes the strict decoder rejects (" + (e && e.code) + ")", e); }
|
|
642
|
+
return out;
|
|
643
|
+
}
|
|
644
|
+
// Mirror the decoder's total-size cap (constants.LIMITS.CBOR_MAX_BYTES) so a string leaf never emits an
|
|
645
|
+
// item the strict reader would reject as too large -- the encode/decode round trip stays closed under the
|
|
646
|
+
// default limits.
|
|
647
|
+
function _capString(out, who) {
|
|
648
|
+
if (out.length > constants.LIMITS.CBOR_MAX_BYTES) throw new CborError("cbor/too-large", who + ": encoded " + out.length + " bytes exceeds cap " + constants.LIMITS.CBOR_MAX_BYTES);
|
|
649
|
+
return out;
|
|
650
|
+
}
|
|
651
|
+
// Reject a JS string carrying an unpaired UTF-16 surrogate: Buffer.from(..., "utf8") would silently
|
|
652
|
+
// substitute U+FFFD, so the emitted text string would not be the input (and read.text enforces well-formed
|
|
653
|
+
// UTF-8). Fail closed to match the decoder rather than corrupt the text.
|
|
654
|
+
function _assertWellFormedText(str, who) {
|
|
655
|
+
for (var i = 0; i < str.length; i++) {
|
|
656
|
+
var c = str.charCodeAt(i);
|
|
657
|
+
if (c >= 0xd800 && c <= 0xdbff) { var n = str.charCodeAt(i + 1); if (!(n >= 0xdc00 && n <= 0xdfff)) throw new CborError("cbor/bad-utf8", who + ": the string has an unpaired high surrogate"); i++; }
|
|
658
|
+
else if (c >= 0xdc00 && c <= 0xdfff) throw new CborError("cbor/bad-utf8", who + ": the string has an unpaired low surrogate");
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// The initial byte + minimal-width argument for major type `mt` and unsigned `argument`.
|
|
662
|
+
function _head(mt, argument, who) {
|
|
663
|
+
var a = typeof argument === "bigint" ? argument : BigInt(argument);
|
|
664
|
+
if (a < 0n) throw new CborError("cbor/bad-argument", who + ": a CBOR head argument must be non-negative");
|
|
665
|
+
if (a > 0xffffffffffffffffn) throw new CborError("cbor/bad-argument", who + ": a CBOR head argument exceeds 64 bits");
|
|
666
|
+
var mtb = mt << 5, b;
|
|
667
|
+
if (a <= 23n) return Buffer.from([mtb | Number(a)]);
|
|
668
|
+
if (a < 256n) return Buffer.from([mtb | 24, Number(a)]);
|
|
669
|
+
if (a < 65536n) { b = Buffer.alloc(3); b[0] = mtb | 25; b.writeUInt16BE(Number(a), 1); return b; }
|
|
670
|
+
if (a < 4294967296n) { b = Buffer.alloc(5); b[0] = mtb | 26; b.writeUInt32BE(Number(a), 1); return b; }
|
|
671
|
+
b = Buffer.alloc(9); b[0] = mtb | 27; b.writeBigUInt64BE(a, 1); return b;
|
|
672
|
+
}
|
|
673
|
+
var build = {
|
|
674
|
+
uint: function (n) { var a = typeof n === "bigint" ? n : BigInt(n); if (a < 0n) throw new CborError("cbor/bad-argument", "build.uint: value must be non-negative"); return _head(0, a, "build.uint"); },
|
|
675
|
+
nint: function (n) { var a = typeof n === "bigint" ? n : BigInt(n); if (a >= 0n) throw new CborError("cbor/bad-argument", "build.nint: value must be negative"); return _head(1, -1n - a, "build.nint"); },
|
|
676
|
+
int: function (n) { var a = typeof n === "bigint" ? n : BigInt(n); return a >= 0n ? _head(0, a, "build.int") : _head(1, -1n - a, "build.int"); },
|
|
677
|
+
byteString: function (buf) { var b = _asBuffer(buf, "build.byteString"); return _capString(Buffer.concat([_head(2, b.length, "build.byteString"), b]), "build.byteString"); },
|
|
678
|
+
textString: function (s) { var str = String(s); _assertWellFormedText(str, "build.textString"); var b = Buffer.from(str, "utf8"); return _capString(Buffer.concat([_head(3, b.length, "build.textString"), b]), "build.textString"); },
|
|
679
|
+
array: function (items) {
|
|
680
|
+
if (!Array.isArray(items)) throw new CborError("cbor/bad-argument", "build.array: expected an array of pre-encoded items");
|
|
681
|
+
var parts = items.map(function (x, i) { return _cbItem(x, "build.array[" + i + "]"); });
|
|
682
|
+
return _assertDecodes(Buffer.concat([_head(4, items.length, "build.array")].concat(parts)), "build.array");
|
|
683
|
+
},
|
|
684
|
+
map: function (pairs) {
|
|
685
|
+
if (!Array.isArray(pairs)) throw new CborError("cbor/bad-argument", "build.map: expected an array of [key, value] pairs");
|
|
686
|
+
var enc = pairs.map(function (p, i) { if (!Array.isArray(p) || p.length !== 2) throw new CborError("cbor/bad-argument", "build.map[" + i + "]: each entry is a [key, value] pair"); return [_cbItem(p[0], "build.map key"), _cbItem(p[1], "build.map value")]; });
|
|
687
|
+
// RFC 8949 sec. 4.2.1 Core Deterministic: keys sorted in the BYTEWISE lexicographic order of their
|
|
688
|
+
// encodings -- NOT the length-first order of sec. 4.2.3 (the older RFC 7049 "Canonical CBOR" variant).
|
|
689
|
+
// This MUST match the decoder, which enforces bytewise order (_profile "deterministic" -> Buffer.compare
|
|
690
|
+
// at :70); a length-first sort here would emit a map the strict decoder then rejects as unsorted.
|
|
691
|
+
enc.sort(function (a, b) { return Buffer.compare(a[0], b[0]); });
|
|
692
|
+
for (var i = 1; i < enc.length; i++) { if (Buffer.compare(enc[i - 1][0], enc[i][0]) === 0) throw new CborError("cbor/duplicate-map-key", "build.map: duplicate map key"); }
|
|
693
|
+
var flat = [_head(5, pairs.length, "build.map")];
|
|
694
|
+
enc.forEach(function (p) { flat.push(p[0], p[1]); });
|
|
695
|
+
return _assertDecodes(Buffer.concat(flat), "build.map");
|
|
696
|
+
},
|
|
697
|
+
tag: function (tagNum, inner) { return _assertDecodes(Buffer.concat([_head(6, tagNum, "build.tag"), _cbItem(inner, "build.tag content")]), "build.tag"); },
|
|
698
|
+
boolean: function (v) { return Buffer.from([v ? 0xf5 : 0xf4]); },
|
|
699
|
+
nullValue: function () { return Buffer.from([0xf6]); },
|
|
700
|
+
raw: function (buf) { return _assertDecodes(_asBuffer(buf, "build.raw"), "build.raw"); },
|
|
701
|
+
// Tagged leaves -- the inverse of read.biguint / read.time / read.oid.
|
|
702
|
+
biguint: function (n) {
|
|
703
|
+
var a = typeof n === "bigint" ? n : BigInt(n);
|
|
704
|
+
if (a < 0n) throw new CborError("cbor/bad-argument", "build.biguint: value must be non-negative");
|
|
705
|
+
var hex = a.toString(16); if (hex.length % 2) hex = "0" + hex;
|
|
706
|
+
var mag = a === 0n ? Buffer.alloc(0) : Buffer.from(hex, "hex");
|
|
707
|
+
if (mag.length <= 8) throw new CborError("cbor/bad-argument", "build.biguint: a value that fits 8 bytes must use build.uint (RFC 8949 preferred serialization)");
|
|
708
|
+
// Mirror read.biguint's cap so the tagged bignum builder never emits a value the strict reader rejects.
|
|
709
|
+
if (mag.length > constants.LIMITS.CBOR_MAX_BIGUINT_BYTES) throw new CborError("cbor/biguint-too-large", "build.biguint: " + mag.length + " bytes exceeds cap " + constants.LIMITS.CBOR_MAX_BIGUINT_BYTES);
|
|
710
|
+
return build.tag(2, build.byteString(mag));
|
|
711
|
+
},
|
|
712
|
+
time: function (v) {
|
|
713
|
+
var ms;
|
|
714
|
+
if (v instanceof Date) {
|
|
715
|
+
ms = v.getTime();
|
|
716
|
+
if (Number.isNaN(ms)) throw new CborError("cbor/bad-time", "build.time: an Invalid Date has no epoch value");
|
|
717
|
+
// read.time reads only an integer (second-granularity) tag-1, so a sub-second Date cannot round-trip:
|
|
718
|
+
// reject it rather than silently truncate the milliseconds.
|
|
719
|
+
if (ms % 1000 !== 0) throw new CborError("cbor/bad-time", "build.time: a Date with sub-second precision cannot be a second-granularity CBOR epoch; round to whole seconds");
|
|
720
|
+
}
|
|
721
|
+
var secs = v instanceof Date ? BigInt(Math.floor(ms / 1000)) : (typeof v === "bigint" ? v : BigInt(v));
|
|
722
|
+
// Mirror read.time's bound so build.time never emits a tag-1 the strict reader would reject as out of range.
|
|
723
|
+
if (secs < -_MAX_EPOCH_SECONDS || secs > _MAX_EPOCH_SECONDS) throw new CborError("cbor/bad-time", "build.time: epoch seconds " + secs + " are outside the representable Date range");
|
|
724
|
+
return build.tag(1, build.int(secs));
|
|
725
|
+
},
|
|
726
|
+
oid: function (dotted) { return build.tag(111, build.byteString(asn1.encodeOidContent(dotted))); },
|
|
727
|
+
};
|
|
728
|
+
|
|
627
729
|
module.exports = {
|
|
628
730
|
decode: decode,
|
|
731
|
+
build: build,
|
|
629
732
|
read: {
|
|
630
733
|
uint: readUint,
|
|
631
734
|
nint: readNint,
|
package/lib/schema-all.js
CHANGED
|
@@ -298,7 +298,7 @@ module.exports = {
|
|
|
298
298
|
x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode },
|
|
299
299
|
// C509 is CBOR, not DER: an explicit-call surface only, NOT added to FORMATS / the detect-and-route
|
|
300
300
|
// parse() below (which detect DER shapes). No pemDecode/pemEncode -- C509 is binary CBOR.
|
|
301
|
-
c509: { parse: c509.parse },
|
|
301
|
+
c509: { parse: c509.parse, encode: c509.encode },
|
|
302
302
|
crl: { parse: crl.parse, pemDecode: crl.pemDecode, pemEncode: crl.pemEncode },
|
|
303
303
|
csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
|
|
304
304
|
pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
|
package/lib/schema-c509.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
var cbor = require("./cbor-det");
|
|
18
18
|
var asn1 = require("./asn1-der");
|
|
19
19
|
var oid = require("./oid");
|
|
20
|
+
var x509 = require("./schema-x509");
|
|
21
|
+
var pkix = require("./schema-pkix");
|
|
20
22
|
var constants = require("./constants");
|
|
21
23
|
var frameworkError = require("./framework-error");
|
|
22
24
|
var validator = require("./validator-all");
|
|
@@ -452,14 +454,16 @@ function parse(input) {
|
|
|
452
454
|
signatureValue: signatureValue,
|
|
453
455
|
};
|
|
454
456
|
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
457
|
+
// The RAW bytes of CBOR-array elements 0..9 (NOT the outer array head, NOT the signature) -- a zero-copy
|
|
458
|
+
// subarray. For a native (type-2) certificate this is the signed region a verifier hashes (sec. 3.1.12);
|
|
459
|
+
// surfaced as `_fieldBytes` for BOTH types so encode() re-emits a parsed certificate byte-for-byte (a
|
|
460
|
+
// re-derivation from the decoded values could differ on a canonical-equivalent form, breaking a type-2
|
|
461
|
+
// signature or a type-3 DER reconstruction). `signedData` keeps its type-2-only signed-region meaning.
|
|
462
|
+
// From the DECODED root's own bytes (root.bytes == array head + all 11 elements), not offset arithmetic
|
|
463
|
+
// on `input` (which breaks when input is a Uint8Array the codec normalized to a different buffer): the
|
|
464
|
+
// fields region is root.bytes minus the 1-byte array(11) head and minus the trailing signatureValue.
|
|
465
|
+
result._fieldBytes = root.bytes.subarray(1, root.bytes.length - f[10].bytes.length);
|
|
466
|
+
if (type === 2) result.signedData = result._fieldBytes;
|
|
463
467
|
|
|
464
468
|
// Type-3 is an invertible re-encoding of a DER X.509 certificate: reconstruct the original DER
|
|
465
469
|
// byte-for-byte so the original signature verifies and x509.parse recovers the certificate.
|
|
@@ -476,4 +480,325 @@ function matches(node) {
|
|
|
476
480
|
(Number(node.children[0].argument) === 2 || Number(node.children[0].argument) === 3);
|
|
477
481
|
}
|
|
478
482
|
|
|
479
|
-
|
|
483
|
+
// ---- the encode (the producing side; draft-20 sec. 3) -----------------------
|
|
484
|
+
// The byte-exact inverse of parse/reconstruct: emit the 11-element deterministic-CBOR C509 array via the
|
|
485
|
+
// cbor.build.* emitter. Two inputs, dispatched structurally: a DER X.509 certificate (Buffer/PEM) -> the
|
|
486
|
+
// FLAGSHIP type-3 forward transform (parse(encode(der)).reconstructedDer == der, so the original signature
|
|
487
|
+
// verifies); a c509.parse result -> re-emit its native array. Signing-free (mirrors ct.encodeSctList).
|
|
488
|
+
|
|
489
|
+
// The registry INVERSE tables -- the canonical int per name (the lossy forward map is resolved to ONE
|
|
490
|
+
// choice: EXT_BY_INT maps both 2 and 7 to keyUsage, so keyUsage encodes to the canonical draft int 2).
|
|
491
|
+
var SIG_ALG_TO_INT = { ecdsaWithSHA256: 0, ecdsaWithSHA384: 1, ecdsaWithSHA512: 2 };
|
|
492
|
+
var PK_ALG_TO_INT = { rsaEncryption: 0, "ecPublicKey|prime256v1": 1, "ecPublicKey|secp384r1": 2, "ecPublicKey|secp521r1": 3 };
|
|
493
|
+
var ATTR_TO_INT = { commonName: 1, surname: 2, serialNumber: 3, countryName: 4, localityName: 6, stateOrProvinceName: 7, organizationName: 8, organizationalUnitName: 9, title: 10 };
|
|
494
|
+
var EXT_TO_INT = { subjectKeyIdentifier: 1, keyUsage: 2, subjectAltName: 3, basicConstraints: 4, authorityKeyIdentifier: 10 };
|
|
495
|
+
|
|
496
|
+
// A non-negative BigInt -> its minimal big-endian ~biguint bytes (the leading 0x00 sign octet omitted).
|
|
497
|
+
function _minBytes(n) {
|
|
498
|
+
if (n < 0n) throw _err("c509/bad-serial", "a ~biguint value must be non-negative");
|
|
499
|
+
if (n === 0n) return Buffer.alloc(0);
|
|
500
|
+
var hex = n.toString(16); if (hex.length % 2) hex = "0" + hex;
|
|
501
|
+
return Buffer.from(hex, "hex");
|
|
502
|
+
}
|
|
503
|
+
// A C509 AlgorithmIdentifier -> int (registry) | ~oid (bare bytes) | [~oid, params]. `key` selects the row.
|
|
504
|
+
function _encAlgorithm(alg, toInt, key) {
|
|
505
|
+
var i = toInt[key];
|
|
506
|
+
if (i !== undefined && !(alg.parameters && alg.parameters.length)) return cbor.build.int(BigInt(i));
|
|
507
|
+
var oidBytes = cbor.build.byteString(asn1.encodeOidContent(alg.oid)); // ~oid: bare BER OID content
|
|
508
|
+
if (alg.parameters && alg.parameters.length) return cbor.build.array([oidBytes, cbor.build.byteString(alg.parameters)]);
|
|
509
|
+
return oidBytes;
|
|
510
|
+
}
|
|
511
|
+
// A SpecialText attribute value -> CBOR (text | tag-48 EUI). v1 encodes the text + eui64 forms.
|
|
512
|
+
function _encSpecialText(rdn) {
|
|
513
|
+
if (rdn.eui64) return cbor.build.tag(48, cbor.build.byteString(rdn.eui64));
|
|
514
|
+
return cbor.build.textString(String(rdn.value));
|
|
515
|
+
}
|
|
516
|
+
// A Name -> CBOR: null (issuer only) | a bare SpecialText single utf8 commonName | an array of RDN pairs.
|
|
517
|
+
function _encName(name, isSubject) {
|
|
518
|
+
if (name === null || name === undefined) {
|
|
519
|
+
if (!isSubject) return cbor.build.nullValue(); // issuer == subject (self-signed)
|
|
520
|
+
throw _err("c509/bad-name", "the subject Name is required");
|
|
521
|
+
}
|
|
522
|
+
var rdns = name.rdns || [];
|
|
523
|
+
if (rdns.length === 1 && rdns[0].type === "commonName" && !rdns[0].printable) return _encSpecialText(rdns[0]);
|
|
524
|
+
var items = [];
|
|
525
|
+
rdns.forEach(function (rdn) {
|
|
526
|
+
var ai = ATTR_TO_INT[rdn.type];
|
|
527
|
+
if (ai === undefined) throw _err("c509/bad-name", "attribute type " + rdn.type + " has no C509 registry int");
|
|
528
|
+
items.push(cbor.build.int(BigInt(rdn.printable ? -ai : ai))); // sign selects printableString
|
|
529
|
+
items.push(_encSpecialText(rdn));
|
|
530
|
+
});
|
|
531
|
+
return cbor.build.array(items);
|
|
532
|
+
}
|
|
533
|
+
// subjectPublicKey -> CBOR: EC point byte string, or an RSA ~biguint modulus ([modulus, exponent] when e != 65537).
|
|
534
|
+
function _encSpk(r) {
|
|
535
|
+
if (r.rsaPublicKey) {
|
|
536
|
+
var mod = cbor.build.byteString(_minBytes(r.rsaPublicKey.modulus));
|
|
537
|
+
if (r.rsaPublicKey.exponent === 65537n) return mod;
|
|
538
|
+
return cbor.build.array([mod, cbor.build.byteString(_minBytes(r.rsaPublicKey.exponent))]);
|
|
539
|
+
}
|
|
540
|
+
if (!Buffer.isBuffer(r.subjectPublicKey)) throw _err("c509/bad-spki", "the subjectPublicKey bytes are missing");
|
|
541
|
+
return cbor.build.byteString(r.subjectPublicKey);
|
|
542
|
+
}
|
|
543
|
+
// extensions -> CBOR: the keyUsage int-shortcut (a lone keyUsage), else an array of [extID, extValue] pairs.
|
|
544
|
+
function _encExtensions(exts) {
|
|
545
|
+
if (exts.length === 1 && exts[0].name === "keyUsage" && typeof exts[0].keyUsageBits === "number") {
|
|
546
|
+
return cbor.build.int(BigInt(exts[0].critical ? -exts[0].keyUsageBits : exts[0].keyUsageBits));
|
|
547
|
+
}
|
|
548
|
+
var items = [];
|
|
549
|
+
exts.forEach(function (ext) {
|
|
550
|
+
var ei = EXT_TO_INT[ext.name];
|
|
551
|
+
if (ei !== undefined) {
|
|
552
|
+
items.push(cbor.build.int(BigInt(ext.critical ? -ei : ei)));
|
|
553
|
+
// a registered-int extension carries its extnValue DER bytes as a bare byte string.
|
|
554
|
+
if (!Buffer.isBuffer(ext.value)) throw _err("c509/non-invertible", "extension " + ext.name + " has no byte-string value to encode");
|
|
555
|
+
items.push(cbor.build.byteString(ext.value));
|
|
556
|
+
} else {
|
|
557
|
+
items.push(cbor.build.byteString(asn1.encodeOidContent(ext.oid))); // ~oid extension id
|
|
558
|
+
if (!Buffer.isBuffer(ext.value)) throw _err("c509/non-invertible", "extension " + (ext.oid || ext.name) + " has no byte-string value to encode");
|
|
559
|
+
var bs = cbor.build.byteString(ext.value);
|
|
560
|
+
items.push(ext.critical ? cbor.build.array([bs]) : bs); // critical ~oid value wraps in a 1-element array
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
return cbor.build.array(items);
|
|
564
|
+
}
|
|
565
|
+
// forward-declared below; the DER X.509 -> type-3 C509 structured result.
|
|
566
|
+
var _derToType3;
|
|
567
|
+
// A hand-built (no _fieldBytes) result must carry the structured fields the encode below reads; a missing or
|
|
568
|
+
// wrong-typed field fails closed with a typed verdict rather than a raw property-access crash.
|
|
569
|
+
function _requireResultShape(r) {
|
|
570
|
+
if (r.certificateType == null) throw _err("c509/bad-input", "a C509 result must carry certificateType");
|
|
571
|
+
if (r.serialNumber == null && r.serialNumberHex == null) throw _err("c509/bad-input", "a C509 result must carry serialNumber or serialNumberHex");
|
|
572
|
+
if (!r.signatureAlgorithm || typeof r.signatureAlgorithm.name !== "string") throw _err("c509/bad-input", "a C509 result must carry signatureAlgorithm.name");
|
|
573
|
+
if (!r.subjectPublicKeyAlgorithm || typeof r.subjectPublicKeyAlgorithm.name !== "string") throw _err("c509/bad-input", "a C509 result must carry subjectPublicKeyAlgorithm.name");
|
|
574
|
+
if (!r.validity || !(r.validity.notBefore instanceof Date) || (r.validity.notAfter !== null && !(r.validity.notAfter instanceof Date))) throw _err("c509/bad-input", "a C509 result must carry validity.notBefore (Date) and notAfter (Date or null)");
|
|
575
|
+
if (!Array.isArray(r.extensions)) throw _err("c509/bad-input", "a C509 result must carry an extensions array");
|
|
576
|
+
if (!Buffer.isBuffer(r.signatureValue)) throw _err("c509/bad-input", "a C509 result must carry a Buffer signatureValue");
|
|
577
|
+
}
|
|
578
|
+
// A validity Date -> its C509 ~time (a non-negative CBOR epoch uint). A pre-epoch date cannot be
|
|
579
|
+
// represented (the parser accepts only an unwrapped major-type-0 integer) and fails closed here.
|
|
580
|
+
function _validityUint(date, label) {
|
|
581
|
+
var secs = Math.floor(date.getTime() / 1000);
|
|
582
|
+
if (!isFinite(secs) || secs < 0) throw _err("c509/bad-validity", label + " is before the Unix epoch or not a valid date; C509 ~time is a non-negative CBOR epoch");
|
|
583
|
+
return cbor.build.uint(BigInt(secs));
|
|
584
|
+
}
|
|
585
|
+
// A structured C509 result -> the 11-element deterministic-CBOR array.
|
|
586
|
+
function _encodeC509Array(r) {
|
|
587
|
+
// Re-emit a PARSED certificate's raw fields (elements 0..9) VERBATIM -- re-deriving from the decoded
|
|
588
|
+
// values could differ on a canonical-equivalent form (a byte-string attribute value, a registry alias)
|
|
589
|
+
// and break a type-2 native signature (which covers these bytes) or a type-3 DER reconstruction (which
|
|
590
|
+
// depends on the field values). Both types preserve the exact bytes; only a hand-built result (no
|
|
591
|
+
// _fieldBytes) re-derives from the structured values below.
|
|
592
|
+
if (Buffer.isBuffer(r._fieldBytes)) {
|
|
593
|
+
if (!Buffer.isBuffer(r.signatureValue)) throw _err("c509/bad-input", "a re-emitted certificate must carry a Buffer signatureValue");
|
|
594
|
+
var out = Buffer.concat([Buffer.from([0x8b]), r._fieldBytes, cbor.build.byteString(r.signatureValue)]); // array(11) head + fields 0..9 + signatureValue
|
|
595
|
+
parse(out); // fail closed: a caller-mutated _fieldBytes must still re-parse as a valid C509, else parse throws a typed c509/* verdict
|
|
596
|
+
return out;
|
|
597
|
+
}
|
|
598
|
+
_requireResultShape(r);
|
|
599
|
+
var pkKey = r.subjectPublicKeyAlgorithm.curve ? r.subjectPublicKeyAlgorithm.name + "|" + r.subjectPublicKeyAlgorithm.curve : r.subjectPublicKeyAlgorithm.name;
|
|
600
|
+
var arr = cbor.build.array([
|
|
601
|
+
cbor.build.int(BigInt(r.certificateType)),
|
|
602
|
+
cbor.build.byteString(r.serialNumberHex != null ? Buffer.from(r.serialNumberHex, "hex") : _minBytes(r.serialNumber)),
|
|
603
|
+
_encAlgorithm(r.signatureAlgorithm, SIG_ALG_TO_INT, r.signatureAlgorithm.name),
|
|
604
|
+
_encName(r.issuer, false),
|
|
605
|
+
_validityUint(r.validity.notBefore, "validityNotBefore"),
|
|
606
|
+
r.validity.notAfter === null ? cbor.build.nullValue() : _validityUint(r.validity.notAfter, "validityNotAfter"),
|
|
607
|
+
_encName(r.subject, true),
|
|
608
|
+
_encAlgorithm(r.subjectPublicKeyAlgorithm, PK_ALG_TO_INT, pkKey),
|
|
609
|
+
_encSpk(r),
|
|
610
|
+
_encExtensions(r.extensions),
|
|
611
|
+
cbor.build.byteString(r.signatureValue),
|
|
612
|
+
]);
|
|
613
|
+
parse(arr); // fail closed: a hand-built result must re-parse as a valid C509 (as the verbatim path checks), else parse throws a typed c509/* verdict
|
|
614
|
+
return arr;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* @primitive pki.schema.c509.encode
|
|
619
|
+
* @signature pki.schema.c509.encode(input[, opts]) -> Buffer
|
|
620
|
+
* @since 0.3.4
|
|
621
|
+
* @status experimental
|
|
622
|
+
* @spec draft-ietf-cose-cbor-encoded-cert, RFC 8949, RFC 9090
|
|
623
|
+
* @related pki.schema.c509.parse
|
|
624
|
+
*
|
|
625
|
+
* Encode a C509 certificate to its deterministic-CBOR bytes -- the producing-side inverse of
|
|
626
|
+
* `pki.schema.c509.parse`. `input` is either a DER X.509 v3 certificate (a Buffer or PEM string), which is
|
|
627
|
+
* forward-transformed to a **type-3** C509 (a compact CBOR re-encoding whose signature is copied from the
|
|
628
|
+
* source and re-expressed as a fixed-width r||s, so `parse(encode(der)).reconstructedDer` reproduces the
|
|
629
|
+
* original DER byte for byte and the original signature still verifies), or a `pki.schema.c509.parse`
|
|
630
|
+
* result object, which is re-emitted to its native deterministic-CBOR array. The emission is canonical
|
|
631
|
+
* deterministic CBOR (RFC 8949 sec. 4.2) -- shortest-form heads, definite lengths, sorted map keys, and the
|
|
632
|
+
* registry integer shorthand for every registered algorithm / attribute / extension. It is signing-free (a
|
|
633
|
+
* byte transform, like `pki.ct.encodeSctList`); a shape outside the covered set throws a typed `C509Error`.
|
|
634
|
+
*
|
|
635
|
+
* The fixed-width ECDSA r||s is sized by the ISSUER's signing curve, which a leaf certificate does not
|
|
636
|
+
* carry. It is resolved authoritatively (never a magnitude guess, and matching issuer/subject Names are not
|
|
637
|
+
* taken as proof of self-signing): from `opts.issuerCurve`, or from the RFC 5480 standard digest<->curve
|
|
638
|
+
* pairing the signature algorithm implies. A certificate signed with a non-standard digest/curve pairing
|
|
639
|
+
* (its r/s wider than the digest's standard curve) fails closed -- supply the issuer curve via
|
|
640
|
+
* `opts.issuerCurve`.
|
|
641
|
+
*
|
|
642
|
+
* @opts
|
|
643
|
+
* - `issuerCurve` (string) -- the ISSUER's ECDSA signing curve "P-256" / "P-384" / "P-521" (or the OID
|
|
644
|
+
* names prime256v1 / secp384r1 / secp521r1); authoritative, overrides the resolution above. Consulted
|
|
645
|
+
* only for the DER -> type-3 path; ignored when re-emitting a parse result.
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* var cbor = pki.schema.c509.encode(signerCertDer); // a DER cert -> a compact type-3 C509
|
|
649
|
+
* pki.schema.c509.parse(cbor).certificateType; // 3
|
|
650
|
+
*/
|
|
651
|
+
// A commonName string that is a MAC / EUI address ("HH-HH-HH-FF-FE-HH-HH-HH") -> the C509 tag-48 byte
|
|
652
|
+
// value (draft-20 sec. 3.2.3): an FF-FE-in-the-middle EUI-64 collapses to its 6-byte EUI-48, else the
|
|
653
|
+
// 8-byte EUI-64 verbatim. A non-MAC commonName returns null (encoded as text). The exact inverse of
|
|
654
|
+
// _macToEui64String, so the reconstruction rebuilds the identical DER commonName string.
|
|
655
|
+
function _euiFromCn(value) {
|
|
656
|
+
if (!/^[0-9A-F]{2}(-[0-9A-F]{2}){7}$/.test(value)) return null;
|
|
657
|
+
var bytes = Buffer.from(value.replace(/-/g, ""), "hex");
|
|
658
|
+
if (bytes[3] === 0xff && bytes[4] === 0xfe) return Buffer.concat([bytes.subarray(0, 3), bytes.subarray(5)]);
|
|
659
|
+
return bytes;
|
|
660
|
+
}
|
|
661
|
+
// A DER Name -> the C509 structured rdns, decoding each attribute's string type (PrintableString ->
|
|
662
|
+
// printable, UTF8String -> utf8; the C509 int sign carries this). Single-attribute RDNs only (v1).
|
|
663
|
+
function _c509NameFromDer(nameBytes) {
|
|
664
|
+
var node = asn1.decode(nameBytes);
|
|
665
|
+
var rdns = [];
|
|
666
|
+
(node.children || []).forEach(function (rdnSet) {
|
|
667
|
+
if (!rdnSet.children || rdnSet.children.length !== 1) throw _err("c509/non-invertible", "a C509 Name requires single-attribute RDNs");
|
|
668
|
+
var attr = rdnSet.children[0];
|
|
669
|
+
var attrName = oid.name(asn1.read.oid(attr.children[0]));
|
|
670
|
+
if (attrName == null || ATTR_TO_INT[attrName] === undefined) throw _err("c509/non-invertible", "attribute type " + attrName + " has no C509 registry integer");
|
|
671
|
+
var valNode = attr.children[1];
|
|
672
|
+
var value = asn1.read.string(valNode);
|
|
673
|
+
var eui = attrName === "commonName" ? _euiFromCn(value) : null;
|
|
674
|
+
if (eui) rdns.push({ type: attrName, value: value, eui64: eui }); // tag-48 MAC commonName
|
|
675
|
+
else rdns.push({ type: attrName, value: value, printable: valNode.tagNumber === asn1.TAGS.PRINTABLE_STRING });
|
|
676
|
+
});
|
|
677
|
+
return { rdns: rdns };
|
|
678
|
+
}
|
|
679
|
+
// A keyUsage extnValue (a DER KeyUsage BIT STRING) -> the C509 integer whose bit i is the named bit i.
|
|
680
|
+
function _keyUsageBitsFromDer(extnValue) {
|
|
681
|
+
var bs;
|
|
682
|
+
try {
|
|
683
|
+
bs = asn1.read.bitString(asn1.decode(extnValue));
|
|
684
|
+
} catch (_e) {
|
|
685
|
+
return null; // a malformed keyUsage BIT STRING cannot take the int shortcut -> encode it as a raw extension
|
|
686
|
+
}
|
|
687
|
+
var total = bs.bytes.length * 8 - bs.unusedBits, value = 0;
|
|
688
|
+
for (var bit = 0; bit < total && bit < 31; bit++) { if (bs.bytes[bit >> 3] & (0x80 >> (bit & 7))) value |= (1 << bit); }
|
|
689
|
+
return value > 0 && value <= 0x1ff ? value : null;
|
|
690
|
+
}
|
|
691
|
+
var _NO_EXPIRY = Date.UTC(9999, 11, 31, 23, 59, 59);
|
|
692
|
+
// The C509 (OID/node) curve names <-> the WebCrypto namedCurve the P1363 converter expects, and the
|
|
693
|
+
// RFC 5480 standard digest<->curve pairing an ECDSA signature algorithm implies.
|
|
694
|
+
var NODE_TO_WEBCRYPTO = { "prime256v1": "P-256", "secp384r1": "P-384", "secp521r1": "P-521" };
|
|
695
|
+
var WEBCRYPTO_FIELD_BYTES = { "P-256": 32, "P-384": 48, "P-521": 66 };
|
|
696
|
+
var SIG_ALG_TO_CURVE = { "ecdsaWithSHA256": "P-256", "ecdsaWithSHA384": "P-384", "ecdsaWithSHA512": "P-521" };
|
|
697
|
+
// A DER INTEGER magnitude byte length: the content past any leading sign/pad octet.
|
|
698
|
+
function _magBytes(intNode) {
|
|
699
|
+
var c = intNode.content, i = 0;
|
|
700
|
+
while (i < c.length - 1 && c[i] === 0x00) i++;
|
|
701
|
+
return c.length - i;
|
|
702
|
+
}
|
|
703
|
+
// The max r/s magnitude width of a DER ECDSA signature (validating its two-INTEGER shape).
|
|
704
|
+
function _sigMagWidth(derSig) {
|
|
705
|
+
var n;
|
|
706
|
+
try { n = asn1.decode(derSig); } catch (e) { throw _err("c509/bad-signature", "the ECDSA issuer signature is not valid DER", e); }
|
|
707
|
+
if (n.tagNumber !== asn1.TAGS.SEQUENCE || n.tagClass !== "universal" || !n.children || n.children.length !== 2) throw _err("c509/bad-signature", "the ECDSA issuer signature must be a SEQUENCE of two INTEGERs");
|
|
708
|
+
for (var i = 0; i < 2; i++) {
|
|
709
|
+
var ch = n.children[i];
|
|
710
|
+
if (ch.tagNumber !== asn1.TAGS.INTEGER || ch.tagClass !== "universal" || ch.constructed) throw _err("c509/bad-signature", "the ECDSA issuer signature r and s must be universal INTEGERs");
|
|
711
|
+
}
|
|
712
|
+
return Math.max(_magBytes(n.children[0]), _magBytes(n.children[1]));
|
|
713
|
+
}
|
|
714
|
+
var CURVE_ORDER = ["P-256", "P-384", "P-521"]; // ascending field size
|
|
715
|
+
// The smallest supported curve whose field holds a magnitude, or null if none does.
|
|
716
|
+
function _minCurveForMag(mag) {
|
|
717
|
+
for (var i = 0; i < CURVE_ORDER.length; i++) { if (mag <= WEBCRYPTO_FIELD_BYTES[CURVE_ORDER[i]]) return CURVE_ORDER[i]; }
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
// The ISSUER signature's fixed-width r||s is sized by the ISSUER's signing curve. The DER ECDSA signature
|
|
721
|
+
// does NOT carry the curve and the r/s magnitudes are only a lower bound (a P-384 signature with small r/s
|
|
722
|
+
// is byte-indistinguishable from a P-256 one), so the curve is resolved from an AUTHORITATIVE source, never
|
|
723
|
+
// a guess: (1) an explicit opts.issuerCurve, or (2) the RFC 5480 standard digest<->curve pairing the
|
|
724
|
+
// signature algorithm implies -- and ONLY when that pairing is the smallest curve the magnitude admits.
|
|
725
|
+
// Matching issuer/subject Names are NOT treated as proof of self-signing (a self-issued certificate may be
|
|
726
|
+
// cross-signed by a different key on a different curve). A signature whose r/s exceed the digest's standard
|
|
727
|
+
// curve (a larger key) OR whose magnitude also fits a SMALLER curve (a smaller key signing with a larger
|
|
728
|
+
// digest) leaves the curve undetermined: it fails closed and directs the caller to opts.issuerCurve.
|
|
729
|
+
function _resolveIssuerSigCurve(c, opts) {
|
|
730
|
+
var mag = _sigMagWidth(c.signatureValue.bytes), curve;
|
|
731
|
+
if (opts && opts.issuerCurve != null) {
|
|
732
|
+
curve = String(opts.issuerCurve);
|
|
733
|
+
if (NODE_TO_WEBCRYPTO[curve]) curve = NODE_TO_WEBCRYPTO[curve];
|
|
734
|
+
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);
|
|
735
|
+
} else {
|
|
736
|
+
curve = SIG_ALG_TO_CURVE[c.signatureAlgorithm.name];
|
|
737
|
+
if (!curve) throw _err("c509/non-invertible", "cannot resolve the issuer signing curve for signature algorithm " + c.signatureAlgorithm.name);
|
|
738
|
+
var minCurve = _minCurveForMag(mag);
|
|
739
|
+
if (!minCurve) throw _err("c509/non-invertible", "the ECDSA signature r/s width " + mag + " exceeds every supported curve field");
|
|
740
|
+
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 " + c.signatureAlgorithm.name + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
|
|
741
|
+
if (WEBCRYPTO_FIELD_BYTES[curve] > WEBCRYPTO_FIELD_BYTES[minCurve]) throw _err("c509/non-invertible", "signature algorithm " + c.signatureAlgorithm.name + " does not uniquely determine the issuer curve (r/s width " + mag + " also fits " + minCurve + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
|
|
742
|
+
}
|
|
743
|
+
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");
|
|
744
|
+
return curve;
|
|
745
|
+
}
|
|
746
|
+
// Compress an uncompressed SEC1 EC point (0x04||X||Y) to the C509 marker form (draft-20 sec. 3.2.2): the
|
|
747
|
+
// sign-of-Y marker 0xFE (Y even) / 0xFD (Y odd) followed by X. The inverse of webcrypto.decompressEcPoint,
|
|
748
|
+
// so the type-3 reconstruction recovers the exact original point. A non-0x04 point is kept verbatim.
|
|
749
|
+
function _compressEcPoint(point, coordLen) {
|
|
750
|
+
if (!point.length || point[0] !== 0x04) return point;
|
|
751
|
+
if (point.length !== 1 + 2 * coordLen) throw _err("c509/non-invertible", "uncompressed EC point length " + point.length + " does not match the curve field size");
|
|
752
|
+
var x = point.subarray(1, 1 + coordLen), y = point.subarray(1 + coordLen);
|
|
753
|
+
return Buffer.concat([Buffer.from([(y[y.length - 1] & 1) ? 0xfd : 0xfe]), x]);
|
|
754
|
+
}
|
|
755
|
+
// A DER X.509 v3 certificate -> the type-3 C509 structured result (the inverse of _reconstructDer). Only
|
|
756
|
+
// the reconstruction's covered set is invertible; encode() self-verifies the byte-exact round trip.
|
|
757
|
+
_derToType3 = function (input, opts) {
|
|
758
|
+
var c;
|
|
759
|
+
try { c = x509.parse(input); } catch (e) { throw _err("c509/bad-input", "the input is not a valid X.509 certificate", e); }
|
|
760
|
+
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"));
|
|
761
|
+
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"));
|
|
762
|
+
var curveOid = asn1.read.oid(asn1.decode(c.subjectPublicKeyInfo.algorithm.parameters));
|
|
763
|
+
var curve = oid.name(curveOid);
|
|
764
|
+
var coordLen = EC_FIELD_BYTES[curve];
|
|
765
|
+
if (!coordLen) throw _err("c509/non-invertible", "unsupported EC subject curve " + (curve || curveOid));
|
|
766
|
+
var sigCurve = _resolveIssuerSigCurve(c, opts); // the ISSUER signing curve, resolved authoritatively (opts / digest pairing)
|
|
767
|
+
var spkiNode = asn1.decode(c.subjectPublicKeyInfo.bytes);
|
|
768
|
+
return {
|
|
769
|
+
certificateType: 3,
|
|
770
|
+
serialNumber: c.serialNumber, // no serialNumberHex -> the encoder uses the minimal ~biguint magnitude
|
|
771
|
+
signatureAlgorithm: { name: c.signatureAlgorithm.name, oid: c.signatureAlgorithm.oid },
|
|
772
|
+
issuer: _c509NameFromDer(c.issuer.bytes),
|
|
773
|
+
validity: { notBefore: c.validity.notBefore, notAfter: c.validity.notAfter.getTime() === _NO_EXPIRY ? null : c.validity.notAfter },
|
|
774
|
+
subject: _c509NameFromDer(c.subject.bytes),
|
|
775
|
+
subjectPublicKeyAlgorithm: { name: "ecPublicKey", oid: c.subjectPublicKeyInfo.algorithm.oid, curve: curve },
|
|
776
|
+
subjectPublicKey: _compressEcPoint(asn1.read.bitString(spkiNode.children[1]).bytes, coordLen), // 0x04||X||Y -> C509 compressed marker
|
|
777
|
+
rsaPublicKey: null,
|
|
778
|
+
extensions: (c.extensions || []).map(function (e) {
|
|
779
|
+
var ext = { name: e.name, oid: e.oid, critical: !!e.critical, value: e.value };
|
|
780
|
+
if (e.name === "keyUsage") { var bits = _keyUsageBitsFromDer(e.value); if (bits != null) ext.keyUsageBits = bits; } // enable the int-shortcut
|
|
781
|
+
return ext;
|
|
782
|
+
}),
|
|
783
|
+
signatureValue: validator.sig.ecdsaDerToP1363(c.signatureValue.bytes, sigCurve, C509Error, "c509/bad-signature"),
|
|
784
|
+
};
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
function encode(input, opts) {
|
|
788
|
+
if (input && typeof input === "object" && !Buffer.isBuffer(input) && input.certificateType != null) {
|
|
789
|
+
return _encodeC509Array(input); // a parse result -> re-emit its native array
|
|
790
|
+
}
|
|
791
|
+
if (!Buffer.isBuffer(input) && typeof input !== "string") throw _err("c509/bad-input", "encode input must be a DER/PEM X.509 certificate or a c509.parse result");
|
|
792
|
+
// Normalize input to DER through the SAME shared coercion x509.parse uses (a PEM string, a PEM-armored
|
|
793
|
+
// Buffer, or raw DER all collapse to the certificate DER), so the self-verify compares against exactly the
|
|
794
|
+
// bytes _derToType3 processed -- never a Buffer of PEM text.
|
|
795
|
+
var origDer = pkix.coerceToDer(input, { pemLabel: "CERTIFICATE", PemError: frameworkError.PemError, ErrorClass: C509Error, prefix: "c509" });
|
|
796
|
+
var encoded = _encodeC509Array(_derToType3(origDer, opts));
|
|
797
|
+
// The type-3 transform MUST invert back to the original DER byte-for-byte (so the original signature
|
|
798
|
+
// verifies) -- self-verify it, failing closed on any edge the reconstruction cannot reproduce.
|
|
799
|
+
var recon = parse(encoded).reconstructedDer;
|
|
800
|
+
if (!recon || Buffer.compare(recon, origDer) !== 0) throw _err("c509/non-invertible", "the type-3 C509 does not reconstruct the source certificate byte-for-byte");
|
|
801
|
+
return encoded;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
module.exports = { parse: parse, matches: matches, encode: encode };
|
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:4cfd38cc-6772-42e6-bed2-46e04c2a950e",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-17T17:26:06.040Z",
|
|
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.3.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.3.4",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.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.3.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.3.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.3.
|
|
57
|
+
"ref": "@blamejs/pki@0.3.4",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|