@blamejs/pki 0.3.1 → 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 +17 -0
- package/README.md +2 -0
- package/index.js +10 -0
- package/lib/asn1-der.js +9 -0
- package/lib/attrcert-sign.js +504 -0
- package/lib/cms-sign.js +2 -9
- package/lib/crmf-sign.js +275 -0
- package/lib/csr-sign.js +3 -43
- package/lib/pki-build.js +107 -4
- package/lib/schema-attrcert.js +17 -0
- package/lib/x509-sign.js +5 -38
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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
|
+
|
|
16
|
+
## v0.3.2 — 2026-07-17
|
|
17
|
+
|
|
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.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- pki.attrcert.sign(spec, issuer, opts) builds and signs an RFC 5755 attribute certificate as an Attribute Authority -- and returns DER, or a PEM ATTRIBUTE CERTIFICATE with opts.pem. The holder is exactly one of an entity name, a baseCertificateID, a fromCertificate binding derived from a public-key certificate, or an object digest; the attributes are the sec. 4.4 privilege syntaxes (role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo) or pre-encoded Attribute DER; the extensions are auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, and authorityKeyIdentifier or pre-encoded Extension DER, each carried with its RFC 5755 criticality. The AA signs with RSA PKCS#1 v1.5 / PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm, and the signature is verified under the AA public key before the certificate is returned. Attribute-certificate parsing remains pki.schema.attrcert.parse.
|
|
23
|
+
|
|
7
24
|
## v0.3.1 — 2026-07-17
|
|
8
25
|
|
|
9
26
|
pki.csr.sign issues PKCS#10 certification requests -- self-signed by the subject key for proof of possession, over every signature algorithm the toolkit supports.
|
package/README.md
CHANGED
|
@@ -224,6 +224,8 @@ is callable today; nothing below is a stub.
|
|
|
224
224
|
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA signatures — a composite is accepted only when **both** its post-quantum and traditional components verify; validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes, and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes via `checkPurpose`; `crlChecker` supplies CRL-based revocation — including partitioned/sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets a corresponding full-reason shard establish non-revocation — and `ocspChecker` supplies OCSP-based revocation (RFC 6960 — CertID binding, responder authorization, signature, currency) over the same pluggable hook. Pure and re-entrant, fail-closed — `validate`, `crlChecker`, `ocspChecker` |
|
|
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
|
+
| `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` |
|
|
227
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` |
|
|
228
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` |
|
|
229
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
|
@@ -42,6 +42,8 @@ var tsp = require("./lib/tsp-sign");
|
|
|
42
42
|
var ocsp = require("./lib/ocsp");
|
|
43
43
|
var x509 = require("./lib/x509-sign");
|
|
44
44
|
var csr = require("./lib/csr-sign");
|
|
45
|
+
var attrcert = require("./lib/attrcert-sign");
|
|
46
|
+
var crmf = require("./lib/crmf-sign");
|
|
45
47
|
var merkle = require("./lib/merkle");
|
|
46
48
|
var shbs = require("./lib/shbs");
|
|
47
49
|
var hpke = require("./lib/hpke");
|
|
@@ -93,6 +95,14 @@ module.exports = {
|
|
|
93
95
|
// possession, with requested extensions in a PKCS#9 extensionRequest attribute. Parsing
|
|
94
96
|
// lives at pki.schema.csr.parse.
|
|
95
97
|
csr: csr,
|
|
98
|
+
// `attrcert` is the RFC 5755 attribute-certificate producing side -- pki.attrcert.sign
|
|
99
|
+
// builds and signs an AttributeCertificate binding a Holder to privilege attributes as an
|
|
100
|
+
// Attribute Authority (never self-signed). Parsing lives at pki.schema.attrcert.parse.
|
|
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,
|
|
96
106
|
// `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
|
|
97
107
|
// pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
|
|
98
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
|
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @module pki.attrcert
|
|
7
|
+
* @nav Signing
|
|
8
|
+
* @title Attribute certificates
|
|
9
|
+
* @intro The RFC 5755 attribute-certificate producing side. `pki.attrcert.sign` builds an
|
|
10
|
+
* `AttributeCertificateInfo` binding a Holder to a set of privilege attributes (role, clearance,
|
|
11
|
+
* group, ...) over a validity window, signs it with an Attribute Authority's private key, and emits an
|
|
12
|
+
* `AttributeCertificate` that `pki.schema.attrcert.parse` accepts and re-validates byte for byte.
|
|
13
|
+
* Unlike a public-key certificate an attribute certificate is never self-signed -- the holder has no
|
|
14
|
+
* key, so the issuing AA is always a distinct signer. Parsing lives at `pki.schema.attrcert.parse`.
|
|
15
|
+
* @spec RFC 5755
|
|
16
|
+
* @card Build and sign an RFC 5755 attribute certificate as an Attribute Authority.
|
|
17
|
+
*/
|
|
18
|
+
//
|
|
19
|
+
// RFC 5755 is DEFINITIONS IMPLICIT TAGS (App. B): a context [n] on a non-CHOICE component REPLACES the
|
|
20
|
+
// underlying tag (its children ARE the component's fields -- Holder [0]/[1]/[2], AttCertIssuer v2Form [0],
|
|
21
|
+
// RoleSyntax roleAuthority [0], IetfAttrSyntax policyAuthority [0], AAControls [0]/[1]), built with
|
|
22
|
+
// b.contextConstructed(n, ...); a context [n] wrapping a GeneralName CHOICE (roleName [1], Target [0]/[1])
|
|
23
|
+
// is EXPLICIT, built with b.explicit(n, ...). The signature algorithm resolves from the AA public key
|
|
24
|
+
// through the shared sign-scheme registry (RSA / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite). The name /
|
|
25
|
+
// GeneralName / extension / SPKI encoders + the post-sign self-check are the shared lib/pki-build
|
|
26
|
+
// primitives; the AC-specific structures are the byte-exact inverse of the schema-attrcert.js readers,
|
|
27
|
+
// which re-validate every emitted structure on the round trip.
|
|
28
|
+
|
|
29
|
+
var asn1 = require("./asn1-der");
|
|
30
|
+
var oid = require("./oid");
|
|
31
|
+
var attrcert = require("./schema-attrcert");
|
|
32
|
+
var x509 = require("./schema-x509");
|
|
33
|
+
var signScheme = require("./sign-scheme");
|
|
34
|
+
var guard = require("./guard-all");
|
|
35
|
+
var pkix = require("./schema-pkix");
|
|
36
|
+
var pkiBuild = require("./pki-build");
|
|
37
|
+
var frameworkError = require("./framework-error");
|
|
38
|
+
|
|
39
|
+
var AttrCertError = frameworkError.AttrCertError;
|
|
40
|
+
var b = asn1.build;
|
|
41
|
+
function _err(code, message, cause) { return new AttrCertError(code, message, cause); }
|
|
42
|
+
function _signE(kind, message, cause) { return new AttrCertError("attrcert/" + kind, message, cause); }
|
|
43
|
+
function O(n) { return oid.byName(n); }
|
|
44
|
+
|
|
45
|
+
var NS = pkix.makeNS("attrcert", AttrCertError, oid);
|
|
46
|
+
var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
|
|
47
|
+
var _b = pkiBuild.makeBuilder({
|
|
48
|
+
ErrorClass: AttrCertError, prefix: "attrcert", O: O, NS: NS,
|
|
49
|
+
NAME_SCHEMA: pkix.name(NS), SPKI_SCHEMA: pkix.spki(NS), EXT_DECODERS: EXT_DECODERS,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// The recognized spec keys; an unknown key is a typo and throws at config-time (a custom attribute /
|
|
53
|
+
// extension is passed as pre-encoded DER via the array form).
|
|
54
|
+
var KNOWN_SPEC_KEYS = { holder: 1, notBeforeTime: 1, notAfterTime: 1, serialNumber: 1, attributes: 1, extensions: 1 };
|
|
55
|
+
var KNOWN_HOLDER_KEYS = { entityName: 1, baseCertificateID: 1, fromCertificate: 1, objectDigestInfo: 1 };
|
|
56
|
+
// object key -> the registered OID name of the AC attribute type it emits.
|
|
57
|
+
var ATTR_OID_NAME = {
|
|
58
|
+
role: "role", clearance: "clearance", group: "group", chargingIdentity: "chargingIdentity",
|
|
59
|
+
accessIdentity: "accessIdentity", authenticationInfo: "authenticationInfo",
|
|
60
|
+
};
|
|
61
|
+
// object key -> { oid name, critical } of the AC / AA extension it emits, with the RFC 5755 criticality:
|
|
62
|
+
// auditIdentity MUST be critical (sec. 4.3.1), targetInformation MUST be critical (sec. 4.3.2), proxying
|
|
63
|
+
// MUST be critical (sec. 7.2), noRevAvail MUST be non-critical (sec. 4.3.6), authorityKeyIdentifier MUST
|
|
64
|
+
// be non-critical (sec. 4.3.3); AAControls MAY be critical (sec. 7.4) and is emitted critical as the safer
|
|
65
|
+
// default for a delegation-constraint extension (a verifier that cannot process it MUST reject the AC).
|
|
66
|
+
var EXT_META = {
|
|
67
|
+
auditIdentity: { name: "acAuditIdentity", critical: true },
|
|
68
|
+
targetInformation: { name: "targetInformation", critical: true },
|
|
69
|
+
noRevAvail: { name: "noRevAvail", critical: false },
|
|
70
|
+
aaControls: { name: "aaControls", critical: true },
|
|
71
|
+
acProxying: { name: "acProxying", critical: true },
|
|
72
|
+
authorityKeyIdentifier: { name: "authorityKeyIdentifier", critical: false },
|
|
73
|
+
};
|
|
74
|
+
// The RFC 5755-mandated criticality per extension OID (from EXT_META), enforced on the pre-encoded array
|
|
75
|
+
// hatch so an escape-hatch extension cannot ship with a criticality the profile forbids.
|
|
76
|
+
var REQUIRED_CRITICALITY = {};
|
|
77
|
+
Object.keys(EXT_META).forEach(function (k) { REQUIRED_CRITICALITY[O(EXT_META[k].name)] = EXT_META[k].critical; });
|
|
78
|
+
// ClassList ::= BIT STRING { unmarked(0) .. topSecret(5) } (RFC 5755 sec. 4.4.4) -- the encode inverse of
|
|
79
|
+
// the schema-attrcert.js _CLASSLIST_NAMES decode array; DEFAULT {unclassified} (bit 1) is omitted.
|
|
80
|
+
var CLASSLIST_BIT = { unmarked: 0, unclassified: 1, restricted: 2, confidential: 3, secret: 4, topSecret: 5 };
|
|
81
|
+
|
|
82
|
+
var _tbsNameBytes = pkiBuild.tbsNameField; // the AA issuerName / holder baseCertificateID chain exactly to a cert's DN
|
|
83
|
+
// Parse a certificate DER/PEM (or accept a parsed certificate), re-typing a raw x509/* parse fault to the
|
|
84
|
+
// attrcert domain so a malformed AA cert / holder cert surfaces attrcert/*, not a foreign CertificateError.
|
|
85
|
+
function _parseCert(cert, what) {
|
|
86
|
+
if (!Buffer.isBuffer(cert) && typeof cert !== "string") {
|
|
87
|
+
if (!cert || !cert.tbsBytes) throw _err("attrcert/bad-input", what + " must be a certificate DER/PEM or a parsed certificate");
|
|
88
|
+
return cert;
|
|
89
|
+
}
|
|
90
|
+
try { return x509.parse(cert); }
|
|
91
|
+
catch (e) { if (e instanceof AttrCertError) throw e; throw _err("attrcert/bad-input", what + " is not a well-formed certificate", e); }
|
|
92
|
+
}
|
|
93
|
+
// The raw content octets of an OBJECT IDENTIFIER (past its own tag+len) -- the body of a [0] IMPLICIT OID.
|
|
94
|
+
function _oidContent(name) {
|
|
95
|
+
var dotted = O(name) || name;
|
|
96
|
+
var enc;
|
|
97
|
+
try { enc = b.oid(dotted); } catch (e) { throw _err("attrcert/bad-input", "not a valid object identifier: " + JSON.stringify(name), e); }
|
|
98
|
+
return asn1.decode(enc).content;
|
|
99
|
+
}
|
|
100
|
+
// A GeneralNames spec (a single GeneralName object or an array of them) -> the member list.
|
|
101
|
+
function _gnList(spec) {
|
|
102
|
+
if (Array.isArray(spec)) return spec;
|
|
103
|
+
return [spec];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- structural encoders (byte-exact inverses of the schema-attrcert.js readers) ----
|
|
107
|
+
|
|
108
|
+
// IssuerSerial ::= SEQUENCE { issuer GeneralNames, serial CertificateSerialNumber, issuerUID BIT STRING
|
|
109
|
+
// OPTIONAL }. Returns the CONTENT (concatenated field TLVs) so a [n] IMPLICIT caller wraps it directly.
|
|
110
|
+
function _issuerSerialContent(is) {
|
|
111
|
+
if (!is || typeof is !== "object" || Buffer.isBuffer(is)) throw _err("attrcert/bad-input", "baseCertificateID must be an object { issuer, serial, issuerUID? }");
|
|
112
|
+
if (is.issuer == null) throw _err("attrcert/bad-input", "baseCertificateID.issuer (a GeneralNames) is required");
|
|
113
|
+
var parts = [_b.encodeGeneralNames(_gnList(is.issuer)), _b.serialInteger(is.serial)];
|
|
114
|
+
if (is.issuerUID != null) {
|
|
115
|
+
if (!Buffer.isBuffer(is.issuerUID)) throw _err("attrcert/bad-input", "baseCertificateID.issuerUID must be a Buffer");
|
|
116
|
+
parts.push(b.bitString(is.issuerUID, 0));
|
|
117
|
+
}
|
|
118
|
+
return Buffer.concat(parts);
|
|
119
|
+
}
|
|
120
|
+
// ObjectDigestInfo ::= SEQUENCE { digestedObjectType ENUMERATED, otherObjectTypeID OID OPTIONAL,
|
|
121
|
+
// digestAlgorithm AlgorithmIdentifier, objectDigest BIT STRING }. otherObjectTypes(2) is forbidden (sec.
|
|
122
|
+
// 7.3), so otherObjectTypeID is never emitted. Returns the CONTENT for a [n] IMPLICIT caller.
|
|
123
|
+
var ODT_CODE = { publicKey: 0, publicKeyCert: 1 };
|
|
124
|
+
function _objectDigestInfoContent(odi) {
|
|
125
|
+
if (!odi || typeof odi !== "object" || Buffer.isBuffer(odi)) throw _err("attrcert/bad-input", "objectDigestInfo must be an object");
|
|
126
|
+
var code = ODT_CODE[odi.digestedObjectType];
|
|
127
|
+
if (code == null) throw _err("attrcert/bad-input", "objectDigestInfo.digestedObjectType must be 'publicKey' or 'publicKeyCert' (otherObjectTypes is forbidden, RFC 5755 sec. 7.3)");
|
|
128
|
+
if (!Buffer.isBuffer(odi.objectDigest)) throw _err("attrcert/bad-input", "objectDigestInfo.objectDigest must be a Buffer (the whole-octet digest)");
|
|
129
|
+
var algName = odi.digestAlgorithm;
|
|
130
|
+
var algOid = O(algName) || algName;
|
|
131
|
+
var algTlv;
|
|
132
|
+
try { algTlv = b.sequence([b.oid(algOid)]); } catch (e) { throw _err("attrcert/bad-input", "objectDigestInfo.digestAlgorithm is not a valid algorithm identifier", e); }
|
|
133
|
+
return Buffer.concat([b.enumerated(BigInt(code)), algTlv, b.bitString(odi.objectDigest, 0)]);
|
|
134
|
+
}
|
|
135
|
+
// Holder ::= SEQUENCE { baseCertificateID [0] IMPLICIT IssuerSerial, entityName [1] IMPLICIT GeneralNames,
|
|
136
|
+
// objectDigestInfo [2] IMPLICIT ObjectDigestInfo } -- exactly one form (the profile binds a real holder).
|
|
137
|
+
function _encodeHolder(holder) {
|
|
138
|
+
if (!holder || typeof holder !== "object" || Buffer.isBuffer(holder)) throw _err("attrcert/bad-input", "holder must be an object with exactly one form");
|
|
139
|
+
Object.keys(holder).forEach(function (k) { if (!KNOWN_HOLDER_KEYS[k]) throw _err("attrcert/bad-input", "unknown holder form " + JSON.stringify(k)); });
|
|
140
|
+
var forms = Object.keys(holder).filter(function (k) { return holder[k] != null; });
|
|
141
|
+
if (forms.length !== 1) throw _err("attrcert/bad-input", "holder must carry exactly one form (entityName, baseCertificateID, fromCertificate, or objectDigestInfo), got " + forms.length);
|
|
142
|
+
if (holder.entityName != null) {
|
|
143
|
+
// entityName [1] IMPLICIT GeneralNames -> the [1] node's children ARE the GeneralName members.
|
|
144
|
+
return b.sequence([_b.encodeGeneralNames(_gnList(holder.entityName), 1)]);
|
|
145
|
+
}
|
|
146
|
+
if (holder.baseCertificateID != null) {
|
|
147
|
+
return b.sequence([b.contextConstructed(0, _issuerSerialContent(holder.baseCertificateID))]);
|
|
148
|
+
}
|
|
149
|
+
if (holder.fromCertificate != null) {
|
|
150
|
+
// Bind to a public-key certificate's identity: baseCertificateID = { issuer = the PKC's issuer DN as a
|
|
151
|
+
// directoryName, serial = the PKC serialNumber } (RFC 5755 sec. 4.1 / 7.3).
|
|
152
|
+
var pkc = _parseCert(holder.fromCertificate, "holder.fromCertificate");
|
|
153
|
+
var content = _issuerSerialContent({ issuer: [{ directoryName: _tbsNameBytes(pkc, "issuer") }], serial: pkc.serialNumber });
|
|
154
|
+
return b.sequence([b.contextConstructed(0, content)]);
|
|
155
|
+
}
|
|
156
|
+
// objectDigestInfo [2] IMPLICIT ObjectDigestInfo.
|
|
157
|
+
return b.sequence([b.contextConstructed(2, _objectDigestInfoContent(holder.objectDigestInfo))]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// AttCertIssuer v2Form [0] IMPLICIT V2Form { issuerName GeneralNames } -- issuerName is exactly one
|
|
161
|
+
// directoryName [4] over a non-empty DN (RFC 5755 sec. 4.2.3). The [0] replaces the V2Form SEQUENCE tag.
|
|
162
|
+
function _encodeV2FormIssuer(dnSpec) {
|
|
163
|
+
var dnDer = _b.encodeName(dnSpec == null ? [] : dnSpec);
|
|
164
|
+
if (_b.isEmptyName(dnDer)) throw _err("attrcert/bad-issuer-name", "the AA issuerName must be a non-empty distinguished name (RFC 5755 sec. 4.2.3)");
|
|
165
|
+
var issuerNameGns = _b.encodeGeneralNames([{ directoryName: dnDer }]); // a universal SEQUENCE OF one directoryName
|
|
166
|
+
return b.contextConstructed(0, issuerNameGns);
|
|
167
|
+
}
|
|
168
|
+
// AttCertValidityPeriod ::= SEQUENCE { notBeforeTime GeneralizedTime, notAfterTime GeneralizedTime }
|
|
169
|
+
// (RFC 5755 sec. 4.2.6) -- ALWAYS GeneralizedTime, never UTCTime; reject an inverted window.
|
|
170
|
+
function _encodeValidity(notBefore, notAfter) {
|
|
171
|
+
guard.time.assertValid(notBefore, _err, "attrcert/bad-input", "notBeforeTime");
|
|
172
|
+
guard.time.assertValid(notAfter, _err, "attrcert/bad-input", "notAfterTime");
|
|
173
|
+
// allow:nan-date-comparison-unguarded -- both operands are guard.time.assertValid'd above.
|
|
174
|
+
if (notBefore.getTime() > notAfter.getTime()) throw _err("attrcert/bad-input", "notBeforeTime must not be after notAfterTime (RFC 5755 sec. 4.2.6)");
|
|
175
|
+
return b.sequence([b.generalizedTime(notBefore), b.generalizedTime(notAfter)]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---- attribute-value encoders (sec. 4.4) ----
|
|
179
|
+
|
|
180
|
+
// RoleSyntax ::= SEQUENCE { roleAuthority [0] IMPLICIT GeneralNames OPTIONAL, roleName [1] EXPLICIT
|
|
181
|
+
// GeneralName }. roleName is a GeneralName CHOICE -> EXPLICIT; roleAuthority a GeneralNames -> IMPLICIT [0].
|
|
182
|
+
function _encodeRole(role) {
|
|
183
|
+
if (!role || typeof role !== "object" || role.roleName == null) throw _err("attrcert/bad-input", "role must be an object with a roleName GeneralName");
|
|
184
|
+
var parts = [];
|
|
185
|
+
if (role.roleAuthority != null) parts.push(_b.encodeGeneralNames(_gnList(role.roleAuthority), 0));
|
|
186
|
+
parts.push(b.explicit(1, _b.encodeGeneralName(role.roleName)));
|
|
187
|
+
return b.sequence(parts);
|
|
188
|
+
}
|
|
189
|
+
// Clearance ::= SEQUENCE { policyId OID, classList BIT STRING DEFAULT {unclassified}, securityCategories
|
|
190
|
+
// SET OF SecurityCategory OPTIONAL } (RFC 5755 sec. 4.4.4). A classList equal to the DEFAULT is omitted.
|
|
191
|
+
function _encodeClearance(cl) {
|
|
192
|
+
if (!cl || typeof cl !== "object" || cl.policyId == null) throw _err("attrcert/bad-input", "clearance must be an object with a policyId OID");
|
|
193
|
+
var parts;
|
|
194
|
+
try { parts = [b.oid(O(cl.policyId) || cl.policyId)]; } catch (e) { throw _err("attrcert/bad-input", "clearance.policyId is not a valid object identifier", e); }
|
|
195
|
+
if (cl.classList != null) {
|
|
196
|
+
if (!Array.isArray(cl.classList)) throw _err("attrcert/bad-input", "clearance.classList must be an array of class names");
|
|
197
|
+
var positions = cl.classList.map(function (n) {
|
|
198
|
+
var pos = CLASSLIST_BIT[n];
|
|
199
|
+
if (pos == null) throw _err("attrcert/bad-input", "unknown clearance class " + JSON.stringify(n));
|
|
200
|
+
return pos;
|
|
201
|
+
});
|
|
202
|
+
// A present classList equal to the DEFAULT {unclassified} is non-canonical DER (the parser rejects it).
|
|
203
|
+
var isDefault = positions.length === 1 && positions[0] === CLASSLIST_BIT.unclassified;
|
|
204
|
+
if (!isDefault) parts.push(b.namedBitString(positions));
|
|
205
|
+
}
|
|
206
|
+
if (cl.securityCategories != null) {
|
|
207
|
+
if (!Array.isArray(cl.securityCategories) || !cl.securityCategories.length) throw _err("attrcert/bad-input", "clearance.securityCategories must be a non-empty array");
|
|
208
|
+
parts.push(b.set(cl.securityCategories.map(_encodeSecurityCategory)));
|
|
209
|
+
}
|
|
210
|
+
return b.sequence(parts);
|
|
211
|
+
}
|
|
212
|
+
// SecurityCategory ::= SEQUENCE { type [0] IMPLICIT OBJECT IDENTIFIER, value [1] EXPLICIT ANY }. The
|
|
213
|
+
// value is one element the parser EXPLICIT [1] wrapper carries verbatim, so it MUST be a single
|
|
214
|
+
// well-formed DER TLV -- validate it (no malformed / multi-element / trailing bytes) before signing.
|
|
215
|
+
function _encodeSecurityCategory(sc) {
|
|
216
|
+
if (!sc || typeof sc !== "object" || sc.type == null || !Buffer.isBuffer(sc.value)) throw _err("attrcert/bad-input", "each securityCategory must be { type: OID, value: DER Buffer }");
|
|
217
|
+
var node;
|
|
218
|
+
try { node = asn1.decode(sc.value); } catch (e) { throw _err("attrcert/bad-input", "securityCategory value must be a single well-formed DER element", e); }
|
|
219
|
+
if (node.bytes.length !== sc.value.length) throw _err("attrcert/bad-input", "securityCategory value must be exactly one DER element (no trailing bytes)");
|
|
220
|
+
return b.sequence([b.contextPrimitive(0, _oidContent(sc.type)), b.explicit(1, sc.value)]);
|
|
221
|
+
}
|
|
222
|
+
// IetfAttrSyntax ::= SEQUENCE { policyAuthority [0] IMPLICIT GeneralNames OPTIONAL, values SEQUENCE OF
|
|
223
|
+
// CHOICE { octets OCTET STRING, oid OBJECT IDENTIFIER, string UTF8String } } (group / chargingIdentity).
|
|
224
|
+
function _encodeIetfAttrSyntax(ia) {
|
|
225
|
+
if (!ia || typeof ia !== "object" || !Array.isArray(ia.values) || !ia.values.length) throw _err("attrcert/bad-input", "an IetfAttrSyntax attribute must be an object with a non-empty values array");
|
|
226
|
+
var parts = [];
|
|
227
|
+
if (ia.policyAuthority != null) parts.push(_b.encodeGeneralNames(_gnList(ia.policyAuthority), 0));
|
|
228
|
+
parts.push(b.sequence(ia.values.map(function (v) {
|
|
229
|
+
if (v == null || typeof v !== "object") throw _err("attrcert/bad-input", "each IetfAttrSyntax value must be { octets }, { oid }, or { string }");
|
|
230
|
+
if (v.octets != null) { if (!Buffer.isBuffer(v.octets)) throw _err("attrcert/bad-input", "IetfAttrSyntax octets must be a Buffer"); return b.octetString(v.octets); }
|
|
231
|
+
if (v.oid != null) { try { return b.oid(O(v.oid) || v.oid); } catch (e) { throw _err("attrcert/bad-input", "IetfAttrSyntax oid is not a valid object identifier", e); } }
|
|
232
|
+
if (v.string != null) return b.utf8(String(v.string));
|
|
233
|
+
throw _err("attrcert/bad-input", "each IetfAttrSyntax value must be { octets }, { oid }, or { string }");
|
|
234
|
+
})));
|
|
235
|
+
return b.sequence(parts);
|
|
236
|
+
}
|
|
237
|
+
// SvceAuthInfo ::= SEQUENCE { service GeneralName, ident GeneralName, authInfo OCTET STRING OPTIONAL }.
|
|
238
|
+
// authenticationInfo (sec. 4.4.1) permits authInfo; accessIdentity (sec. 4.4.2) MUST omit it.
|
|
239
|
+
function _encodeSvceAuthInfo(sai, authInfoAllowed, label) {
|
|
240
|
+
if (!sai || typeof sai !== "object" || sai.service == null || sai.ident == null) throw _err("attrcert/bad-input", label + " must be an object with a service and an ident GeneralName");
|
|
241
|
+
var parts = [_b.encodeGeneralName(sai.service), _b.encodeGeneralName(sai.ident)];
|
|
242
|
+
if (sai.authInfo != null) {
|
|
243
|
+
if (!authInfoAllowed) throw _err("attrcert/bad-input", "accessIdentity must not carry authInfo (RFC 5755 sec. 4.4.2)");
|
|
244
|
+
if (!Buffer.isBuffer(sai.authInfo)) throw _err("attrcert/bad-input", label + " authInfo must be a Buffer");
|
|
245
|
+
parts.push(b.octetString(sai.authInfo));
|
|
246
|
+
}
|
|
247
|
+
return b.sequence(parts);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
var ATTR_VALUE_ENCODER = {
|
|
251
|
+
role: function (v) { return _encodeRole(v); },
|
|
252
|
+
clearance: function (v) { return _encodeClearance(v); },
|
|
253
|
+
group: function (v) { return _encodeIetfAttrSyntax(v); },
|
|
254
|
+
chargingIdentity: function (v) { return _encodeIetfAttrSyntax(v); },
|
|
255
|
+
authenticationInfo: function (v) { return _encodeSvceAuthInfo(v, true, "authenticationInfo"); },
|
|
256
|
+
accessIdentity: function (v) { return _encodeSvceAuthInfo(v, false, "accessIdentity"); },
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// ---- extension-value encoders (sec. 4.3 / 7.4) ----
|
|
260
|
+
|
|
261
|
+
// Target ::= CHOICE { targetName [0] EXPLICIT GeneralName, targetGroup [1] EXPLICIT GeneralName }
|
|
262
|
+
// (targetCert [2] is out of v1 scope -- use a pre-encoded Extension DER for a targetCert-bearing value).
|
|
263
|
+
function _encodeTarget(t) {
|
|
264
|
+
if (!t || typeof t !== "object") throw _err("attrcert/bad-input", "each target must be { targetName } or { targetGroup }");
|
|
265
|
+
if (t.targetName != null) return b.explicit(0, _b.encodeGeneralName(t.targetName));
|
|
266
|
+
if (t.targetGroup != null) return b.explicit(1, _b.encodeGeneralName(t.targetGroup));
|
|
267
|
+
throw _err("attrcert/bad-input", "each target must be { targetName } or { targetGroup }");
|
|
268
|
+
}
|
|
269
|
+
// targetInformation / acProxying value ::= SEQUENCE OF Targets, Targets ::= SEQUENCE OF Target. A
|
|
270
|
+
// conformant issuer emits exactly one Targets.
|
|
271
|
+
function _encodeSeqOfTargets(targets) {
|
|
272
|
+
if (!Array.isArray(targets) || !targets.length) throw _err("attrcert/bad-input", "a targets value must be a non-empty array of targets");
|
|
273
|
+
return b.sequence([b.sequence(targets.map(_encodeTarget))]);
|
|
274
|
+
}
|
|
275
|
+
// AAControls ::= SEQUENCE { pathLenConstraint INTEGER OPTIONAL, permittedAttrs [0] IMPLICIT AttrSpec
|
|
276
|
+
// OPTIONAL, excludedAttrs [1] IMPLICIT AttrSpec OPTIONAL, permitUnSpecified BOOLEAN DEFAULT TRUE }.
|
|
277
|
+
// AttrSpec ::= SEQUENCE OF OID (the [0]/[1] IMPLICIT tag replaces the SEQUENCE tag).
|
|
278
|
+
function _attrSpec(names, tag, label) {
|
|
279
|
+
if (!Array.isArray(names)) throw _err("attrcert/bad-input", "aaControls." + label + " must be an array of attribute-type OIDs");
|
|
280
|
+
var oids = names.map(function (n) {
|
|
281
|
+
try { return b.oid(O(n) || n); } catch (e) { throw _err("attrcert/bad-input", "aaControls." + label + " contains an invalid object identifier " + JSON.stringify(n), e); }
|
|
282
|
+
});
|
|
283
|
+
return b.contextConstructed(tag, Buffer.concat(oids));
|
|
284
|
+
}
|
|
285
|
+
function _encodeAAControls(aac) {
|
|
286
|
+
if (!aac || typeof aac !== "object" || Buffer.isBuffer(aac)) throw _err("attrcert/bad-input", "aaControls must be an object");
|
|
287
|
+
var parts = [];
|
|
288
|
+
if (aac.pathLenConstraint != null) {
|
|
289
|
+
// The parser narrows pathLenConstraint through guard.range.uint31 (0..2^31-1), so a larger value the
|
|
290
|
+
// builder emits would fail the round trip -- bound it to the same range at build time.
|
|
291
|
+
if (typeof aac.pathLenConstraint !== "number" || !Number.isInteger(aac.pathLenConstraint) || aac.pathLenConstraint < 0 || aac.pathLenConstraint > 0x7fffffff) throw _err("attrcert/bad-input", "aaControls.pathLenConstraint must be an integer in 0..2147483647 (RFC 5755 sec. 7.4)");
|
|
292
|
+
parts.push(b.integer(BigInt(aac.pathLenConstraint)));
|
|
293
|
+
}
|
|
294
|
+
if (aac.permittedAttrs != null) parts.push(_attrSpec(aac.permittedAttrs, 0, "permittedAttrs"));
|
|
295
|
+
if (aac.excludedAttrs != null) parts.push(_attrSpec(aac.excludedAttrs, 1, "excludedAttrs"));
|
|
296
|
+
// permitUnSpecified DEFAULT TRUE -- a TRUE value equals the DEFAULT and is omitted (non-canonical DER).
|
|
297
|
+
if (aac.permitUnSpecified === false) parts.push(b.boolean(false));
|
|
298
|
+
else if (aac.permitUnSpecified != null && aac.permitUnSpecified !== true) throw _err("attrcert/bad-input", "aaControls.permitUnSpecified must be a boolean");
|
|
299
|
+
return b.sequence(parts);
|
|
300
|
+
}
|
|
301
|
+
// Emit a single AC / AA extension's value DER from its object-form spec.
|
|
302
|
+
function _extensionValue(key, val, aaSpki) {
|
|
303
|
+
switch (key) {
|
|
304
|
+
case "auditIdentity":
|
|
305
|
+
if (!Buffer.isBuffer(val)) throw _err("attrcert/bad-input", "auditIdentity must be a Buffer (an OCTET STRING audit tag)");
|
|
306
|
+
return b.octetString(val);
|
|
307
|
+
case "targetInformation": return _encodeSeqOfTargets(val);
|
|
308
|
+
case "acProxying": return _encodeSeqOfTargets(val);
|
|
309
|
+
case "noRevAvail": return b.nullValue();
|
|
310
|
+
case "aaControls": return _encodeAAControls(val);
|
|
311
|
+
case "authorityKeyIdentifier": return _b.extAki(_b.skiKeyId(val, aaSpki));
|
|
312
|
+
default: throw _err("attrcert/bad-input", "unknown extension " + JSON.stringify(key));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---- attributes / extensions assembly ----
|
|
317
|
+
|
|
318
|
+
// attributes ::= SEQUENCE OF Attribute (RFC 5755 sec. 4.2.7) -- non-empty, each AttributeType OID unique;
|
|
319
|
+
// or an array of pre-encoded Attribute DER (validated in shape AND value). Each Attribute value SET is
|
|
320
|
+
// DER-sorted.
|
|
321
|
+
function _buildAttributes(attrSpec) {
|
|
322
|
+
var attrs = [], seen = {};
|
|
323
|
+
function add(type, valueTlv) {
|
|
324
|
+
if (seen[type]) throw _err("attrcert/duplicate-attribute", "duplicate " + (oid.name(type) || type) + " attribute (RFC 5755 sec. 4.2.7)");
|
|
325
|
+
seen[type] = true;
|
|
326
|
+
attrs.push(b.sequence([b.oid(type), b.set([valueTlv])]));
|
|
327
|
+
}
|
|
328
|
+
if (Array.isArray(attrSpec)) {
|
|
329
|
+
attrSpec.forEach(function (a, i) {
|
|
330
|
+
var der = _b.reqDer(a, "attribute [" + i + "]");
|
|
331
|
+
var n;
|
|
332
|
+
try { n = asn1.decode(der); } catch (e) { throw _err("attrcert/bad-input", "pre-encoded attribute [" + i + "] is not valid DER", e); }
|
|
333
|
+
if (n.tagNumber !== asn1.TAGS.SEQUENCE || n.tagClass !== "universal" || !n.children || n.children.length !== 2 || n.children[1].tagNumber !== asn1.TAGS.SET) throw _err("attrcert/bad-input", "pre-encoded attribute [" + i + "] must be an Attribute SEQUENCE { type OID, SET OF value }");
|
|
334
|
+
var at;
|
|
335
|
+
try { at = asn1.read.oid(n.children[0]); } catch (e) { throw _err("attrcert/bad-input", "pre-encoded attribute [" + i + "] type is not an OBJECT IDENTIFIER", e); }
|
|
336
|
+
if (!n.children[1].children || !n.children[1].children.length) throw _err("attrcert/bad-input", "pre-encoded attribute [" + i + "] value SET must contain at least one value");
|
|
337
|
+
if (seen[at]) throw _err("attrcert/duplicate-attribute", "duplicate " + (oid.name(at) || at) + " attribute (RFC 5755 sec. 4.2.7)");
|
|
338
|
+
seen[at] = true;
|
|
339
|
+
// A recognized AC attribute type is validated against its real sec. 4.4 value decoder; an
|
|
340
|
+
// unrecognized type stays opaque (the parser also leaves it opaque).
|
|
341
|
+
n.children[1].children.forEach(function (valNode) {
|
|
342
|
+
try { attrcert.validateAttributeValue(at, valNode.bytes); }
|
|
343
|
+
catch (err) { if (err instanceof AttrCertError) throw err; throw _err("attrcert/bad-input", "pre-encoded " + (oid.name(at) || at) + " attribute value is malformed", err); }
|
|
344
|
+
});
|
|
345
|
+
attrs.push(b.raw(der));
|
|
346
|
+
});
|
|
347
|
+
if (!attrs.length) throw _err("attrcert/bad-attributes", "attributes must carry at least one Attribute (RFC 5755 sec. 4.2.7)");
|
|
348
|
+
return b.sequence(attrs);
|
|
349
|
+
}
|
|
350
|
+
if (!attrSpec || typeof attrSpec !== "object") throw _err("attrcert/bad-input", "attributes must be an object or an array of pre-encoded Attribute DER");
|
|
351
|
+
Object.keys(attrSpec).forEach(function (k) {
|
|
352
|
+
if (!ATTR_VALUE_ENCODER[k]) throw _err("attrcert/bad-input", "unknown attribute " + JSON.stringify(k) + "; pass a pre-encoded Attribute DER via the array form for a custom attribute");
|
|
353
|
+
add(O(ATTR_OID_NAME[k]), ATTR_VALUE_ENCODER[k](attrSpec[k]));
|
|
354
|
+
});
|
|
355
|
+
if (!attrs.length) throw _err("attrcert/bad-attributes", "attributes must carry at least one Attribute (RFC 5755 sec. 4.2.7)");
|
|
356
|
+
return b.sequence(attrs);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// extensions ::= SEQUENCE OF Extension (RFC 5755 sec. 4.2.9) -- the recognized object form, or an array
|
|
360
|
+
// of pre-encoded Extension DER. At most one instance of a particular extension OID. Returns a universal
|
|
361
|
+
// SEQUENCE OF Extension, or null when none are requested.
|
|
362
|
+
function _buildExtensions(extSpec, aaSpki) {
|
|
363
|
+
if (extSpec == null) return null;
|
|
364
|
+
if (Array.isArray(extSpec)) {
|
|
365
|
+
if (!extSpec.length) return null;
|
|
366
|
+
var seenA = {};
|
|
367
|
+
var exts = extSpec.map(function (e, i) {
|
|
368
|
+
var der = _b.reqDer(e, "extension");
|
|
369
|
+
_b.assertValidExtension(der, i);
|
|
370
|
+
var n = asn1.decode(der);
|
|
371
|
+
var extnId = asn1.read.oid(n.children[0]);
|
|
372
|
+
if (seenA[extnId]) throw _err("attrcert/bad-input", "duplicate extension " + extnId + " (RFC 5755 sec. 4.2.9)");
|
|
373
|
+
seenA[extnId] = true;
|
|
374
|
+
// A recognized RFC 5755-profiled extension MUST carry its mandated criticality even via the escape
|
|
375
|
+
// hatch (assertValidExtension already rejected an explicit critical=FALSE, so 3 children == critical).
|
|
376
|
+
var required = REQUIRED_CRITICALITY[extnId];
|
|
377
|
+
if (required != null && (n.children.length === 3) !== required) {
|
|
378
|
+
throw _err("attrcert/bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension must be marked " + (required ? "critical" : "non-critical") + " (RFC 5755)");
|
|
379
|
+
}
|
|
380
|
+
// Validate a recognized value against its real decoder: a cert-style AA extension (AKI / CRLDP /
|
|
381
|
+
// AIA) through the RFC 5280 sec. 4.2.1 table, an AC-specific extension through the sec. 4.3 table.
|
|
382
|
+
var extnValue = asn1.read.octetString(n.children[n.children.length - 1]);
|
|
383
|
+
try {
|
|
384
|
+
if (EXT_DECODERS[extnId]) EXT_DECODERS[extnId](extnValue);
|
|
385
|
+
else attrcert.validateExtensionValue(extnId, extnValue);
|
|
386
|
+
} catch (err) { if (err instanceof AttrCertError) throw err; throw _err("attrcert/bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension value is malformed", err); }
|
|
387
|
+
return b.raw(der);
|
|
388
|
+
});
|
|
389
|
+
return b.sequence(exts);
|
|
390
|
+
}
|
|
391
|
+
if (typeof extSpec !== "object") throw _err("attrcert/bad-input", "extensions must be an object or an array of pre-encoded Extension DER");
|
|
392
|
+
Object.keys(extSpec).forEach(function (k) {
|
|
393
|
+
if (!EXT_META[k]) throw _err("attrcert/bad-input", "unknown extension " + JSON.stringify(k) + "; pass a pre-encoded Extension DER via the array form for a custom extension");
|
|
394
|
+
});
|
|
395
|
+
var out = [], seen = {};
|
|
396
|
+
Object.keys(extSpec).forEach(function (k) {
|
|
397
|
+
if (extSpec[k] == null) return; // a null/omitted extension is not requested (matches the attribute form)
|
|
398
|
+
var meta = EXT_META[k], eOid = O(meta.name);
|
|
399
|
+
if (seen[eOid]) throw _err("attrcert/bad-input", "duplicate extension " + JSON.stringify(k) + " (RFC 5755 sec. 4.2.9)");
|
|
400
|
+
seen[eOid] = true;
|
|
401
|
+
out.push(_b.ext(eOid, meta.critical, _extensionValue(k, extSpec[k], aaSpki)));
|
|
402
|
+
});
|
|
403
|
+
if (!out.length) return null;
|
|
404
|
+
return b.sequence(out);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* @primitive pki.attrcert.sign
|
|
409
|
+
* @signature pki.attrcert.sign(spec, issuer, opts?) -> Promise<Buffer|string>
|
|
410
|
+
* @since 0.3.2
|
|
411
|
+
* @status experimental
|
|
412
|
+
* @spec RFC 5755
|
|
413
|
+
* @defends forged-attribute-certificate (CWE-347)
|
|
414
|
+
* @related pki.schema.attrcert.parse, pki.x509.sign
|
|
415
|
+
*
|
|
416
|
+
* Build, sign, and DER-encode an RFC 5755 attribute certificate as an Attribute Authority. `spec`
|
|
417
|
+
* describes the certificate -- `holder` (exactly one form: `entityName`, `baseCertificateID`,
|
|
418
|
+
* `fromCertificate` to bind a public-key certificate's identity, or `objectDigestInfo`), `notBeforeTime`
|
|
419
|
+
* / `notAfterTime` (`Date`s -> GeneralizedTime), an optional `serialNumber` (positive, <= 20 octets;
|
|
420
|
+
* a random 20-octet serial is generated when omitted), `attributes` (an object of the sec. 4.4 privilege
|
|
421
|
+
* syntaxes -- role / clearance / group / chargingIdentity / accessIdentity / authenticationInfo -- or an
|
|
422
|
+
* array of pre-encoded Attribute DER), and optional `extensions` (an object of auditIdentity /
|
|
423
|
+
* targetInformation / noRevAvail / aaControls / acProxying / authorityKeyIdentifier, or an array of
|
|
424
|
+
* pre-encoded Extension DER). `issuer` is the signing AA -- `{ cert, key }` (the AA certificate DER/PEM
|
|
425
|
+
* and its private key) or `{ name, publicKey, key }` (an explicit issuer DN, AA SPKI DER, and key); an
|
|
426
|
+
* attribute certificate is never self-signed. The signature algorithm is resolved from the AA key (RSA
|
|
427
|
+
* PKCS#1 v1.5 or PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm), and the signature is verified
|
|
428
|
+
* under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE
|
|
429
|
+
* CERTIFICATE` with `opts.pem`. Malformed input throws a typed `AttrCertError`. The AA certificate's own
|
|
430
|
+
* profile (RFC 5755 sec. 4.5) and validity are a verification-layer concern -- validate the AA
|
|
431
|
+
* certificate with `pki.path.validate` before trusting the attribute certificate. Parsing is
|
|
432
|
+
* `pki.schema.attrcert.parse`.
|
|
433
|
+
*
|
|
434
|
+
* @opts
|
|
435
|
+
* - `pem` (boolean) -- return a PEM `ATTRIBUTE CERTIFICATE` string instead of DER.
|
|
436
|
+
* - `pss` (boolean) -- sign an RSA key with RSASSA-PSS rather than PKCS#1 v1.5.
|
|
437
|
+
* - `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.
|
|
438
|
+
* @example
|
|
439
|
+
* var ac = await pki.attrcert.sign(
|
|
440
|
+
* { holder: { entityName: { directoryName: "CN=Alice" } },
|
|
441
|
+
* notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
|
|
442
|
+
* attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } } },
|
|
443
|
+
* { cert: signerCertDer, key: signerKeyPkcs8 });
|
|
444
|
+
* pki.schema.attrcert.parse(ac).attributes[0].type; // the role attribute OID
|
|
445
|
+
*/
|
|
446
|
+
function sign(spec, issuer, opts) {
|
|
447
|
+
return Promise.resolve().then(function () { return _sign(spec, issuer, opts); });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function _sign(spec, issuer, opts) {
|
|
451
|
+
opts = opts || {};
|
|
452
|
+
if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("attrcert/bad-input", "the attribute-certificate spec must be an object");
|
|
453
|
+
Object.keys(spec).forEach(function (k) { if (!KNOWN_SPEC_KEYS[k]) throw _err("attrcert/bad-input", "unknown spec field " + JSON.stringify(k)); });
|
|
454
|
+
issuer = issuer || {};
|
|
455
|
+
if (issuer.key == null) throw _err("attrcert/bad-input", "a signing key (issuer.key, the AA's PKCS#8 private key) is required");
|
|
456
|
+
|
|
457
|
+
// Resolve the AA issuer DN + signing-key SPKI. An attribute certificate is NEVER self-signed. No CA
|
|
458
|
+
// gate on issuer.cert -- an AA is NOT a certificate authority (its PKC MUST NOT assert keyCertSign,
|
|
459
|
+
// RFC 5755 sec. 4.5), so requiring cA/keyCertSign would reject every conformant AA certificate.
|
|
460
|
+
var issuerDnSpec, aaSpki;
|
|
461
|
+
if (issuer.cert != null) {
|
|
462
|
+
var aaCert = _parseCert(issuer.cert, "issuer.cert");
|
|
463
|
+
issuerDnSpec = _tbsNameBytes(aaCert, "subject");
|
|
464
|
+
aaSpki = aaCert.subjectPublicKeyInfo.bytes;
|
|
465
|
+
} else if (issuer.name != null && issuer.publicKey != null) {
|
|
466
|
+
issuerDnSpec = issuer.name;
|
|
467
|
+
aaSpki = _b.reqDer(issuer.publicKey, "issuer.publicKey (the AA SPKI DER)");
|
|
468
|
+
_b.assertValidSpki(aaSpki, "issuer.publicKey");
|
|
469
|
+
} else {
|
|
470
|
+
throw _err("attrcert/bad-input", "issuer must be { cert, key } or { name, publicKey, key } (an attribute certificate is never self-signed)");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
var issuerDer = _encodeV2FormIssuer(issuerDnSpec);
|
|
474
|
+
var holderDer = _encodeHolder(spec.holder);
|
|
475
|
+
var validityDer = _encodeValidity(spec.notBeforeTime, spec.notAfterTime);
|
|
476
|
+
var attributesDer = _buildAttributes(spec.attributes);
|
|
477
|
+
var extsDer = _buildExtensions(spec.extensions, aaSpki);
|
|
478
|
+
|
|
479
|
+
// Resolve the signature scheme from the AA (signing) key's SPKI -- the whole registry, for free.
|
|
480
|
+
var scheme = signScheme.resolveSignScheme(_b.certLikeFromSpki(aaSpki), { combinedRsaSig: true, pss: opts.pss, digestAlgorithm: opts.digestAlgorithm }, true, _signE);
|
|
481
|
+
|
|
482
|
+
// AttributeCertificateInfo ::= SEQUENCE { version(bare INTEGER 1), holder, issuer, signature
|
|
483
|
+
// AlgorithmIdentifier, serialNumber, attrCertValidityPeriod, attributes, [issuerUniqueID], [extensions] }.
|
|
484
|
+
// signature == the outer signatureAlgorithm (RFC 5755 sec. 4.2.4), single source scheme.sigAlgId.
|
|
485
|
+
var acinfoChildren = [
|
|
486
|
+
b.integer(1n), holderDer, issuerDer, scheme.sigAlgId,
|
|
487
|
+
_b.serialInteger(spec.serialNumber), validityDer, attributesDer,
|
|
488
|
+
];
|
|
489
|
+
if (extsDer) acinfoChildren.push(extsDer); // issuerUniqueID omitted (sec. 4.2.8); extensions is a bare universal SEQUENCE
|
|
490
|
+
var acinfoDer = b.sequence(acinfoChildren);
|
|
491
|
+
|
|
492
|
+
return signScheme.signOverTbs(scheme, issuer.key, acinfoDer, _signE).then(function (sig) {
|
|
493
|
+
// The signature MUST verify under the AA public key (the AA proof); composite verifies both arms.
|
|
494
|
+
return Promise.resolve(_b.assertSignatureVerifies(acinfoDer, sig, aaSpki, scheme)).then(function () {
|
|
495
|
+
var acDer = b.sequence([acinfoDer, scheme.sigAlgId, b.bitString(sig, 0)]);
|
|
496
|
+
return opts.pem ? attrcert.pemEncode(acDer, "ATTRIBUTE CERTIFICATE") : acDer;
|
|
497
|
+
});
|
|
498
|
+
}, function (e) {
|
|
499
|
+
if (e instanceof AttrCertError) throw e;
|
|
500
|
+
throw _err("attrcert/bad-input", "signing the attribute certificate failed -- the signing key does not match the resolved algorithm or is invalid", e);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
module.exports = { sign: sign };
|