@blamejs/pki 0.3.3 → 0.3.5
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 +18 -0
- package/README.md +2 -1
- package/index.js +5 -0
- package/lib/cbor-det.js +103 -0
- package/lib/cmp-build.js +490 -0
- package/lib/crmf-sign.js +28 -3
- 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,24 @@ 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.5 — 2026-07-17
|
|
8
|
+
|
|
9
|
+
pki.cmp.build assembles protected RFC 9810 CMP PKIMessages -- certificate requests, confirmations, revocations, and general messages, protected by a sender-key signature or a PBMAC1 shared secret.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.cmp.build(message, opts) assembles a protected RFC 9810 CMP PKIMessage. message.header carries the sender / recipient GeneralNames (including the anonymous NULL-DN) and optional transaction metadata; message.body is a single-key arm -- ir / cr / kur (a CertReqMessages via pki.crmf.build), p10cr (a PKCS#10 CertificationRequest), certConf, pollReq, genm, or rr. Protection is exactly one of opts.{ key, cert } (a signature under the sender key, algorithm resolved from the certificate -- RSA / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite) or opts.mac (a PBMAC1 shared-secret HMAC, RFC 9481 / RFC 9579), computed over the exact ProtectedPart DER and self-verified before return. Returns DER, or a PEM CMP block with opts.pem; malformed input throws a typed CmpError. Message parsing remains pki.schema.cmp.parse.
|
|
14
|
+
- pki.crmf.buildCertTemplate(template) encodes a bare RFC 4211 CertTemplate (subject, public key, validity, requested extensions, version) to canonical DER -- the certTemplate interior of pki.crmf.build, exposed for the CMP rr revocation body whose certDetails names the certificate to revoke.
|
|
15
|
+
|
|
16
|
+
## v0.3.4 — 2026-07-17
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- 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.
|
|
23
|
+
- 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.
|
|
24
|
+
|
|
7
25
|
## v0.3.3 — 2026-07-17
|
|
8
26
|
|
|
9
27
|
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` |
|
|
@@ -226,6 +226,7 @@ is callable today; nothing below is a stub.
|
|
|
226
226
|
| `pki.csr` | PKCS#10 certification-request issuance (RFC 2986 / RFC 2985) — `sign(spec, key, opts)` builds and signs a `CertificationRequest`: a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER; may be empty), the public key being certified, an optional `extensionRequest` (requested v3 extensions — subject alternative names, key usage, extended key usage, basic constraints, certificate policies, subject key identifier, or an array of pre-encoded Extension DER — that a CA copies into the issued certificate), and an optional `challengePassword`. `key` (or `{ key }`) is the subject's own private key: the request is self-signed to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the request is returned (what `openssl req -verify` checks). The signature algorithm is resolved from the subject key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. Returns DER, or a PEM `CERTIFICATE REQUEST` with `opts.pem`; malformed input throws a typed `CsrError`. Parsing stays at `pki.schema.csr.parse` — `sign` |
|
|
227
227
|
| `pki.attrcert` | RFC 5755 attribute-certificate issuance — `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` public-key-certificate reference, a `fromCertificate` binding derived from a certificate, or an object digest), the validity window (GeneralizedTime), an optional serial (positive, ≤ 20 octets; randomly generated when omitted), the `attributes` (the privilege syntaxes — role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo — or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, or pre-encoded Extension DER, each with its RFC 5755 criticality). An attribute certificate is never self-signed — the `issuer` is the signing AA, supplied as `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the AA key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE CERTIFICATE` with `opts.pem`; malformed input throws a typed `AttrCertError`. Parsing stays at `pki.schema.attrcert.parse` — `sign` |
|
|
228
228
|
| `pki.crmf` | RFC 4211 certificate-request-message issuance — `build(spec, key, opts)` assembles a `CertReqMessages`: a `spec` of `certReqId` (default 0; the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested certificate fields (`subject`, `publicKey` — the SPKI DER of the key being certified — `validity`, requested `extensions`, an optional `version` 2), optional `controls` and `regInfo` (regToken / authenticator / utf8Pairs / oldCertID / protocolEncrKey, or pre-encoded `AttributeTypeAndValue` DER), and an optional `pop` selector. `key` (or `{ key }`) is the requester's private key — the message carries a `POPOSigningKey` proof of possession signed with the private half of `certTemplate.publicKey` (verified before the message is returned), exactly as a PKCS#10 CSR proves possession; a complete template signs the `CertRequest`, an incomplete one signs a `POPOSigningKeyInput`. The signature algorithm is resolved from the requested public key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. `key` is optional for a `raVerified` proof. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. Parsing stays at `pki.schema.crmf.parse` — `build` |
|
|
229
|
+
| `pki.cmp` | RFC 9810 Certificate Management Protocol message building — `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` / `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata (`transactionID`, `senderNonce` / `recipNonce`, `messageTime` as a GeneralizedTime, `senderKID` / `recipKID`, `freeText`, `generalInfo`); `message.body` is a single-key object naming the request or transaction arm — `ir` / `cr` / `kur` (a `CertReqMessages` spec delegated to `pki.crmf.build`), `p10cr` (a PKCS#10 `CertificationRequest`), `certConf`, `pollReq`, `genm`, or `rr`. Protection is exactly one of `opts.{ key, cert }` — a signature over the message under the sender key, the algorithm resolved from the signer certificate so RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch — or `opts.mac` — a PBMAC1 shared-secret HMAC (RFC 9481 / 9579, PBKDF2-derived). The protection covers the exact DER of the virtual `ProtectedPart` (the header and body) and is self-verified before the message is returned; the `protectionAlg` is derived, never caller-set, so the message the parser accepts is coherent by construction. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. Parsing stays at `pki.schema.cmp.parse` — `build` |
|
|
229
230
|
| `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. **Content encryption** (RFC 5652/5083/5084/9629): `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData — recipients auto-dispatch off the certificate key to key-transport (RSAES-OAEP; v1.5 never emitted), key-agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key-wrap, password (PBKDF2 + RFC 3211 PWRI-KEK), or the post-quantum ML-KEM KEMRecipientInfo (RFC 9629/9936) — one fresh content key wrapped for every recipient. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher / EFAIL / password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. **Compression** (RFC 3274): `compress(content, opts)` / `decompress(input, opts)` produce and consume a CompressedData (ZLIB, version 0, id-alg-zlibCompress); decompress bounds the uncompressed output at 16 MiB and stops before it is materialized, so a decompression bomb fails closed as `cms/decompress-too-large` — a size transform with no integrity/confidentiality (RFC 8551 §2.4.5). Fail-closed with typed `cms/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
|
|
230
231
|
| `pki.smime` | RFC 8551 S/MIME message assembly, verification, encryption, and compression over the CMS layer — `sign(content, signers, opts)` wraps a MIME entity as a signed S/MIME message in either form: `multipart/signed` (clear-signed — the content stays readable in any MUA, a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`) or `application/pkcs7-mime; smime-type=signed-data` (opaque — the whole entity is a base64 CMS SignedData). The signed bytes are the entity's RFC 8551 §3.1.1 canonical form (CRLF line endings); `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies and a tampered part fails. `encrypt(content, recipients, opts)` envelopes a MIME entity as an opaque `application/pkcs7-mime` message and `decrypt(message, keyMaterial, opts)` opens one — `smime-type=authEnveloped-data` (AES-GCM, confidentiality and integrity, the default) or `smime-type=enveloped-data` (AES-CBC, confidentiality only, so `decrypt` reports `authenticated: false`, the §3.3 no-integrity caveat); the `smime-type` is derived from the CMS body, not the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt` — any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA signer and any RSA-OAEP / ECDH / X25519 / X448 / AES-KW / PBKDF2 / ML-KEM recipient carries through (algorithm-agnostic). Like `cms.verify`, `verify` returns the per-signer cryptographic verdict plus the recovered content; chaining a signer to a trust anchor is the caller's `pki.path.validate` step. `compress(content, opts)` / `decompress(message, opts)` add the opaque `application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z` frame (RFC 8551 §3.6, RFC 3274) — a size transform with no integrity/confidentiality (§2.4.5), decompress bounded against a bomb; the recovered content, which may itself be signed or enveloped, is returned for the caller to re-verify. Bidirectionally interoperable with `openssl smime` / `openssl cms`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
|
|
231
232
|
| `pki.tsp` | RFC 3161 Time-Stamp Protocol — `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData (over `pki.cms.sign`) whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` (with optional accuracy / nonce / ordering), plus the RFC 3161 §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). `request` / `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), `response` / `parseResponse` the TimeStampResp a TSA returns — a granted status wrapping a token, or a rejection with PKIStatus and failure info, the §2.4.2 status↔token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and — with a trust anchor supplied — full certification-path validation of the TSA certificate at the token's `genTime`, returning `{ valid, genTime, serialNumber, tstInfo, … }` — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
|
package/index.js
CHANGED
|
@@ -44,6 +44,7 @@ var x509 = require("./lib/x509-sign");
|
|
|
44
44
|
var csr = require("./lib/csr-sign");
|
|
45
45
|
var attrcert = require("./lib/attrcert-sign");
|
|
46
46
|
var crmf = require("./lib/crmf-sign");
|
|
47
|
+
var cmp = require("./lib/cmp-build");
|
|
47
48
|
var merkle = require("./lib/merkle");
|
|
48
49
|
var shbs = require("./lib/shbs");
|
|
49
50
|
var hpke = require("./lib/hpke");
|
|
@@ -103,6 +104,10 @@ module.exports = {
|
|
|
103
104
|
// assembles a CertReqMessages (a CertTemplate + a signature proof of possession) for a CMP
|
|
104
105
|
// or EST enrollment. Parsing lives at pki.schema.crmf.parse.
|
|
105
106
|
crmf: crmf,
|
|
107
|
+
// `cmp` is the RFC 9810 Certificate Management Protocol producing side -- pki.cmp.build assembles a
|
|
108
|
+
// protected PKIMessage (a certificate request / confirmation / revocation / general message).
|
|
109
|
+
// Parsing lives at pki.schema.cmp.parse.
|
|
110
|
+
cmp: cmp,
|
|
106
111
|
// `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
|
|
107
112
|
// pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
|
|
108
113
|
// (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
|
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/cmp-build.js
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @module pki.cmp
|
|
7
|
+
* @nav Signing
|
|
8
|
+
* @title Certificate management protocol messages
|
|
9
|
+
* @intro The RFC 9810 Certificate Management Protocol producing side. `pki.cmp.build` assembles a
|
|
10
|
+
* `PKIMessage` -- a `PKIHeader` (sender / recipient / transaction metadata), a `PKIBody` carrying one
|
|
11
|
+
* request or transaction (an `ir` / `cr` / `kur` certificate request via `pki.crmf.build`, a `p10cr`
|
|
12
|
+
* PKCS#10 via `pki.csr.sign`, or a `certConf` / `pollReq` / `genm` / `rr`), and an optional
|
|
13
|
+
* `PKIProtection` computed over the message. Protection is a signature under the sender key (any
|
|
14
|
+
* registry algorithm) or a PBMAC1 shared-secret MAC. The emitted message round-trips through
|
|
15
|
+
* `pki.schema.cmp.parse` and its protection verifies. Parsing lives at `pki.schema.cmp.parse`.
|
|
16
|
+
* @spec RFC 9810
|
|
17
|
+
* @card Build a CMP PKIMessage with signature or PBMAC1 protection.
|
|
18
|
+
*/
|
|
19
|
+
//
|
|
20
|
+
// RFC 9810 Appendix A is DEFINITIONS EXPLICIT TAGS: every context tag in the PKIMessage envelope --
|
|
21
|
+
// each PKIHeader [0..8] optional, every PKIBody [n] arm, protection [0], extraCerts [1] -- is an EXPLICIT
|
|
22
|
+
// wrapper (asn1.build.explicit), the exact inverse of the RFC 4211 CRMF interior (IMPLICIT TAGS). The two
|
|
23
|
+
// IMPLICIT islands are foreign imports produced wholesale by pki.crmf.build (the CertReqMessages /
|
|
24
|
+
// CertTemplate interior) and embedded verbatim, never re-encoded here. Protection is computed over the
|
|
25
|
+
// byte-exact DER of the virtual ProtectedPart ::= SEQUENCE { header, body }, built from the same header
|
|
26
|
+
// and body TLVs placed in the envelope. The signature scheme resolves from the sender key through the
|
|
27
|
+
// shared sign-scheme registry; the Name / GeneralName / SPKI encoders and the post-sign self-check are the
|
|
28
|
+
// shared lib/pki-build primitives, bound to the cmp namespace.
|
|
29
|
+
|
|
30
|
+
var asn1 = require("./asn1-der");
|
|
31
|
+
var oid = require("./oid");
|
|
32
|
+
var cmp = require("./schema-cmp");
|
|
33
|
+
var crmf = require("./crmf-sign");
|
|
34
|
+
var csr = require("./schema-csr");
|
|
35
|
+
var x509 = require("./schema-x509");
|
|
36
|
+
var signScheme = require("./sign-scheme");
|
|
37
|
+
var pkix = require("./schema-pkix");
|
|
38
|
+
var pkiBuild = require("./pki-build");
|
|
39
|
+
var webcrypto = require("./webcrypto");
|
|
40
|
+
var constants = require("./constants");
|
|
41
|
+
var guard = require("./guard-all");
|
|
42
|
+
var frameworkError = require("./framework-error");
|
|
43
|
+
|
|
44
|
+
var CmpError = frameworkError.CmpError;
|
|
45
|
+
var b = asn1.build;
|
|
46
|
+
function _err(code, message, cause) { return new CmpError(code, message, cause); }
|
|
47
|
+
function _signE(kind, message, cause) { return new CmpError("cmp/" + kind, message, cause); }
|
|
48
|
+
function O(n) { return oid.byName(n); }
|
|
49
|
+
|
|
50
|
+
var NS = pkix.makeNS("cmp", CmpError, oid);
|
|
51
|
+
var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
|
|
52
|
+
var _b = pkiBuild.makeBuilder({
|
|
53
|
+
ErrorClass: CmpError, prefix: "cmp", O: O, NS: NS,
|
|
54
|
+
NAME_SCHEMA: pkix.name(NS), SPKI_SCHEMA: pkix.spki(NS), EXT_DECODERS: EXT_DECODERS,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
var KNOWN_HEADER_KEYS = {
|
|
58
|
+
sender: 1, recipient: 1, pvno: 1, messageTime: 1, senderKID: 1, recipKID: 1,
|
|
59
|
+
transactionID: 1, senderNonce: 1, recipNonce: 1, freeText: 1, generalInfo: 1,
|
|
60
|
+
};
|
|
61
|
+
var KNOWN_BODY_KEYS = { ir: 1, cr: 1, kur: 1, p10cr: 1, certConf: 1, pollReq: 1, genm: 1, rr: 1 };
|
|
62
|
+
// The PKIBody arm -> its EXPLICIT context tag number (schema-cmp.js BODY_ARMS). rr is [11], NOT [15].
|
|
63
|
+
var BODY_TAG = { ir: 0, cr: 2, p10cr: 4, kur: 7, rr: 11, genm: 21, certConf: 24, pollReq: 25 };
|
|
64
|
+
var CRMF_BODY = { ir: 1, cr: 1, kur: 1 }; // arms whose content is a CertReqMessages via pki.crmf.build
|
|
65
|
+
var KNOWN_OPTS_KEYS = { key: 1, cert: 1, mac: 1, extraCerts: 1, pem: 1, pss: 1, digestAlgorithm: 1 };
|
|
66
|
+
var KNOWN_MAC_KEYS = { secret: 1, salt: 1, iterationCount: 1, prf: 1, keyLength: 1, algorithm: 1 };
|
|
67
|
+
|
|
68
|
+
var PBMAC1_DEFAULT_ITER = 100000;
|
|
69
|
+
var PBMAC1_DEFAULT_SALT_BYTES = 16;
|
|
70
|
+
var PBMAC1_DEFAULT_KEYLEN = 32; // bytes -- HMAC-SHA256 key
|
|
71
|
+
var PBMAC1_MAX_KEYLEN = 1024; // bytes -- an HMAC key beyond a hash block is pointless; bound the work
|
|
72
|
+
var PBMAC1_MIN_ITER = 1000; // RFC 8018 sec. 4.2 recommended minimum -- reject a trivially weak count
|
|
73
|
+
var PBMAC1_PRF = { "SHA-256": "hmacWithSHA256", "SHA-384": "hmacWithSHA384", "SHA-512": "hmacWithSHA512" };
|
|
74
|
+
var PBMAC1_MAC_OID = { "SHA-256": "hmacWithSHA256", "SHA-384": "hmacWithSHA384", "SHA-512": "hmacWithSHA512" };
|
|
75
|
+
// PKIFailureInfo named bits (RFC 9810 sec. 5.2.3), position = bit index -- mirrors schema-cmp's decode list;
|
|
76
|
+
// the build -> parse round-trip cross-checks the positions against the parser.
|
|
77
|
+
var FAIL_INFO_NAMES = ["badAlg", "badMessageCheck", "badRequest", "badTime", "badCertId", "badDataFormat",
|
|
78
|
+
"wrongAuthority", "incorrectData", "missingTimeStamp", "badPOP", "certRevoked", "certConfirmed",
|
|
79
|
+
"wrongIntegrity", "badRecipientNonce", "timeNotAvailable", "unacceptedPolicy", "unacceptedExtension",
|
|
80
|
+
"addInfoNotAvailable", "badSenderNonce", "badCertTemplate", "signerNotTrusted", "transactionIdInUse",
|
|
81
|
+
"unsupportedVersion", "notAuthorized", "systemUnavail", "systemFailure", "duplicateCertReq"];
|
|
82
|
+
var FAIL_INFO_INDEX = {};
|
|
83
|
+
FAIL_INFO_NAMES.forEach(function (n, i) { FAIL_INFO_INDEX[n] = i; });
|
|
84
|
+
// CertStatus.hashAlg names the hash used to compute certHash -- restrict it to hash algorithms, not any OID.
|
|
85
|
+
var CERT_CONF_HASH_ALGS = { sha1: 1, sha256: 1, sha384: 1, sha512: 1, "sha3-256": 1, "sha3-512": 1 };
|
|
86
|
+
|
|
87
|
+
// ---- small shared encoders (byte-exact inverses of schema-cmp.js readers) ----
|
|
88
|
+
|
|
89
|
+
function _reqOctets(v, what) {
|
|
90
|
+
var buf = _b.reqDer(v, what);
|
|
91
|
+
return b.octetString(buf);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String -- non-empty, every element UTF8String.
|
|
95
|
+
function _encodePkiFreeText(strings, code, what) {
|
|
96
|
+
if (!Array.isArray(strings) || !strings.length) throw _err(code, what + " must be a non-empty array of strings");
|
|
97
|
+
return b.sequence(strings.map(function (s) {
|
|
98
|
+
if (typeof s !== "string") throw _err(code, what + " entries must be strings");
|
|
99
|
+
return b.utf8(s);
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A recognized fixed-syntax id-it value (RFC 9810 sec. 5.1.1.1/.2/.4): implicitConfirm -> NULL,
|
|
104
|
+
// confirmWaitTime -> GeneralizedTime, certProfile -> non-empty SEQUENCE OF UTF8String. An unknown id-it
|
|
105
|
+
// carries a pre-encoded infoValue DER (or none).
|
|
106
|
+
function _encodeInfoValue(itav) {
|
|
107
|
+
var name = itav.infoType;
|
|
108
|
+
if (name === "implicitConfirm") {
|
|
109
|
+
if (itav.infoValue != null) throw _err("cmp/bad-info-value", "implicitConfirm carries a NULL infoValue");
|
|
110
|
+
return b.nullValue();
|
|
111
|
+
}
|
|
112
|
+
if (name === "confirmWaitTime") {
|
|
113
|
+
guard.time.assertValid(itav.infoValue, _err, "cmp/bad-info-value", "confirmWaitTime infoValue"); // reject a non-Date / Invalid Date
|
|
114
|
+
return b.generalizedTime(itav.infoValue);
|
|
115
|
+
}
|
|
116
|
+
if (name === "certProfile") {
|
|
117
|
+
return _encodePkiFreeText(itav.infoValue, "cmp/bad-info-value", "certProfile");
|
|
118
|
+
}
|
|
119
|
+
if (itav.infoValue == null) return null;
|
|
120
|
+
return b.raw(_b.reqDer(itav.infoValue, "infoValue (a pre-encoded DER value)"));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// InfoTypeAndValue ::= SEQUENCE { infoType OID, infoValue ANY DEFINED BY infoType OPTIONAL }.
|
|
124
|
+
function _encodeInfoTypeAndValue(itav) {
|
|
125
|
+
if (!itav || typeof itav !== "object" || Buffer.isBuffer(itav)) throw _err("cmp/bad-info-value", "an InfoTypeAndValue must be an object { infoType, infoValue? }");
|
|
126
|
+
if (typeof itav.infoType !== "string" || O(itav.infoType) == null) throw _err("cmp/bad-name", "unknown infoType " + JSON.stringify(itav.infoType));
|
|
127
|
+
var children = [b.oid(O(itav.infoType))];
|
|
128
|
+
var val = _encodeInfoValue(itav);
|
|
129
|
+
if (val != null) children.push(val);
|
|
130
|
+
return b.sequence(children);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// generalInfo [8] / genm content: SEQUENCE SIZE (1..MAX) OF InfoTypeAndValue.
|
|
134
|
+
function _encodeGeneralInfo(itavs, code, what) {
|
|
135
|
+
if (!Array.isArray(itavs) || !itavs.length) throw _err(code, what + " must be a non-empty array of InfoTypeAndValue");
|
|
136
|
+
return b.sequence(itavs.map(_encodeInfoTypeAndValue));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---- PKIHeader ----
|
|
140
|
+
|
|
141
|
+
// An AlgorithmIdentifier { algorithm OID, parameters ANY OPTIONAL } for a bare/absent-params digest.
|
|
142
|
+
function _algIdNoParams(name) { return b.sequence([b.oid(O(name))]); }
|
|
143
|
+
// An HMAC AlgorithmIdentifier carries NULL parameters (RFC 8018 App. B.1.1 / RFC 4231), unlike a bare SHA-2
|
|
144
|
+
// digest algId (ABSENT parameters, RFC 5754 sec. 2). Used for the PBMAC1 prf + messageAuthScheme.
|
|
145
|
+
function _hmacAlgId(name) { return b.sequence([b.oid(O(name)), b.nullValue()]); }
|
|
146
|
+
|
|
147
|
+
// PKIHeader ::= SEQUENCE { pvno INTEGER, sender GeneralName, recipient GeneralName, EXPLICIT [0..8] }.
|
|
148
|
+
// protectionAlgDer is the DERIVED AlgorithmIdentifier ([1]), present iff the message is protected. Returns
|
|
149
|
+
// the exact headerTLV, built ONCE and reused in both the envelope and the ProtectedPart (RFC 9810 5.1.3).
|
|
150
|
+
function _encodeHeader(headerSpec, protectionAlgDer, pvno) {
|
|
151
|
+
if (!headerSpec || typeof headerSpec !== "object" || Buffer.isBuffer(headerSpec)) throw _err("cmp/bad-input", "message.header must be an object");
|
|
152
|
+
Object.keys(headerSpec).forEach(function (k) { if (!KNOWN_HEADER_KEYS[k]) throw _err("cmp/bad-input", "unknown header field " + JSON.stringify(k)); });
|
|
153
|
+
if (headerSpec.sender == null) throw _err("cmp/bad-input", "message.header.sender is required (GeneralName)");
|
|
154
|
+
if (headerSpec.recipient == null) throw _err("cmp/bad-input", "message.header.recipient is required (GeneralName)");
|
|
155
|
+
|
|
156
|
+
var children = [
|
|
157
|
+
b.integer(BigInt(pvno)),
|
|
158
|
+
_b.encodeGeneralName(headerSpec.sender),
|
|
159
|
+
_b.encodeGeneralName(headerSpec.recipient),
|
|
160
|
+
];
|
|
161
|
+
// EXPLICIT [0..8] optionals, ascending, at most once each.
|
|
162
|
+
if (headerSpec.messageTime != null) {
|
|
163
|
+
guard.time.assertValid(headerSpec.messageTime, _err, "cmp/bad-input", "header.messageTime"); // reject a non-Date / Invalid Date
|
|
164
|
+
children.push(b.explicit(0, b.generalizedTime(headerSpec.messageTime))); // GeneralizedTime ONLY (never UTCTime)
|
|
165
|
+
}
|
|
166
|
+
if (protectionAlgDer != null) children.push(b.explicit(1, b.raw(protectionAlgDer)));
|
|
167
|
+
if (headerSpec.senderKID != null) children.push(b.explicit(2, _reqOctets(headerSpec.senderKID, "header.senderKID")));
|
|
168
|
+
if (headerSpec.recipKID != null) children.push(b.explicit(3, _reqOctets(headerSpec.recipKID, "header.recipKID")));
|
|
169
|
+
if (headerSpec.transactionID != null) children.push(b.explicit(4, _reqOctets(headerSpec.transactionID, "header.transactionID")));
|
|
170
|
+
if (headerSpec.senderNonce != null) children.push(b.explicit(5, _reqOctets(headerSpec.senderNonce, "header.senderNonce")));
|
|
171
|
+
if (headerSpec.recipNonce != null) children.push(b.explicit(6, _reqOctets(headerSpec.recipNonce, "header.recipNonce")));
|
|
172
|
+
if (headerSpec.freeText != null) children.push(b.explicit(7, _encodePkiFreeText(headerSpec.freeText, "cmp/bad-freetext", "header.freeText")));
|
|
173
|
+
if (headerSpec.generalInfo != null) children.push(b.explicit(8, _encodeGeneralInfo(headerSpec.generalInfo, "cmp/bad-general-info", "header.generalInfo")));
|
|
174
|
+
return b.sequence(children);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---- PKIBody arm content encoders ----
|
|
178
|
+
|
|
179
|
+
// A certReqId is an UNBOUNDED INTEGER (RFC 9483 -1 sentinel, no upper bound): accept a safe-integer number
|
|
180
|
+
// or a bigint (a large value beyond 2^53), reject a non-integer / other type. Returns a BigInt for b.integer.
|
|
181
|
+
function _reqIdInt(v, code, what) {
|
|
182
|
+
if (typeof v === "bigint") return v;
|
|
183
|
+
// Number.isSafeInteger (not isInteger): a value above 2^53 is imprecise as a Number, so it MUST be a bigint.
|
|
184
|
+
if (typeof v === "number" && Number.isSafeInteger(v)) return BigInt(v);
|
|
185
|
+
throw _err(code, what + " must be an integer (a safe-integer number, or a bigint for a large value)");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// PKIFailureInfo ::= BIT STRING (named bits, RFC 9810 sec. 5.2.3) -- a minimal NamedBitList from bit names.
|
|
189
|
+
function _encodeFailInfo(names) {
|
|
190
|
+
if (!Array.isArray(names)) throw _err("cmp/bad-cert-status", "statusInfo.failInfo must be an array of PKIFailureInfo bit names");
|
|
191
|
+
return b.namedBitString(names.map(function (n) {
|
|
192
|
+
if (typeof n !== "string" || FAIL_INFO_INDEX[n] === undefined) throw _err("cmp/bad-cert-status", "unknown PKIFailureInfo bit " + JSON.stringify(n));
|
|
193
|
+
return FAIL_INFO_INDEX[n];
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// PKIStatusInfo ::= SEQUENCE { status PKIStatus INTEGER, statusString PKIFreeText OPTIONAL, failInfo BIT STRING OPTIONAL }.
|
|
198
|
+
function _encodePkiStatusInfo(si) {
|
|
199
|
+
if (typeof si.status !== "number" || !Number.isInteger(si.status)) throw _err("cmp/bad-cert-status", "statusInfo.status must be a PKIStatus integer");
|
|
200
|
+
var children = [b.integer(BigInt(si.status))];
|
|
201
|
+
if (si.statusString != null) children.push(_encodePkiFreeText(si.statusString, "cmp/bad-cert-status", "statusInfo.statusString"));
|
|
202
|
+
if (si.failInfo != null) children.push(_encodeFailInfo(si.failInfo)); // strict order: status -> statusString -> failInfo (both optionals untagged)
|
|
203
|
+
return b.sequence(children);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// CertStatus ::= SEQUENCE { certHash OCTET STRING, certReqId INTEGER, statusInfo PKIStatusInfo OPTIONAL,
|
|
207
|
+
// hashAlg [0] EXPLICIT AlgorithmIdentifier OPTIONAL }. hashAlg present => cmp2021 (pvno bump).
|
|
208
|
+
function _encodeCertStatus(cs, state) {
|
|
209
|
+
if (!cs || typeof cs !== "object" || Buffer.isBuffer(cs)) throw _err("cmp/bad-cert-status", "each CertStatus must be an object");
|
|
210
|
+
var children = [
|
|
211
|
+
_reqOctets(cs.certHash, "certConf certHash"),
|
|
212
|
+
b.integer(_reqIdInt(cs.certReqId, "cmp/bad-cert-status", "CertStatus certReqId")), // signed, -1 legal, unbounded
|
|
213
|
+
];
|
|
214
|
+
if (cs.statusInfo != null) children.push(_encodePkiStatusInfo(cs.statusInfo));
|
|
215
|
+
if (cs.hashAlg != null) {
|
|
216
|
+
if (typeof cs.hashAlg !== "string" || !CERT_CONF_HASH_ALGS[cs.hashAlg]) throw _err("cmp/bad-name", "certConf hashAlg must be a hash algorithm (sha256 / sha384 / sha512 / sha3-256 / sha3-512 / sha1); got " + JSON.stringify(cs.hashAlg));
|
|
217
|
+
children.push(b.explicit(0, _algIdNoParams(cs.hashAlg)));
|
|
218
|
+
state.usesCmp2021 = true;
|
|
219
|
+
}
|
|
220
|
+
return b.sequence(children);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// CertConfirmContent ::= SEQUENCE OF CertStatus (empty legal, no SIZE floor).
|
|
224
|
+
function _encodeCertConfirmContent(list, state) {
|
|
225
|
+
if (!Array.isArray(list)) throw _err("cmp/bad-cert-status", "certConf must be an array of CertStatus");
|
|
226
|
+
return b.sequence(list.map(function (cs) { return _encodeCertStatus(cs, state); }));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// PollReqContent ::= SEQUENCE OF SEQUENCE { certReqId INTEGER } (-1 legal).
|
|
230
|
+
function _encodePollReqContent(list) {
|
|
231
|
+
if (!Array.isArray(list)) throw _err("cmp/bad-poll-req", "pollReq must be an array of { certReqId }");
|
|
232
|
+
return b.sequence(list.map(function (pr) {
|
|
233
|
+
if (!pr || typeof pr !== "object" || Buffer.isBuffer(pr)) throw _err("cmp/bad-poll-req", "each pollReq entry must be { certReqId: <integer> }");
|
|
234
|
+
return b.sequence([b.integer(_reqIdInt(pr.certReqId, "cmp/bad-poll-req", "pollReq certReqId"))]);
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// GenMsgContent ::= SEQUENCE OF InfoTypeAndValue (empty legal).
|
|
239
|
+
function _encodeGenMsgContent(list) {
|
|
240
|
+
if (!Array.isArray(list)) throw _err("cmp/bad-info-type-and-value", "genm must be an array of InfoTypeAndValue");
|
|
241
|
+
return b.sequence(list.map(_encodeInfoTypeAndValue));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// RevReqContent ::= SEQUENCE OF RevDetails { certDetails CertTemplate, crlEntryDetails Extensions OPTIONAL }.
|
|
245
|
+
function _encodeRevDetails(rd) {
|
|
246
|
+
if (!rd || typeof rd !== "object" || Buffer.isBuffer(rd)) throw _err("cmp/bad-rev-req", "each RevDetails must be an object { certDetails, crlEntryDetails? }");
|
|
247
|
+
if (rd.certDetails == null) throw _err("cmp/bad-rev-req", "RevDetails.certDetails (a CertTemplate) is required");
|
|
248
|
+
var certDetails;
|
|
249
|
+
if (Buffer.isBuffer(rd.certDetails) || rd.certDetails instanceof Uint8Array) {
|
|
250
|
+
certDetails = b.raw(_b.reqDer(rd.certDetails, "RevDetails.certDetails (a pre-encoded CertTemplate DER)"));
|
|
251
|
+
} else {
|
|
252
|
+
// A revocation must identify the certificate to revoke by issuer + serialNumber (RFC 9810 App. D sec.
|
|
253
|
+
// 5.3.2); the pre-encoded-CertTemplate hatch above is the escape for an advanced identification.
|
|
254
|
+
if (rd.certDetails.issuer == null || rd.certDetails.serialNumber == null) throw _err("cmp/bad-rev-req", "a revocation certDetails must identify the certificate by issuer and serialNumber");
|
|
255
|
+
certDetails = b.raw(crmf.buildCertTemplate(rd.certDetails));
|
|
256
|
+
}
|
|
257
|
+
var children = [certDetails];
|
|
258
|
+
if (rd.crlEntryDetails != null) children.push(b.raw(_b.reqDer(rd.crlEntryDetails, "RevDetails.crlEntryDetails (a pre-encoded Extensions DER)")));
|
|
259
|
+
return b.sequence(children);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function _encodeRevReqContent(list) {
|
|
263
|
+
if (!Array.isArray(list) || !list.length) throw _err("cmp/bad-rev-req", "rr must be a non-empty array of RevDetails");
|
|
264
|
+
return b.sequence(list.map(_encodeRevDetails));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Dispatch the single-key body object to its EXPLICIT-tagged arm. Returns { bodyTLV, usesCmp2021 }.
|
|
268
|
+
function _encodeBody(bodySpec, key, opts) {
|
|
269
|
+
if (!bodySpec || typeof bodySpec !== "object" || Buffer.isBuffer(bodySpec)) throw _err("cmp/bad-input", "message.body must be a single-key object");
|
|
270
|
+
var keys = Object.keys(bodySpec);
|
|
271
|
+
if (keys.length !== 1) throw _err("cmp/bad-input", "message.body must have exactly one arm, got " + keys.length);
|
|
272
|
+
var arm = keys[0];
|
|
273
|
+
if (!KNOWN_BODY_KEYS[arm]) throw _err("cmp/bad-input", "unknown body arm " + JSON.stringify(arm));
|
|
274
|
+
var tag = BODY_TAG[arm], state = { usesCmp2021: false };
|
|
275
|
+
|
|
276
|
+
if (CRMF_BODY[arm]) {
|
|
277
|
+
// The CRMF proof of possession is signed with the REQUESTED key's private half, which is distinct from
|
|
278
|
+
// the message-protection key. Take it from a `key` field on the request spec; default to the protection
|
|
279
|
+
// key only for a self-request (the client certifying the very key it protects with).
|
|
280
|
+
var reqSpec = bodySpec[arm], popKey = key;
|
|
281
|
+
if (reqSpec && typeof reqSpec === "object" && !Buffer.isBuffer(reqSpec) && "key" in reqSpec) {
|
|
282
|
+
popKey = reqSpec.key;
|
|
283
|
+
reqSpec = Object.assign({}, reqSpec);
|
|
284
|
+
delete reqSpec.key;
|
|
285
|
+
}
|
|
286
|
+
return crmf.build(reqSpec, popKey, {}).then(function (crmfDer) {
|
|
287
|
+
return { bodyTLV: b.explicit(tag, b.raw(crmfDer)), usesCmp2021: false };
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
var inner;
|
|
291
|
+
if (arm === "p10cr") {
|
|
292
|
+
var csrDer = _b.reqDer(bodySpec.p10cr, "p10cr (a CertificationRequest DER)");
|
|
293
|
+
try { csr.parse(csrDer); } // validate the embedded PKCS#10 via the real parser (fail-closed on a non-CSR)
|
|
294
|
+
catch (e) { if (e instanceof CmpError) throw e; throw _err("cmp/bad-input", "p10cr is not a valid PKCS#10 CertificationRequest", e); }
|
|
295
|
+
inner = b.raw(csrDer);
|
|
296
|
+
} else if (arm === "certConf") {
|
|
297
|
+
inner = _encodeCertConfirmContent(bodySpec.certConf, state);
|
|
298
|
+
} else if (arm === "pollReq") {
|
|
299
|
+
inner = _encodePollReqContent(bodySpec.pollReq);
|
|
300
|
+
} else if (arm === "genm") {
|
|
301
|
+
inner = _encodeGenMsgContent(bodySpec.genm);
|
|
302
|
+
} else { // rr
|
|
303
|
+
inner = _encodeRevReqContent(bodySpec.rr);
|
|
304
|
+
}
|
|
305
|
+
return Promise.resolve({ bodyTLV: b.explicit(tag, inner), usesCmp2021: state.usesCmp2021 });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---- protection ----
|
|
309
|
+
|
|
310
|
+
// PBMAC1 protectionAlg: id-PBMAC1 with PBMAC1-params { keyDerivationFunc PBKDF2{salt,iter,keyLength,prf},
|
|
311
|
+
// messageAuthScheme HMAC } -- the byte-exact inverse of schema-pkcs12.js PBMAC1_PARAMS / PBKDF2_PARAMS.
|
|
312
|
+
function _encodePbmac1AlgId(pd) {
|
|
313
|
+
var pbkdf2Params = [b.octetString(pd.salt), b.integer(BigInt(pd.iterationCount)), b.integer(BigInt(pd.keyLength))];
|
|
314
|
+
// prf: omit iff the DEFAULT algid-hmacWithSHA1; else the prf AlgorithmIdentifier with ABSENT params.
|
|
315
|
+
// nosemgrep: pki-non-constant-time-secret-compare -- prfName is a PRF algorithm name, not a secret
|
|
316
|
+
if (pd.prfName !== "hmacWithSHA1") pbkdf2Params.push(_hmacAlgId(pd.prfName));
|
|
317
|
+
var pbkdf2AlgId = b.sequence([b.oid(O("pbkdf2")), b.sequence(pbkdf2Params)]);
|
|
318
|
+
var pbmac1Params = b.sequence([pbkdf2AlgId, _hmacAlgId(pd.macName)]);
|
|
319
|
+
return b.sequence([b.oid(O("pbmac1")), pbmac1Params]);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Resolve the protection selector to { protectionAlgDer, computeBits(protectedPartDer)->Promise<Buffer> },
|
|
323
|
+
// senderSpki, senderScheme } BEFORE the header is built (protectionAlg is opts-derived, not header-derived).
|
|
324
|
+
function _resolveProtection(opts) {
|
|
325
|
+
var hasSig = opts.key != null || opts.cert != null;
|
|
326
|
+
var hasMac = opts.mac != null;
|
|
327
|
+
if (hasSig && hasMac) throw _err("cmp/bad-input", "supply exactly one of { key, cert } (signature) or { mac } (PBMAC1), not both");
|
|
328
|
+
if (!hasSig && !hasMac) throw _err("cmp/bad-input", "a PKIMessage requires protection: supply { key, cert } for a signature or { mac } for PBMAC1");
|
|
329
|
+
|
|
330
|
+
if (hasSig) {
|
|
331
|
+
if (opts.key == null || opts.cert == null) throw _err("cmp/bad-input", "signature protection requires both opts.key (the private key) and opts.cert (the signer certificate)");
|
|
332
|
+
var certDer = _b.reqDer(opts.cert, "opts.cert (the signer certificate DER)");
|
|
333
|
+
var senderSpki;
|
|
334
|
+
try { senderSpki = x509.parse(certDer).subjectPublicKeyInfo.bytes; }
|
|
335
|
+
catch (e) { throw _err("cmp/bad-input", "opts.cert is not a valid X.509 certificate", e); }
|
|
336
|
+
_b.assertValidSpki(senderSpki, "the sender certificate SPKI");
|
|
337
|
+
var scheme = signScheme.resolveSignScheme(_b.certLikeFromSpki(senderSpki), { combinedRsaSig: true, pss: opts.pss, digestAlgorithm: opts.digestAlgorithm }, true, _signE);
|
|
338
|
+
return {
|
|
339
|
+
protectionAlgDer: scheme.sigAlgId,
|
|
340
|
+
certDer: certDer,
|
|
341
|
+
computeBits: function (protectedPartDer) {
|
|
342
|
+
return Promise.resolve(signScheme.signOverTbs(scheme, opts.key, protectedPartDer, _signE)).then(function (sig) {
|
|
343
|
+
return Promise.resolve(_b.assertSignatureVerifies(protectedPartDer, sig, senderSpki, scheme)).then(function () {
|
|
344
|
+
return b.bitString(sig, 0);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// PBMAC1
|
|
352
|
+
var m = opts.mac;
|
|
353
|
+
if (!m || typeof m !== "object" || Buffer.isBuffer(m)) throw _err("cmp/bad-input", "opts.mac must be an object { secret, salt?, iterationCount?, prf?, keyLength? }");
|
|
354
|
+
Object.keys(m).forEach(function (k) { if (!KNOWN_MAC_KEYS[k]) throw _err("cmp/bad-input", "unknown opts.mac field " + JSON.stringify(k)); });
|
|
355
|
+
if (m.algorithm != null && m.algorithm !== "pbmac1") throw _err("cmp/unsupported-algorithm", "opts.mac.algorithm " + JSON.stringify(m.algorithm) + " is not supported (v1 ships pbmac1; passwordBasedMac is deferred)");
|
|
356
|
+
var secret = m.secret;
|
|
357
|
+
if (typeof secret !== "string" || !secret) {
|
|
358
|
+
if (!Buffer.isBuffer(secret) || !secret.length) throw _err("cmp/bad-input", "opts.mac.secret must be a non-empty string or Buffer");
|
|
359
|
+
}
|
|
360
|
+
var secretBuf = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, "utf8");
|
|
361
|
+
var prf = m.prf || "SHA-256";
|
|
362
|
+
if (!PBMAC1_PRF[prf]) throw _err("cmp/bad-input", "opts.mac.prf must be SHA-256 / SHA-384 / SHA-512");
|
|
363
|
+
var iterationCount = m.iterationCount != null ? m.iterationCount : PBMAC1_DEFAULT_ITER;
|
|
364
|
+
if (typeof iterationCount !== "number" || !Number.isInteger(iterationCount) || iterationCount < PBMAC1_MIN_ITER) throw _err("cmp/bad-input", "opts.mac.iterationCount must be at least " + PBMAC1_MIN_ITER + " (RFC 8018 sec. 4.2)");
|
|
365
|
+
// Bound the PBKDF2 work factors BEFORE deriving -- a huge iterationCount / keyLength is self-inflicted work.
|
|
366
|
+
if (iterationCount > constants.LIMITS.PBKDF2_MAX_ITERATIONS) throw _err("cmp/bad-input", "opts.mac.iterationCount exceeds the PBKDF2 work-factor cap " + constants.LIMITS.PBKDF2_MAX_ITERATIONS);
|
|
367
|
+
var keyLength = m.keyLength != null ? m.keyLength : PBMAC1_DEFAULT_KEYLEN;
|
|
368
|
+
if (typeof keyLength !== "number" || !Number.isInteger(keyLength) || keyLength < 1) throw _err("cmp/bad-input", "opts.mac.keyLength must be a positive integer (bytes)");
|
|
369
|
+
if (keyLength > PBMAC1_MAX_KEYLEN) throw _err("cmp/bad-input", "opts.mac.keyLength exceeds the cap " + PBMAC1_MAX_KEYLEN + " bytes");
|
|
370
|
+
var salt = m.salt != null ? _b.reqDer(m.salt, "opts.mac.salt") : Buffer.from(webcrypto.webcrypto.getRandomValues(new Uint8Array(PBMAC1_DEFAULT_SALT_BYTES)));
|
|
371
|
+
if (salt.length > constants.LIMITS.PBKDF2_MAX_SALT) throw _err("cmp/bad-input", "opts.mac.salt exceeds " + constants.LIMITS.PBKDF2_MAX_SALT + " bytes");
|
|
372
|
+
var macDesc = { salt: salt, iterationCount: iterationCount, keyLength: keyLength, prfName: PBMAC1_PRF[prf], macName: PBMAC1_MAC_OID[prf] };
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
protectionAlgDer: _encodePbmac1AlgId(macDesc),
|
|
376
|
+
certDer: null,
|
|
377
|
+
computeBits: function (protectedPartDer) {
|
|
378
|
+
return _pbmac1(secretBuf, salt, iterationCount, keyLength, prf, protectedPartDer).then(function (mac) {
|
|
379
|
+
return b.bitString(mac, 0);
|
|
380
|
+
});
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// PBKDF2-derive an HMAC key from the shared secret, then HMAC the ProtectedPart (both via the engine).
|
|
386
|
+
function _pbmac1(secretBuf, salt, iterationCount, keyLength, prf, message) {
|
|
387
|
+
var subtle = webcrypto.webcrypto.subtle;
|
|
388
|
+
return subtle.importKey("raw", secretBuf, { name: "PBKDF2" }, false, ["deriveBits"]).then(function (baseKey) {
|
|
389
|
+
return subtle.deriveBits({ name: "PBKDF2", salt: salt, iterations: iterationCount, hash: prf }, baseKey, keyLength * 8);
|
|
390
|
+
}).then(function (bits) {
|
|
391
|
+
return subtle.importKey("raw", Buffer.from(bits), { name: "HMAC", hash: prf }, false, ["sign"]).then(function (hmacKey) {
|
|
392
|
+
return subtle.sign({ name: "HMAC" }, hmacKey, message);
|
|
393
|
+
});
|
|
394
|
+
}).then(function (sig) { return Buffer.from(sig); });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---- orchestrator ----
|
|
398
|
+
|
|
399
|
+
function build(message, opts) {
|
|
400
|
+
return Promise.resolve().then(function () { return _build(message, opts); });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function _build(message, opts) {
|
|
404
|
+
opts = opts || {};
|
|
405
|
+
if (!message || typeof message !== "object" || Buffer.isBuffer(message)) throw _err("cmp/bad-input", "the PKIMessage spec must be an object { header, body }");
|
|
406
|
+
Object.keys(message).forEach(function (k) { if (k !== "header" && k !== "body") throw _err("cmp/bad-input", "unknown message field " + JSON.stringify(k) + " (a message carries only header + body)"); });
|
|
407
|
+
Object.keys(opts).forEach(function (k) { if (!KNOWN_OPTS_KEYS[k]) throw _err("cmp/bad-input", "unknown opts field " + JSON.stringify(k)); });
|
|
408
|
+
if (message.header == null) throw _err("cmp/bad-input", "message.header is required");
|
|
409
|
+
if (message.body == null) throw _err("cmp/bad-input", "message.body is required");
|
|
410
|
+
|
|
411
|
+
var prot = _resolveProtection(opts);
|
|
412
|
+
var senderKey = opts.key;
|
|
413
|
+
|
|
414
|
+
return Promise.resolve(_encodeBody(message.body, senderKey, opts)).then(function (bodyResult) {
|
|
415
|
+
var bodyTLV = bodyResult.bodyTLV;
|
|
416
|
+
var pvno = message.header.pvno != null ? message.header.pvno : 2;
|
|
417
|
+
if (typeof pvno !== "number" || !Number.isInteger(pvno)) throw _err("cmp/bad-input", "header.pvno must be an integer");
|
|
418
|
+
if (bodyResult.usesCmp2021 && pvno < 3) pvno = 3; // a certConf hashAlg forces cmp2021(3)
|
|
419
|
+
|
|
420
|
+
var headerTLV = _encodeHeader(message.header, prot.protectionAlgDer, pvno); // built ONCE
|
|
421
|
+
var protectedPartDer = b.sequence([headerTLV, bodyTLV]); // virtual ProtectedPart ::= SEQUENCE { header, body }
|
|
422
|
+
|
|
423
|
+
return prot.computeBits(protectedPartDer).then(function (protectionBits) {
|
|
424
|
+
var msgChildren = [headerTLV, bodyTLV, b.explicit(0, protectionBits)]; // reuse the SAME header/body TLVs
|
|
425
|
+
var extraCertsDers = _collectExtraCerts(opts, prot.certDer);
|
|
426
|
+
if (extraCertsDers.length) msgChildren.push(b.explicit(1, b.sequence(extraCertsDers)));
|
|
427
|
+
var der = b.sequence(msgChildren);
|
|
428
|
+
|
|
429
|
+
cmp.parse(der); // round-trip re-validation: the emitted message MUST parse (protection<=>protectionAlg, pvno, freeText, etc.)
|
|
430
|
+
if (opts.pem != null) {
|
|
431
|
+
if (opts.pem === true) return cmp.pemEncode(der, "CMP");
|
|
432
|
+
if (typeof opts.pem !== "string" || !opts.pem) throw _err("cmp/bad-input", "opts.pem must be true or a non-empty PEM label string");
|
|
433
|
+
return cmp.pemEncode(der, opts.pem);
|
|
434
|
+
}
|
|
435
|
+
return der;
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function _collectExtraCerts(opts, protCertDer) {
|
|
441
|
+
var out = [];
|
|
442
|
+
if (protCertDer != null) out.push(b.raw(protCertDer));
|
|
443
|
+
if (opts.extraCerts != null) {
|
|
444
|
+
if (!Array.isArray(opts.extraCerts)) throw _err("cmp/bad-extra-certs", "opts.extraCerts must be an array of certificate DERs");
|
|
445
|
+
opts.extraCerts.forEach(function (c) {
|
|
446
|
+
var der = _b.reqDer(c, "extraCerts entry (a Certificate DER)");
|
|
447
|
+
try { x509.parse(der); } // each extraCerts entry MUST be a valid X.509 certificate (RFC 9810 sec. 5.1)
|
|
448
|
+
catch (e) { if (e instanceof CmpError) throw e; throw _err("cmp/bad-extra-certs", "an extraCerts entry is not a valid X.509 certificate", e); }
|
|
449
|
+
out.push(b.raw(der));
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* @primitive pki.cmp.build
|
|
457
|
+
* @signature pki.cmp.build(message, opts?) -> Promise<Buffer|string>
|
|
458
|
+
* @since 0.3.5
|
|
459
|
+
* @status experimental
|
|
460
|
+
* @spec RFC 9810, RFC 9481, RFC 9579
|
|
461
|
+
* @related pki.schema.cmp.parse
|
|
462
|
+
*
|
|
463
|
+
* Build an RFC 9810 CMP `PKIMessage` -- the producing-side inverse of `pki.schema.cmp.parse`. `message` is
|
|
464
|
+
* `{ header, body }`: `header` carries the `sender` / `recipient` GeneralNames plus optional transaction
|
|
465
|
+
* metadata (`transactionID`, `senderNonce`, `messageTime`, `freeText`, `generalInfo`, ...); `body` is a
|
|
466
|
+
* single-key object naming the request or transaction arm -- `{ ir }` / `{ cr }` / `{ kur }` (a
|
|
467
|
+
* `CertReqMessages` spec delegated to `pki.crmf.build`), `{ p10cr }` (a PKCS#10 CertificationRequest DER),
|
|
468
|
+
* `{ certConf }`, `{ pollReq }`, `{ genm }`, or `{ rr }`. The message is protected: `opts` carries EXACTLY
|
|
469
|
+
* ONE of `{ key, cert }` (a signature under the sender key over the message -- any registry algorithm, RSA
|
|
470
|
+
* / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite, resolved from the certificate) or `{ mac }` (a PBMAC1
|
|
471
|
+
* shared-secret MAC). The protection is computed over the exact DER of the virtual
|
|
472
|
+
* `ProtectedPart ::= SEQUENCE { header, body }` and self-verified before the message is returned. The
|
|
473
|
+
* emitted PKIMessage round-trips byte-identically through `pki.schema.cmp.parse`.
|
|
474
|
+
*
|
|
475
|
+
* @opts
|
|
476
|
+
* - `key` (Buffer|CryptoKey) + `cert` (Buffer) -- signature protection under the sender key; `cert` is
|
|
477
|
+
* the signer certificate (its SPKI resolves the algorithm) and is placed in `extraCerts`.
|
|
478
|
+
* - `mac` ({ secret, salt?, iterationCount?, prf?, keyLength? }) -- PBMAC1 protection from a shared secret.
|
|
479
|
+
* - `extraCerts` (array of Buffer) -- additional certificates to carry in `extraCerts [1]`.
|
|
480
|
+
* - `pem` (boolean|string) -- return a PEM `CMP` block instead of DER.
|
|
481
|
+
* - `pss` (boolean) / `digestAlgorithm` (string) -- signature-protection algorithm options.
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* var der = await pki.cmp.build(
|
|
485
|
+
* { header: { sender: { directoryName: "CN=client" }, recipient: { directoryName: "CN=CA" } },
|
|
486
|
+
* body: { p10cr: csrDer } },
|
|
487
|
+
* { key: signerKeyPkcs8, cert: signerCertDer });
|
|
488
|
+
* pki.schema.cmp.parse(der).body.arm; // "p10cr"
|
|
489
|
+
*/
|
|
490
|
+
module.exports = { build: build };
|
package/lib/crmf-sign.js
CHANGED
|
@@ -47,6 +47,7 @@ var _b = pkiBuild.makeBuilder({
|
|
|
47
47
|
|
|
48
48
|
var KNOWN_SPEC_KEYS = { certReqId: 1, certTemplate: 1, controls: 1, regInfo: 1, pop: 1 };
|
|
49
49
|
var KNOWN_TEMPLATE_KEYS = { version: 1, subject: 1, publicKey: 1, validity: 1, extensions: 1, issuer: 1 };
|
|
50
|
+
var REVOCATION_TEMPLATE_KEYS = { version: 1, subject: 1, publicKey: 1, validity: 1, extensions: 1, issuer: 1, serialNumber: 1 };
|
|
50
51
|
// The controls (RFC 4211 sec. 6) and regInfo (sec. 7) are DISJOINT AttributeTypeAndValue namespaces --
|
|
51
52
|
// regToken/authenticator/oldCertID/protocolEncrKey are controls; utf8Pairs is regInfo -- so the object
|
|
52
53
|
// form validates each key against its own field's registry (a control name is not a valid regInfo, and
|
|
@@ -77,14 +78,18 @@ function _encodeOptionalValidity(validity) {
|
|
|
77
78
|
// CertTemplate ::= SEQUENCE { [0..9] all IMPLICIT OPTIONAL }. A REQUEST omits serialNumber [1] / signingAlg
|
|
78
79
|
// [2] / issuerUID [7] / subjectUID [8] (CA-assigned or deprecated, RFC 4211 sec. 5) -- the builder never
|
|
79
80
|
// emits them. Fields are emitted in ascending tag order.
|
|
80
|
-
function _encodeCertTemplate(tpl) {
|
|
81
|
+
function _encodeCertTemplate(tpl, opts) {
|
|
81
82
|
if (!tpl || typeof tpl !== "object" || Buffer.isBuffer(tpl)) throw _err("crmf/bad-cert-template", "certTemplate must be an object");
|
|
82
|
-
|
|
83
|
+
// A REQUEST template omits serialNumber (CA-assigned, RFC 4211 sec. 5); a REVOCATION template (CMP rr,
|
|
84
|
+
// RFC 9810 sec. 5.3.9) carries serialNumber [1] to name the certificate to revoke -- allowed only then.
|
|
85
|
+
var allowed = (opts && opts.revocation) ? REVOCATION_TEMPLATE_KEYS : KNOWN_TEMPLATE_KEYS;
|
|
86
|
+
Object.keys(tpl).forEach(function (k) { if (!allowed[k]) throw _err("crmf/bad-input", "unknown certTemplate field " + JSON.stringify(k)); });
|
|
83
87
|
var fields = [];
|
|
84
88
|
if (tpl.version != null) {
|
|
85
89
|
if (tpl.version !== 2) throw _err("crmf/bad-version", "certTemplate version MUST be 2 (v3) if supplied (RFC 4211 sec. 5)");
|
|
86
90
|
fields.push(b.implicit(0, b.integer(2n))); // version [0]
|
|
87
91
|
}
|
|
92
|
+
if (tpl.serialNumber != null) fields.push(b.implicit(1, _b.serialInteger(tpl.serialNumber))); // serialNumber [1] (revocation only)
|
|
88
93
|
// issuer [3] / subject [5] are EXPLICIT: Name is a CHOICE, and X.680 sec. 31.2.7 forces a context tag on
|
|
89
94
|
// a CHOICE to EXPLICIT even under the module's IMPLICIT TAGS default (the [3]/[5] wraps the RDNSequence
|
|
90
95
|
// SEQUENCE, it does not replace its tag). The parser accepts both forms; this is the conformant one.
|
|
@@ -272,4 +277,24 @@ function _build(spec, key, opts) {
|
|
|
272
277
|
});
|
|
273
278
|
}
|
|
274
279
|
|
|
275
|
-
|
|
280
|
+
/**
|
|
281
|
+
* @primitive pki.crmf.buildCertTemplate
|
|
282
|
+
* @signature pki.crmf.buildCertTemplate(template) -> Buffer
|
|
283
|
+
* @since 0.3.5
|
|
284
|
+
* @status experimental
|
|
285
|
+
* @spec RFC 4211
|
|
286
|
+
* @related pki.crmf.build
|
|
287
|
+
*
|
|
288
|
+
* Encode a bare RFC 4211 `CertTemplate` (the requested-certificate fields -- `subject`, `publicKey`,
|
|
289
|
+
* `validity`, requested `extensions`, an optional `version` 2, `issuer`) to canonical DER. This is the
|
|
290
|
+
* `certTemplate` interior of `pki.crmf.build`, exposed for the RFC 9810 CMP `rr` (revocation request) body,
|
|
291
|
+
* whose `RevDetails.certDetails` carries a `CertTemplate` naming the certificate to revoke. Returns the DER
|
|
292
|
+
* `Buffer`; a malformed template throws a typed `CrmfError`.
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* var tpl = pki.crmf.buildCertTemplate({ serialNumber: 42n, issuer: "CN=CA" });
|
|
296
|
+
* pki.asn1.decode(tpl).tagNumber === pki.asn1.TAGS.SEQUENCE; // the CertTemplate SEQUENCE
|
|
297
|
+
*/
|
|
298
|
+
function buildCertTemplate(template) { return _encodeCertTemplate(template, { revocation: true }).der; }
|
|
299
|
+
|
|
300
|
+
module.exports = { build: build, buildCertTemplate: buildCertTemplate };
|
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:9ded865a-a922-4de1-a8f0-bd02d3b29a2e",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-17T19:09:09.529Z",
|
|
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.5",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.5",
|
|
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.5",
|
|
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.5",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|