@blamejs/pki 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +1 -0
- package/index.js +5 -0
- package/lib/asn1-der.js +9 -0
- package/lib/crmf-sign.js +275 -0
- package/lib/csr-sign.js +3 -43
- package/lib/pki-build.js +64 -1
- package/lib/x509-sign.js +3 -8
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.3.3 — 2026-07-17
|
|
8
|
+
|
|
9
|
+
pki.crmf.build issues RFC 4211 certificate request messages -- a CertReqMessages with a signature proof of possession, over every algorithm the toolkit supports.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.crmf.build(spec, key, opts) builds and DER-encodes an RFC 4211 CertReqMessages -- a CertTemplate of the requested certificate fields plus a POPOSigningKey proof of possession signed with the requester's key (or a raVerified proof, opted into without a key) -- and returns DER, or a PEM block with opts.pem. The signing algorithm is resolved from the requested public key: RSA PKCS#1 v1.5 / PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm. Requested v3 extensions, registration controls, and regInfo ride in the message; pass an array of specs for a batch. Certificate-request-message parsing remains pki.schema.crmf.parse.
|
|
14
|
+
- pki.asn1.build.implicit(tag, tlv) retags an encoded universal TLV as a context-class IMPLICIT [tag], preserving the source's primitive/constructed bit and content -- the single home for the IMPLICIT tag replacement the CertTemplate and other context-tagged structures compose.
|
|
15
|
+
|
|
7
16
|
## v0.3.2 — 2026-07-17
|
|
8
17
|
|
|
9
18
|
pki.attrcert.sign issues RFC 5755 attribute certificates -- an Attribute Authority binds a holder to privilege attributes and signs with any algorithm the toolkit supports.
|
package/README.md
CHANGED
|
@@ -225,6 +225,7 @@ is callable today; nothing below is a stub.
|
|
|
225
225
|
| `pki.x509` | X.509 certificate issuance (RFC 5280 §4) — `sign(spec, issuer, opts)` builds and signs a certificate: a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER), the public key being certified, the validity window, an optional serial, and an optional `extensions` object; an `issuer` that is a key alone (self-signed — issuer equals subject, signed with that key) or a name + public key + key, or an issuing certificate + key (CA-signed). The signature algorithm is resolved from the signing key through the shared registry, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA P-256/384/521, Ed25519, Ed448, ML-DSA-44/65/87, the twelve SLH-DSA sets, and the composite arms all issue without a per-algorithm branch. It encodes basic constraints, key usage, extended key usage, subject and authority key identifiers (the SKI auto-derived by SHA-1 of the subject key), subject alternative names, and certificate policies from the spec — any other extension supplied as pre-encoded DER — derives the version from the field set, and enforces the serial bounds, the UTCTime/GeneralizedTime cutover, the DER default omissions, and the CA cross-field rules; a violation throws a typed `CertificateError`. Returns DER, or a PEM `CERTIFICATE` with `opts.pem`; every arm is independently verified by OpenSSL. Parsing stays at `pki.schema.x509.parse` — `sign` |
|
|
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
|
+
| `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` |
|
|
228
229
|
| `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` |
|
|
229
230
|
| `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` |
|
|
230
231
|
| `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
|
@@ -43,6 +43,7 @@ var ocsp = require("./lib/ocsp");
|
|
|
43
43
|
var x509 = require("./lib/x509-sign");
|
|
44
44
|
var csr = require("./lib/csr-sign");
|
|
45
45
|
var attrcert = require("./lib/attrcert-sign");
|
|
46
|
+
var crmf = require("./lib/crmf-sign");
|
|
46
47
|
var merkle = require("./lib/merkle");
|
|
47
48
|
var shbs = require("./lib/shbs");
|
|
48
49
|
var hpke = require("./lib/hpke");
|
|
@@ -98,6 +99,10 @@ module.exports = {
|
|
|
98
99
|
// builds and signs an AttributeCertificate binding a Holder to privilege attributes as an
|
|
99
100
|
// Attribute Authority (never self-signed). Parsing lives at pki.schema.attrcert.parse.
|
|
100
101
|
attrcert: attrcert,
|
|
102
|
+
// `crmf` is the RFC 4211 certificate-request-message producing side -- pki.crmf.build
|
|
103
|
+
// assembles a CertReqMessages (a CertTemplate + a signature proof of possession) for a CMP
|
|
104
|
+
// or EST enrollment. Parsing lives at pki.schema.crmf.parse.
|
|
105
|
+
crmf: crmf,
|
|
101
106
|
// `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
|
|
102
107
|
// pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
|
|
103
108
|
// (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
|
package/lib/asn1-der.js
CHANGED
|
@@ -989,6 +989,15 @@ var build = {
|
|
|
989
989
|
explicit: function (tagNumber, inner) { return encodeTLV(CLASS_CONTEXT, true, tagNumber, _asBuffer(inner, "build.explicit")); },
|
|
990
990
|
contextPrimitive: function (tagNumber, content) { return encodeTLV(CLASS_CONTEXT, false, tagNumber, _asBuffer(content, "build.contextPrimitive")); },
|
|
991
991
|
contextConstructed: function (tagNumber, content) { return encodeTLV(CLASS_CONTEXT, true, tagNumber, _asBuffer(content, "build.contextConstructed")); },
|
|
992
|
+
// IMPLICIT [tag] retag of an already-encoded UNIVERSAL TLV: replace the source's tag with a context-class
|
|
993
|
+
// [tag] identifier, PRESERVING the source's primitive/constructed bit and content octets (X.680 sec. 30.6).
|
|
994
|
+
// A single home for the tag replacement so no format module re-inlines the P/C-bit handling (a decode of
|
|
995
|
+
// the source validates it is exactly one well-formed TLV; its content is the trailing `length` octets).
|
|
996
|
+
implicit: function (tagNumber, tlv) {
|
|
997
|
+
var buf = _asBuffer(tlv, "build.implicit");
|
|
998
|
+
var node = decode(buf);
|
|
999
|
+
return encodeTLV(CLASS_CONTEXT, node.constructed, tagNumber, buf.slice(buf.length - node.length));
|
|
1000
|
+
},
|
|
992
1001
|
raw: function (buf) { return _asBuffer(buf, "build.raw"); },
|
|
993
1002
|
};
|
|
994
1003
|
|
package/lib/crmf-sign.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @module pki.crmf
|
|
7
|
+
* @nav Signing
|
|
8
|
+
* @title Certificate request messages
|
|
9
|
+
* @intro The RFC 4211 certificate-request-message producing side. `pki.crmf.build` assembles a
|
|
10
|
+
* `CertReqMessages` -- one or more `CertReqMsg`, each a `CertRequest` (a `CertTemplate` of the requested
|
|
11
|
+
* certificate fields plus optional controls) paired with a proof of possession. The common proof is a
|
|
12
|
+
* `POPOSigningKey` signature over the `CertRequest`, made with the private half of the key being
|
|
13
|
+
* certified (the requester proves possession, exactly as a PKCS#10 CSR does). The message drops into a
|
|
14
|
+
* CMP (RFC 9810) or EST enrollment body. Parsing lives at `pki.schema.crmf.parse`.
|
|
15
|
+
* @spec RFC 4211
|
|
16
|
+
* @card Build a CRMF CertReqMessages with a signature proof of possession.
|
|
17
|
+
*/
|
|
18
|
+
//
|
|
19
|
+
// RFC 4211 App. B / RFC 5912 sec. 10 are DEFINITIONS IMPLICIT TAGS: a CertTemplate [0]..[9] field tag
|
|
20
|
+
// REPLACES the base universal tag (built via asn1.build.implicit, preserving the primitive/constructed
|
|
21
|
+
// bit); the shipped parser encodes issuer [3] / subject [5] Name IMPLICITLY (the dominant CMP/EST wire
|
|
22
|
+
// form), so the builder does too. The two EXPLICIT exceptions are the OptionalValidity notBefore [0] /
|
|
23
|
+
// notAfter [1] Time (a genuine UTCTime/GeneralizedTime CHOICE). The signature scheme resolves from the
|
|
24
|
+
// requested publicKey through the shared sign-scheme registry; the Name / extension / SPKI encoders and
|
|
25
|
+
// the post-sign self-check are the shared lib/pki-build primitives, bound to the crmf namespace.
|
|
26
|
+
|
|
27
|
+
var asn1 = require("./asn1-der");
|
|
28
|
+
var oid = require("./oid");
|
|
29
|
+
var crmf = require("./schema-crmf");
|
|
30
|
+
var signScheme = require("./sign-scheme");
|
|
31
|
+
var pkix = require("./schema-pkix");
|
|
32
|
+
var pkiBuild = require("./pki-build");
|
|
33
|
+
var frameworkError = require("./framework-error");
|
|
34
|
+
|
|
35
|
+
var CrmfError = frameworkError.CrmfError;
|
|
36
|
+
var b = asn1.build;
|
|
37
|
+
function _err(code, message, cause) { return new CrmfError(code, message, cause); }
|
|
38
|
+
function _signE(kind, message, cause) { return new CrmfError("crmf/" + kind, message, cause); }
|
|
39
|
+
function O(n) { return oid.byName(n); }
|
|
40
|
+
|
|
41
|
+
var NS = pkix.makeNS("crmf", CrmfError, oid);
|
|
42
|
+
var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
|
|
43
|
+
var _b = pkiBuild.makeBuilder({
|
|
44
|
+
ErrorClass: CrmfError, prefix: "crmf", O: O, NS: NS,
|
|
45
|
+
NAME_SCHEMA: pkix.name(NS), SPKI_SCHEMA: pkix.spki(NS), EXT_DECODERS: EXT_DECODERS,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
var KNOWN_SPEC_KEYS = { certReqId: 1, certTemplate: 1, controls: 1, regInfo: 1, pop: 1 };
|
|
49
|
+
var KNOWN_TEMPLATE_KEYS = { version: 1, subject: 1, publicKey: 1, validity: 1, extensions: 1, issuer: 1 };
|
|
50
|
+
// The controls (RFC 4211 sec. 6) and regInfo (sec. 7) are DISJOINT AttributeTypeAndValue namespaces --
|
|
51
|
+
// regToken/authenticator/oldCertID/protocolEncrKey are controls; utf8Pairs is regInfo -- so the object
|
|
52
|
+
// form validates each key against its own field's registry (a control name is not a valid regInfo, and
|
|
53
|
+
// vice versa). The key name equals the registered OID name; complex values (pkiPublicationInfo, certReq)
|
|
54
|
+
// ride the pre-encoded escape hatch. Each entry is the value encoder for that OID.
|
|
55
|
+
function _ctlUtf8(v) { return b.utf8(String(v)); }
|
|
56
|
+
function _ctlSpki(v) { var k = _b.reqDer(v, "protocolEncrKey (an SPKI DER)"); _b.assertValidSpki(k, "protocolEncrKey"); return b.raw(k); }
|
|
57
|
+
var CONTROL_VALUE = { regToken: _ctlUtf8, authenticator: _ctlUtf8, oldCertID: function (v) { return _encodeCertId(v); }, protocolEncrKey: _ctlSpki };
|
|
58
|
+
var REGINFO_VALUE = { utf8Pairs: _ctlUtf8 };
|
|
59
|
+
|
|
60
|
+
// ---- CertTemplate + POP structural encoders (byte-exact inverses of schema-crmf.js) ----
|
|
61
|
+
|
|
62
|
+
// OptionalValidity [4] IMPLICIT SEQUENCE { notBefore [0] EXPLICIT Time, notAfter [1] EXPLICIT Time } --
|
|
63
|
+
// at least one present (RFC 4211 sec. 5). Time is a CHOICE so [0]/[1] are EXPLICIT.
|
|
64
|
+
function _encodeOptionalValidity(validity) {
|
|
65
|
+
if (!validity || typeof validity !== "object" || Buffer.isBuffer(validity)) throw _err("crmf/bad-validity", "validity must be an object { notBefore?, notAfter? }");
|
|
66
|
+
var nb = validity.notBefore, na = validity.notAfter;
|
|
67
|
+
if (nb == null && na == null) throw _err("crmf/bad-validity", "validity must contain notBefore or notAfter (RFC 4211 sec. 5)");
|
|
68
|
+
var parts = [];
|
|
69
|
+
// timeDer validates each instant (guard.time.assertValid throws on an Invalid Date) BEFORE the
|
|
70
|
+
// inverted-window comparison, so getTime() below cannot be NaN.
|
|
71
|
+
if (nb != null) parts.push(b.explicit(0, _b.timeDer(nb, "validity notBefore")));
|
|
72
|
+
if (na != null) parts.push(b.explicit(1, _b.timeDer(na, "validity notAfter")));
|
|
73
|
+
// allow:nan-date-comparison-unguarded -- both instants passed timeDer's guard.time.assertValid above.
|
|
74
|
+
if (nb != null && na != null && nb.getTime() > na.getTime()) throw _err("crmf/bad-validity", "notBefore must not be after notAfter");
|
|
75
|
+
return b.implicit(4, b.sequence(parts));
|
|
76
|
+
}
|
|
77
|
+
// CertTemplate ::= SEQUENCE { [0..9] all IMPLICIT OPTIONAL }. A REQUEST omits serialNumber [1] / signingAlg
|
|
78
|
+
// [2] / issuerUID [7] / subjectUID [8] (CA-assigned or deprecated, RFC 4211 sec. 5) -- the builder never
|
|
79
|
+
// emits them. Fields are emitted in ascending tag order.
|
|
80
|
+
function _encodeCertTemplate(tpl) {
|
|
81
|
+
if (!tpl || typeof tpl !== "object" || Buffer.isBuffer(tpl)) throw _err("crmf/bad-cert-template", "certTemplate must be an object");
|
|
82
|
+
Object.keys(tpl).forEach(function (k) { if (!KNOWN_TEMPLATE_KEYS[k]) throw _err("crmf/bad-input", "unknown certTemplate field " + JSON.stringify(k)); });
|
|
83
|
+
var fields = [];
|
|
84
|
+
if (tpl.version != null) {
|
|
85
|
+
if (tpl.version !== 2) throw _err("crmf/bad-version", "certTemplate version MUST be 2 (v3) if supplied (RFC 4211 sec. 5)");
|
|
86
|
+
fields.push(b.implicit(0, b.integer(2n))); // version [0]
|
|
87
|
+
}
|
|
88
|
+
// issuer [3] / subject [5] are EXPLICIT: Name is a CHOICE, and X.680 sec. 31.2.7 forces a context tag on
|
|
89
|
+
// a CHOICE to EXPLICIT even under the module's IMPLICIT TAGS default (the [3]/[5] wraps the RDNSequence
|
|
90
|
+
// SEQUENCE, it does not replace its tag). The parser accepts both forms; this is the conformant one.
|
|
91
|
+
if (tpl.issuer != null) fields.push(b.explicit(3, _b.encodeName(tpl.issuer))); // issuer [3] EXPLICIT
|
|
92
|
+
if (tpl.validity != null) fields.push(_encodeOptionalValidity(tpl.validity)); // validity [4] IMPLICIT
|
|
93
|
+
if (tpl.subject != null) fields.push(b.explicit(5, _b.encodeName(tpl.subject))); // subject [5] EXPLICIT
|
|
94
|
+
var spki = null;
|
|
95
|
+
if (tpl.publicKey != null) {
|
|
96
|
+
spki = _b.reqDer(tpl.publicKey, "certTemplate.publicKey (the SPKI DER of the requested key)");
|
|
97
|
+
_b.assertValidSpki(spki, "certTemplate.publicKey");
|
|
98
|
+
fields.push(b.implicit(6, spki)); // publicKey [6]
|
|
99
|
+
}
|
|
100
|
+
if (tpl.extensions != null) fields.push(b.implicit(9, _b.requestedExtensions(tpl.extensions, spki))); // extensions [9]
|
|
101
|
+
return { der: b.sequence(fields), spki: spki, complete: tpl.subject != null && tpl.publicKey != null };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Controls / regInfo ::= SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue { type OID, value ANY }. Object
|
|
105
|
+
// form maps recognized names to typed value encoders; any other (or a pre-encoded AttributeTypeAndValue
|
|
106
|
+
// DER array) rides the escape hatch, shape-validated.
|
|
107
|
+
function _buildAttrTypeAndValues(spec, code, label, valueMap) {
|
|
108
|
+
if (Array.isArray(spec)) {
|
|
109
|
+
if (!spec.length) throw _err(code, label + " must carry at least one entry");
|
|
110
|
+
var seenA = {};
|
|
111
|
+
return b.sequence(spec.map(function (e, i) {
|
|
112
|
+
var der = _b.reqDer(e, label + " [" + i + "]");
|
|
113
|
+
var n;
|
|
114
|
+
try { n = asn1.decode(der); } catch (err) { throw _err("crmf/bad-input", "pre-encoded " + label + " [" + i + "] is not valid DER", err); }
|
|
115
|
+
if (n.tagNumber !== asn1.TAGS.SEQUENCE || n.tagClass !== "universal" || !n.children || n.children.length !== 2) throw _err("crmf/bad-input", "pre-encoded " + label + " [" + i + "] must be a SEQUENCE { type OID, value }");
|
|
116
|
+
var t;
|
|
117
|
+
try { t = asn1.read.oid(n.children[0]); } catch (err) { throw _err("crmf/bad-input", "pre-encoded " + label + " [" + i + "] type is not an OBJECT IDENTIFIER", err); }
|
|
118
|
+
if (seenA[t]) throw _err(code, "duplicate " + label + " type " + (oid.name(t) || t));
|
|
119
|
+
seenA[t] = true;
|
|
120
|
+
return b.raw(der);
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
if (!spec || typeof spec !== "object") throw _err("crmf/bad-input", label + " must be an object or an array of pre-encoded AttributeTypeAndValue DER");
|
|
124
|
+
var out = [], seen = {};
|
|
125
|
+
Object.keys(spec).forEach(function (k) {
|
|
126
|
+
var enc = valueMap[k];
|
|
127
|
+
if (!enc) throw _err("crmf/bad-input", "unknown " + label + " " + JSON.stringify(k) + "; pass a pre-encoded AttributeTypeAndValue DER via the array form for a " + label + " entry outside " + Object.keys(valueMap).join("/"));
|
|
128
|
+
var typeOid = O(k);
|
|
129
|
+
if (seen[typeOid]) throw _err(code, "duplicate " + label + " type " + k);
|
|
130
|
+
seen[typeOid] = true;
|
|
131
|
+
out.push(b.sequence([b.oid(typeOid), enc(spec[k])]));
|
|
132
|
+
});
|
|
133
|
+
if (!out.length) throw _err(code, label + " must carry at least one entry");
|
|
134
|
+
return b.sequence(out);
|
|
135
|
+
}
|
|
136
|
+
// certReqId is a signed INTEGER, value UNCONSTRAINED (the RFC 9483 -1 sentinel and 0 are both legal). A
|
|
137
|
+
// number must be a safe integer (a fractional or > 2^53 number loses precision through BigInt); a BigInt
|
|
138
|
+
// or a decimal / 0x-hex string carries an arbitrary value.
|
|
139
|
+
function _certReqId(v) {
|
|
140
|
+
if (v == null) return 0n;
|
|
141
|
+
if (typeof v === "bigint") return v;
|
|
142
|
+
if (typeof v === "number") { if (!Number.isSafeInteger(v)) throw _err("crmf/bad-input", "certReqId number must be a safe integer (pass a BigInt or a string for a larger value)"); return BigInt(v); }
|
|
143
|
+
if (typeof v === "string") { try { return BigInt(v); } catch (e) { throw _err("crmf/bad-input", "certReqId string must be a decimal or 0x-hex integer", e); } }
|
|
144
|
+
throw _err("crmf/bad-input", "certReqId must be a BigInt, a safe integer, or a string");
|
|
145
|
+
}
|
|
146
|
+
// CertId ::= SEQUENCE { issuer GeneralName, serialNumber INTEGER } (RFC 4211 sec. 6.4, oldCertID value).
|
|
147
|
+
function _encodeCertId(id) {
|
|
148
|
+
if (!id || typeof id !== "object" || id.issuer == null || id.serialNumber == null) throw _err("crmf/bad-input", "oldCertID must be { issuer: GeneralName, serialNumber }");
|
|
149
|
+
return b.sequence([_b.encodeGeneralName(id.issuer), _b.serialInteger(id.serialNumber)]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ProofOfPossession. The signature arm (the default when a key is given): when the template carries BOTH
|
|
153
|
+
// subject and publicKey (complete), sign the CertRequest DER and OMIT poposkInput; otherwise build a
|
|
154
|
+
// POPOSigningKeyInput (authInfo sender [0] GeneralName + the requested publicKey), sign its SEQUENCE, and
|
|
155
|
+
// carry it as poposkInput [0] (RFC 4211 sec. 4.1). raVerified is emitted only on an explicit opt-in.
|
|
156
|
+
function _buildProofOfPossession(pop, certReqDer, template, signingKey, opts) {
|
|
157
|
+
if (pop != null && (typeof pop !== "object" || Buffer.isBuffer(pop))) throw _err("crmf/bad-input", "spec.pop must be an object (e.g. { type: 'signature' } or { type: 'raVerified', raVerified: true })");
|
|
158
|
+
var mode = (pop && pop.type) || (signingKey != null ? "signature" : null);
|
|
159
|
+
if (mode == null) return null; // no POP requested and no key -> omit popo (an RA supplies it out of band)
|
|
160
|
+
if (mode === "raVerified") {
|
|
161
|
+
if (!(pop && pop.raVerified === true)) throw _err("crmf/bad-popo", "raVerified must be explicitly opted into (pop: { type: 'raVerified', raVerified: true }) -- a requester does not normally assert it (RFC 4211 sec. 4)");
|
|
162
|
+
return b.implicit(0, b.nullValue()); // raVerified [0] IMPLICIT NULL
|
|
163
|
+
}
|
|
164
|
+
if (mode !== "signature") throw _err("crmf/bad-popo", "unsupported proof-of-possession type " + JSON.stringify(mode) + " (supported: 'signature', 'raVerified')");
|
|
165
|
+
if (signingKey == null) throw _err("crmf/bad-input", "a signature proof of possession requires the requester's private key");
|
|
166
|
+
if (template.spki == null) throw _err("crmf/bad-input", "a signature proof of possession requires certTemplate.publicKey");
|
|
167
|
+
var scheme = signScheme.resolveSignScheme(_b.certLikeFromSpki(template.spki), { combinedRsaSig: true, pss: opts.pss, digestAlgorithm: opts.digestAlgorithm }, true, _signE);
|
|
168
|
+
var signedRegion, poposkInputField = null;
|
|
169
|
+
if (template.complete) {
|
|
170
|
+
signedRegion = certReqDer; // sec. 4.1: complete template -> sign the CertRequest, poposkInput omitted
|
|
171
|
+
} else {
|
|
172
|
+
// POPOSigningKeyInput ::= SEQUENCE { authInfo CHOICE { sender [0] GeneralName, publicKeyMAC }, publicKey }.
|
|
173
|
+
var sender = pop && pop.sender;
|
|
174
|
+
if (sender == null) throw _err("crmf/bad-popo", "an incomplete template (missing subject or publicKey) requires pop.sender (a GeneralName) for the POPOSigningKeyInput authInfo (RFC 4211 sec. 4.1)");
|
|
175
|
+
var poposkSeq = b.sequence([b.explicit(0, _b.encodeGeneralName(sender)), b.raw(template.spki)]);
|
|
176
|
+
signedRegion = poposkSeq;
|
|
177
|
+
poposkInputField = b.implicit(0, poposkSeq); // poposkInput [0] IMPLICIT POPOSigningKeyInput
|
|
178
|
+
}
|
|
179
|
+
return signScheme.signOverTbs(scheme, signingKey, signedRegion, _signE).then(function (sig) {
|
|
180
|
+
return Promise.resolve(_b.assertSignatureVerifies(signedRegion, sig, template.spki, scheme)).then(function () {
|
|
181
|
+
var popoChildren = [];
|
|
182
|
+
if (poposkInputField) popoChildren.push(poposkInputField);
|
|
183
|
+
popoChildren.push(scheme.sigAlgId);
|
|
184
|
+
popoChildren.push(b.bitString(sig, 0));
|
|
185
|
+
return b.implicit(1, b.sequence(popoChildren)); // signature [1] IMPLICIT POPOSigningKey
|
|
186
|
+
});
|
|
187
|
+
}, function (e) {
|
|
188
|
+
if (e instanceof CrmfError) throw e;
|
|
189
|
+
throw _err("crmf/bad-input", "signing the proof of possession failed -- the key does not match the requested public key or is invalid", e);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @primitive pki.crmf.build
|
|
195
|
+
* @signature pki.crmf.build(spec, key?, opts?) -> Promise<Buffer|string>
|
|
196
|
+
* @since 0.3.3
|
|
197
|
+
* @status experimental
|
|
198
|
+
* @spec RFC 4211
|
|
199
|
+
* @defends forged-certificate-request (CWE-347)
|
|
200
|
+
* @related pki.schema.crmf.parse, pki.csr.sign
|
|
201
|
+
*
|
|
202
|
+
* Build and DER-encode an RFC 4211 `CertReqMessages`. `spec` describes one certificate request message (or
|
|
203
|
+
* pass `spec.messages` -- an array of specs -- for a batch): `certReqId` (an integer, default 0; the RFC
|
|
204
|
+
* 9483 `-1` sentinel is allowed), `certTemplate` (the requested certificate fields -- `subject`, `publicKey`
|
|
205
|
+
* (the SPKI DER of the key being certified), `validity` ({ notBefore, notAfter } Dates), `extensions` (an
|
|
206
|
+
* object of subjectAltName / keyUsage / extendedKeyUsage / basicConstraints / certificatePolicies /
|
|
207
|
+
* subjectKeyIdentifier, or pre-encoded Extension DER), and an optional `version` (2)), optional `controls`
|
|
208
|
+
* and `regInfo` (an object of regToken / authenticator / utf8Pairs / oldCertID / protocolEncrKey, or
|
|
209
|
+
* pre-encoded AttributeTypeAndValue DER), and an optional `pop` selector. `key` (or `{ key }`) is the
|
|
210
|
+
* REQUESTER's private key -- the private half of `certTemplate.publicKey`; the message carries a
|
|
211
|
+
* `POPOSigningKey` proof of possession signed with it (verified before the message is returned), exactly
|
|
212
|
+
* as a PKCS#10 CSR proves possession. The signature algorithm is resolved from the requested public key
|
|
213
|
+
* (RSA PKCS#1 v1.5 / PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm). `key` is optional -- omit it
|
|
214
|
+
* for a `raVerified` proof (opt in with `pop: { type: 'raVerified', raVerified: true }`). Returns DER, or a
|
|
215
|
+
* PEM block with `opts.pem` (the label is required). Malformed input throws a typed `CrmfError`.
|
|
216
|
+
* Certificate-request-message parsing is `pki.schema.crmf.parse`.
|
|
217
|
+
*
|
|
218
|
+
* @opts
|
|
219
|
+
* - `pem` (string) -- return a PEM block with this label instead of DER (e.g. "CERTIFICATE REQUEST MESSAGE").
|
|
220
|
+
* - `pss` (boolean) -- sign an RSA key with RSASSA-PSS rather than PKCS#1 v1.5.
|
|
221
|
+
* - `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.
|
|
222
|
+
* @example
|
|
223
|
+
* var msg = await pki.crmf.build(
|
|
224
|
+
* { certReqId: 0, certTemplate: { subject: "device-42", publicKey: signerSpki } },
|
|
225
|
+
* { key: signerKeyPkcs8 });
|
|
226
|
+
* pki.schema.crmf.parse(msg).messages[0].certReq.certTemplate.subject.dn; // "CN=device-42"
|
|
227
|
+
*/
|
|
228
|
+
function build(spec, key, opts) {
|
|
229
|
+
return Promise.resolve().then(function () { return _build(spec, key, opts); });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function _buildCertReqMsg(spec, key, opts) {
|
|
233
|
+
if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("crmf/bad-input", "each certificate-request-message spec must be an object");
|
|
234
|
+
Object.keys(spec).forEach(function (k) { if (!KNOWN_SPEC_KEYS[k]) throw _err("crmf/bad-input", "unknown spec field " + JSON.stringify(k)); });
|
|
235
|
+
if (spec.certTemplate == null) throw _err("crmf/bad-input", "spec.certTemplate is required");
|
|
236
|
+
|
|
237
|
+
var signingKey = (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type == null && "key" in key) ? key.key : key;
|
|
238
|
+
var template = _encodeCertTemplate(spec.certTemplate);
|
|
239
|
+
var certReqChildren = [b.integer(_certReqId(spec.certReqId)), template.der];
|
|
240
|
+
if (spec.controls != null) certReqChildren.push(_buildAttrTypeAndValues(spec.controls, "crmf/bad-controls", "controls", CONTROL_VALUE));
|
|
241
|
+
var certReqDer = b.sequence(certReqChildren);
|
|
242
|
+
|
|
243
|
+
return Promise.resolve(_buildProofOfPossession(spec.pop, certReqDer, template, signingKey, opts)).then(function (popoDer) {
|
|
244
|
+
var msgChildren = [certReqDer];
|
|
245
|
+
if (popoDer) msgChildren.push(popoDer);
|
|
246
|
+
if (spec.regInfo != null) msgChildren.push(_buildAttrTypeAndValues(spec.regInfo, "crmf/bad-reg-info", "regInfo", REGINFO_VALUE));
|
|
247
|
+
return b.sequence(msgChildren); // CertReqMsg
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function _build(spec, key, opts) {
|
|
252
|
+
opts = opts || {};
|
|
253
|
+
if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("crmf/bad-input", "the certificate-request-message spec must be an object");
|
|
254
|
+
var specs;
|
|
255
|
+
if (spec.messages != null) {
|
|
256
|
+
// Batch form: the envelope carries ONLY `messages` -- reject a stray field so a request spec written at
|
|
257
|
+
// the wrong nesting level (e.g. certTemplate alongside messages) is not silently dropped.
|
|
258
|
+
if (!Array.isArray(spec.messages)) throw _err("crmf/bad-input", "spec.messages must be an array of certificate-request-message specs");
|
|
259
|
+
Object.keys(spec).forEach(function (k) { if (k !== "messages") throw _err("crmf/bad-input", "unknown batch-envelope field " + JSON.stringify(k) + " -- a batch spec carries only 'messages'"); });
|
|
260
|
+
specs = spec.messages;
|
|
261
|
+
} else {
|
|
262
|
+
specs = [spec];
|
|
263
|
+
}
|
|
264
|
+
if (!specs.length) throw _err("crmf/bad-input", "at least one certificate request message is required (RFC 4211 sec. 3)");
|
|
265
|
+
return Promise.all(specs.map(function (s) { return _buildCertReqMsg(s, key, opts); })).then(function (msgs) {
|
|
266
|
+
var der = b.sequence(msgs); // CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg
|
|
267
|
+
if (opts.pem != null) {
|
|
268
|
+
if (typeof opts.pem !== "string" || !opts.pem) throw _err("crmf/bad-input", "opts.pem must be a non-empty PEM label string");
|
|
269
|
+
return crmf.pemEncode(der, opts.pem);
|
|
270
|
+
}
|
|
271
|
+
return der;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = { build: build };
|
package/lib/csr-sign.js
CHANGED
|
@@ -42,12 +42,6 @@ var _b = pkiBuild.makeBuilder({
|
|
|
42
42
|
NAME_SCHEMA: pkix.name(NS), SPKI_SCHEMA: pkix.spki(NS), EXT_DECODERS: EXT_DECODERS,
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
// The requested-extension object keys (a CSR REQUESTS extensions; it does NOT enforce issuance policy,
|
|
46
|
-
// so there are no CA cross-field gates and no authorityKeyIdentifier auto-derivation).
|
|
47
|
-
var KNOWN_REQ_EXT_KEYS = {
|
|
48
|
-
subjectAltName: 1, keyUsage: 1, keyUsageCritical: 1, extendedKeyUsage: 1, extendedKeyUsageCritical: 1,
|
|
49
|
-
basicConstraints: 1, certificatePolicies: 1, certificatePoliciesCritical: 1, subjectKeyIdentifier: 1,
|
|
50
|
-
};
|
|
51
45
|
// challengePassword DirectoryString charset (PrintableString set); anything else uses UTF8String.
|
|
52
46
|
var PRINTABLE_RE = /^[A-Za-z0-9 '()+,\-./:=?]*$/;
|
|
53
47
|
|
|
@@ -57,43 +51,9 @@ function _implicitSetOf0(members) {
|
|
|
57
51
|
return b.contextConstructed(0, Buffer.concat(members.slice().sort(Buffer.compare)));
|
|
58
52
|
}
|
|
59
53
|
|
|
60
|
-
// The requested v3 Extensions (a bare SEQUENCE OF Extension, RFC 2985 sec. 5.4.2)
|
|
61
|
-
//
|
|
62
|
-
function _buildRequestedExtensions(extSpec, subjectSpki) {
|
|
63
|
-
if (Array.isArray(extSpec)) {
|
|
64
|
-
if (!extSpec.length) throw _err("csr/bad-input", "extensionRequest must carry at least one extension");
|
|
65
|
-
var seen = {};
|
|
66
|
-
return b.sequence(extSpec.map(function (e, i) {
|
|
67
|
-
var der = _b.reqDer(e, "extension");
|
|
68
|
-
_b.assertValidExtension(der, i);
|
|
69
|
-
var n = asn1.decode(der);
|
|
70
|
-
var extnId = asn1.read.oid(n.children[0]);
|
|
71
|
-
if (seen[extnId]) throw _err("csr/bad-input", "duplicate extension " + extnId + " in extensionRequest (RFC 5280 sec. 4.2)");
|
|
72
|
-
seen[extnId] = true;
|
|
73
|
-
// Fully validate a RECOGNIZED extension via its real RFC 5280 sec. 4.2.1 value decoder (a malformed
|
|
74
|
-
// value fails closed with the decoder's typed code); an unrecognized extension stays opaque.
|
|
75
|
-
var dec = EXT_DECODERS[extnId];
|
|
76
|
-
if (dec) {
|
|
77
|
-
try { dec(asn1.read.octetString(n.children[n.children.length - 1])); }
|
|
78
|
-
catch (err) { if (err instanceof CsrError) throw err; throw _err("csr/bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension value is malformed", err); }
|
|
79
|
-
}
|
|
80
|
-
return b.raw(der);
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
if (!extSpec || typeof extSpec !== "object") throw _err("csr/bad-input", "extensionRequest must be an object or an array of pre-encoded Extension DER");
|
|
84
|
-
Object.keys(extSpec).forEach(function (k) {
|
|
85
|
-
if (!KNOWN_REQ_EXT_KEYS[k]) throw _err("csr/bad-input", "unknown extensionRequest extension " + JSON.stringify(k) + "; pass a pre-encoded Extension DER via the array form for a custom extension");
|
|
86
|
-
});
|
|
87
|
-
var out = [];
|
|
88
|
-
if (extSpec.subjectKeyIdentifier != null) out.push(_b.ext(O("subjectKeyIdentifier"), false, _b.extSki(_b.skiKeyId(extSpec.subjectKeyIdentifier, subjectSpki))));
|
|
89
|
-
if (extSpec.keyUsage != null) out.push(_b.ext(O("keyUsage"), extSpec.keyUsageCritical !== false, _b.extKeyUsage(extSpec.keyUsage)));
|
|
90
|
-
if (extSpec.extendedKeyUsage != null) out.push(_b.ext(O("extKeyUsage"), !!extSpec.extendedKeyUsageCritical, _b.extExtKeyUsage(extSpec.extendedKeyUsage)));
|
|
91
|
-
if (extSpec.basicConstraints != null) { _b.validateBcSpec(extSpec.basicConstraints); out.push(_b.ext(O("basicConstraints"), extSpec.basicConstraints.critical !== false, _b.extBasicConstraints(extSpec.basicConstraints))); }
|
|
92
|
-
if (extSpec.subjectAltName != null) out.push(_b.ext(O("subjectAltName"), false, _b.extSan(extSpec.subjectAltName)));
|
|
93
|
-
if (extSpec.certificatePolicies != null) out.push(_b.ext(O("certificatePolicies"), !!extSpec.certificatePoliciesCritical, _b.extCertPolicies(extSpec.certificatePolicies)));
|
|
94
|
-
if (!out.length) throw _err("csr/bad-input", "extensionRequest must request at least one extension");
|
|
95
|
-
return b.sequence(out);
|
|
96
|
-
}
|
|
54
|
+
// The requested v3 Extensions (a bare SEQUENCE OF Extension, RFC 2985 sec. 5.4.2) -- the shared builder
|
|
55
|
+
// primitive (a CSR REQUESTS extensions; no CA gates), object form or a pre-encoded Extension DER array.
|
|
56
|
+
function _buildRequestedExtensions(extSpec, subjectSpki) { return _b.requestedExtensions(extSpec, subjectSpki); }
|
|
97
57
|
|
|
98
58
|
// challengePassword ::= DirectoryString bounded 1..255 (RFC 2985 sec. 5.4.1). PrintableString when the
|
|
99
59
|
// value is in that charset, else UTF8String; never a Teletex/BMP/Universal string for a new value.
|
package/lib/pki-build.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
var asn1 = require("./asn1-der");
|
|
14
14
|
var schema = require("./schema-engine");
|
|
15
15
|
var compositeSig = require("./composite-sig");
|
|
16
|
+
var guard = require("./guard-all");
|
|
17
|
+
var oid = require("./oid");
|
|
16
18
|
var nodeCrypto = require("crypto");
|
|
17
19
|
|
|
18
20
|
var b = asn1.build;
|
|
@@ -31,6 +33,17 @@ function makeBuilder(ctx) {
|
|
|
31
33
|
var NAME_SCHEMA = ctx.NAME_SCHEMA, SPKI_SCHEMA = ctx.SPKI_SCHEMA;
|
|
32
34
|
function E(kind, message, cause) { return new ErrorClass(ctx.prefix + "/" + kind, message, cause); }
|
|
33
35
|
function code(kind) { return ctx.prefix + "/" + kind; }
|
|
36
|
+
// A full-code error factory (guard.* helpers invoke E as `E(fullCode, msg)`, not `E(kind, msg)`).
|
|
37
|
+
function rawErr(fullCode, message, cause) { return new ErrorClass(fullCode, message, cause); }
|
|
38
|
+
|
|
39
|
+
// RFC 5280 sec. 4.1.2.5 -- UTCTime for 1950..2049, GeneralizedTime otherwise (UTCTime's two-digit year
|
|
40
|
+
// only represents that window). The shared cutover so a certificate / CRMF / any validity encoder cannot
|
|
41
|
+
// drift on the boundary. `which` labels the instant in a validation error.
|
|
42
|
+
function timeDer(date, which) {
|
|
43
|
+
guard.time.assertValid(date, rawErr, code("bad-input"), which);
|
|
44
|
+
var y = date.getUTCFullYear();
|
|
45
|
+
return (y >= 1950 && y <= 2049) ? b.utcTime(date) : b.generalizedTime(date);
|
|
46
|
+
}
|
|
34
47
|
|
|
35
48
|
// ---- distinguished name encoding (RFC 5280 sec. 4.1.2.4) ----
|
|
36
49
|
// countryName is a PrintableString SIZE(2), emailAddress an IA5String; every other new-name attribute
|
|
@@ -226,6 +239,55 @@ function makeBuilder(ctx) {
|
|
|
226
239
|
var last = n.children[n.children.length - 1];
|
|
227
240
|
if (last.tagNumber !== asn1.TAGS.OCTET_STRING || last.tagClass !== "universal") throw E("bad-input", "pre-encoded extension [" + idx + "] extnValue must be an OCTET STRING");
|
|
228
241
|
}
|
|
242
|
+
// The requested v3 Extensions (a bare SEQUENCE OF Extension) from an object of the recognized keys, or
|
|
243
|
+
// an array of pre-encoded Extension DER (shape- + recognized-value-validated). Shared by every enrollment
|
|
244
|
+
// request that carries requested extensions (a PKCS#10 extensionRequest attribute, a CRMF CertTemplate
|
|
245
|
+
// extensions [9]); a request REQUESTS extensions, so there are no CA cross-field gates. `spki` feeds the
|
|
246
|
+
// subjectKeyIdentifier auto-derive.
|
|
247
|
+
var REQ_EXT_KEYS = {
|
|
248
|
+
subjectAltName: 1, keyUsage: 1, keyUsageCritical: 1, extendedKeyUsage: 1, extendedKeyUsageCritical: 1,
|
|
249
|
+
basicConstraints: 1, certificatePolicies: 1, certificatePoliciesCritical: 1, subjectKeyIdentifier: 1,
|
|
250
|
+
};
|
|
251
|
+
function requestedExtensions(extSpec, spki) {
|
|
252
|
+
var EXT_DECODERS = ctx.EXT_DECODERS;
|
|
253
|
+
if (Array.isArray(extSpec)) {
|
|
254
|
+
if (!extSpec.length) throw E("bad-input", "the requested extensions list must carry at least one extension");
|
|
255
|
+
var seen = {};
|
|
256
|
+
return b.sequence(extSpec.map(function (e, i) {
|
|
257
|
+
var der = reqDer(e, "extension");
|
|
258
|
+
assertValidExtension(der, i);
|
|
259
|
+
var n = asn1.decode(der);
|
|
260
|
+
var extnId = asn1.read.oid(n.children[0]);
|
|
261
|
+
if (seen[extnId]) throw E("bad-input", "duplicate requested extension " + extnId + " (RFC 5280 sec. 4.2)");
|
|
262
|
+
seen[extnId] = true;
|
|
263
|
+
var dec = EXT_DECODERS && EXT_DECODERS[extnId];
|
|
264
|
+
if (dec) {
|
|
265
|
+
try { dec(asn1.read.octetString(n.children[n.children.length - 1])); }
|
|
266
|
+
catch (err) { if (err instanceof ErrorClass) throw err; throw E("bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension value is malformed", err); }
|
|
267
|
+
}
|
|
268
|
+
return b.raw(der);
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
if (!extSpec || typeof extSpec !== "object") throw E("bad-input", "requested extensions must be an object or an array of pre-encoded Extension DER");
|
|
272
|
+
Object.keys(extSpec).forEach(function (k) {
|
|
273
|
+
if (!REQ_EXT_KEYS[k]) throw E("bad-input", "unknown requested extension " + JSON.stringify(k) + "; pass a pre-encoded Extension DER via the array form for a custom extension");
|
|
274
|
+
});
|
|
275
|
+
var out = [];
|
|
276
|
+
if (extSpec.subjectKeyIdentifier != null) {
|
|
277
|
+
// Auto-deriving the SKI (subjectKeyIdentifier: true) hashes the subject public key, so it needs one;
|
|
278
|
+
// reject the request when no public key is available (supply a Buffer key id instead).
|
|
279
|
+
if (extSpec.subjectKeyIdentifier === true && spki == null) throw E("bad-input", "subjectKeyIdentifier auto-derive (true) requires the public key -- supply a Buffer key id, or include the public key");
|
|
280
|
+
out.push(ext(O("subjectKeyIdentifier"), false, extSki(skiKeyId(extSpec.subjectKeyIdentifier, spki))));
|
|
281
|
+
}
|
|
282
|
+
if (extSpec.keyUsage != null) out.push(ext(O("keyUsage"), extSpec.keyUsageCritical !== false, extKeyUsage(extSpec.keyUsage)));
|
|
283
|
+
if (extSpec.extendedKeyUsage != null) out.push(ext(O("extKeyUsage"), !!extSpec.extendedKeyUsageCritical, extExtKeyUsage(extSpec.extendedKeyUsage)));
|
|
284
|
+
if (extSpec.basicConstraints != null) { validateBcSpec(extSpec.basicConstraints); out.push(ext(O("basicConstraints"), extSpec.basicConstraints.critical !== false, extBasicConstraints(extSpec.basicConstraints))); }
|
|
285
|
+
if (extSpec.subjectAltName != null) out.push(ext(O("subjectAltName"), false, extSan(extSpec.subjectAltName)));
|
|
286
|
+
if (extSpec.certificatePolicies != null) out.push(ext(O("certificatePolicies"), !!extSpec.certificatePoliciesCritical, extCertPolicies(extSpec.certificatePolicies)));
|
|
287
|
+
if (!out.length) throw E("bad-input", "the requested extensions object must request at least one extension");
|
|
288
|
+
return b.sequence(out);
|
|
289
|
+
}
|
|
290
|
+
|
|
229
291
|
// A positive, non-zero INTEGER of at most 20 content octets (RFC 5280 sec. 4.1.2.2 / RFC 5755 sec.
|
|
230
292
|
// 4.2.5 -- the identical serial rule for a certificate and an attribute certificate). A random 20-octet
|
|
231
293
|
// positive serial is generated when none is supplied. Accepts a BigInt, a safe integer, a decimal /
|
|
@@ -287,7 +349,8 @@ function makeBuilder(ctx) {
|
|
|
287
349
|
return {
|
|
288
350
|
E: E, code: code, KU_BIT: KU_BIT,
|
|
289
351
|
encodeName: encodeName, isEmptyName: isEmptyName, encodeGeneralName: encodeGeneralName,
|
|
290
|
-
encodeGeneralNames: encodeGeneralNames, serialInteger: serialInteger,
|
|
352
|
+
encodeGeneralNames: encodeGeneralNames, serialInteger: serialInteger, timeDer: timeDer,
|
|
353
|
+
requestedExtensions: requestedExtensions,
|
|
291
354
|
extKeyUsage: extKeyUsage, extExtKeyUsage: extExtKeyUsage, validateBcSpec: validateBcSpec,
|
|
292
355
|
extBasicConstraints: extBasicConstraints, pathLen: pathLen, extSki: extSki, extAki: extAki,
|
|
293
356
|
extSan: extSan, extCertPolicies: extCertPolicies, ext: ext,
|
package/lib/x509-sign.js
CHANGED
|
@@ -165,14 +165,9 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
165
165
|
|
|
166
166
|
// ---- serial + validity + key plumbing --------------------------------------
|
|
167
167
|
|
|
168
|
-
// RFC 5280 sec. 4.1.2.5
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
function _timeDer(date, which) {
|
|
172
|
-
guard.time.assertValid(date, _err, "x509/bad-input", "certificate " + which);
|
|
173
|
-
var y = date.getUTCFullYear();
|
|
174
|
-
return (y >= 1950 && y <= 2049) ? b.utcTime(date) : b.generalizedTime(date);
|
|
175
|
-
}
|
|
168
|
+
// RFC 5280 sec. 4.1.2.5 UTCTime/GeneralizedTime cutover -- the shared builder primitive (pki-build), a
|
|
169
|
+
// thin wrapper here so the certificate validity label reads "certificate notBefore/notAfter".
|
|
170
|
+
function _timeDer(date, which) { return _b.timeDer(date, "certificate " + which); }
|
|
176
171
|
// A supplied issuer certificate MUST be a CA that may sign certificates: basicConstraints present AND
|
|
177
172
|
// critical AND cA=TRUE (RFC 5280 sec. 4.2.1.9), and -- when a keyUsage extension is present -- the
|
|
178
173
|
// keyCertSign bit (sec. 4.2.1.3). Refuse a non-CA issuer rather than mint a certificate that will not
|
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:e238289a-30dc-45fc-b08a-4cc7a4f0a503",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-17T09:21:09.414Z",
|
|
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.3",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.3",
|
|
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.3",
|
|
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.3",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|