@blamejs/pki 0.1.13 → 0.1.15
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 +30 -0
- package/README.md +9 -5
- package/index.js +13 -9
- package/lib/asn1-der.js +3 -2
- package/lib/framework-error.js +7 -0
- package/lib/oid.js +25 -5
- package/lib/schema-all.js +24 -1
- package/lib/schema-attrcert.js +383 -0
- package/lib/schema-cms.js +332 -15
- package/lib/schema-crl.js +5 -0
- package/lib/schema-engine.js +6 -5
- package/lib/schema-pkix.js +29 -2
- package/lib/webcrypto.js +2 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ 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.1.15 — 2026-07-06
|
|
8
|
+
|
|
9
|
+
CMS EnvelopedData and EncryptedData join the parser, and every documentation example is now executed as a test.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.cms.parse now decodes CMS EnvelopedData (RFC 5652 §6) and EncryptedData (§8). An EnvelopedData returns { version, originatorInfo, recipientInfos, encryptedContentInfo, unprotectedAttrs } with all five RecipientInfo kinds decoded — KeyTransRecipientInfo (§6.2.1, with the issuerAndSerialNumber/subjectKeyIdentifier version coupling enforced), KeyAgreeRecipientInfo (§6.2.2 + RFC 5753 §3.1), KEKRecipientInfo (§6.2.3), PasswordRecipientInfo (§6.2.4), and OtherRecipientInfo (§6.2.5). An EncryptedData returns { version, encryptedContentInfo, unprotectedAttrs }. The wrapped keys, the ciphertext, and all AlgorithmIdentifier parameters are surfaced raw — every recipient carries the keyEncryptionAlgorithm its encryptedKey must be unwrapped with, and a kekid / rKeyId OtherKeyAttribute is surfaced as raw DER — decryption and key-unwrap are a separate layer. The CMSVersion is recomputed and enforced per structure and per recipient, recipientInfos is required non-empty, and the encryptedContent [0] IMPLICIT OCTET STRING is read as the ciphertext directly.
|
|
14
|
+
- The schema engine gains an implicitTag option on pki.schema.engine.seq() and on pki.schema.pkix.algorithmIdentifier(ns, { implicitTag }) — a [tag] IMPLICIT SEQUENCE / AlgorithmIdentifier reader (used by the PasswordRecipientInfo keyDerivationAlgorithm [0]). A call with no option is byte-identical to before.
|
|
15
|
+
- Every @example in the documentation comment blocks is now executed end-to-end as a test (test/layer-0-primitives/doc-examples.test.js, in the smoke gate), not merely parse-checked: an example must run to completion or throw a typed PkiError, and every documented @primitive path must resolve to a real export — so a documented example can no longer drift from the shipped API. A new @originated comment tag records a callable's original availability version when its documented path is later corrected, enforced alongside the @since version gate.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- The W3C WebCrypto constructor classes (CryptoKey, Crypto, SubtleCrypto, WebCryptoError) are now reachable under pki.webcrypto (e.g. pki.webcrypto.CryptoKey) alongside the ready Crypto instance, matching their documented path; the previously-separate pki.WebCrypto holder is removed.
|
|
20
|
+
- Repository tooling now installs npm packages exclusively through integrity-verified lockfiles: the fuzz build installs the jazzer.js engine via npm ci against the committed fuzz/package-lock.json, and the vendoring script resolves a package to an integrity-pinned lockfile in an isolated staging workspace — no install script runs and the repo's own node_modules is never touched — before bundling. Tooling child processes that need a shell (the Windows npm shim) now receive one explicitly-quoted command string instead of an unescaped argument array.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Two documented API paths that did not resolve at runtime are corrected: pki.webcrypto.CryptoKey (previously reachable only via pki.WebCrypto.CryptoKey) and pki.asn1.read.oid (its comment block labeled the path pki.asn1.readOid, which never existed). Both are now reachable at the documented path.
|
|
25
|
+
- A documentation example for pki.webcrypto.subtle.exportKey referenced an undefined variable; it now generates the key pair it exports.
|
|
26
|
+
|
|
27
|
+
## v0.1.14 — 2026-07-05
|
|
28
|
+
|
|
29
|
+
An RFC 5755 attribute-certificate parser joins the pki.schema family.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- pki.schema.attrcert — an RFC 5755 attribute-certificate parser. pki.schema.attrcert.parse turns a DER Buffer or PEM into a structured v2 attribute certificate ({ version, holder, issuer, signatureAlgorithm, serialNumber, serialNumberHex, validity, attributes, issuerUniqueID, extensions, tbsBytes, signatureValue }). The holder (baseCertificateID / entityName / objectDigestInfo) and issuer (v1Form / v2Form) identities come back as validated GeneralNames; the validity window is real Dates; the privilege attributes (id-at-role, id-aca-group, id-at-clearance, and any others) resolve by name where the registry knows them. The outer-equals-inner signatureAlgorithm agreement (RFC 5755 4.2.4), the positive-and-at-most-20-octet serialNumber (4.2.5), the GeneralizedTime-only validity (4.2.6), the non-empty unique-typed attribute list (4.2.7), and the digestedObjectType enumeration are all enforced fail-closed. pki.schema.parse detect-and-routes an attribute certificate; the obsolete v1 form is recognized and deferred with a precise attrcert/legacy-v1-not-supported.
|
|
34
|
+
- pki.schema.pkix gains a shared GeneralNames validator that the attribute-certificate parser composes for its four GeneralNames-bearing fields — validating every element as a well-formed GeneralName (rejecting a bad tag, a wrong primitive/constructed form, a non-IA5 string, or a mis-sized iPAddress) rather than surfacing the sequence as opaque bytes. It handles both a bare universal SEQUENCE OF GeneralName and a context-tagged IMPLICIT GeneralNames.
|
|
35
|
+
- The OID registry gains the RFC 5755 attribute-certificate object identifiers: the id-aca attribute-type family (authenticationInfo / accessIdentity / chargingIdentity / group), id-at-role and id-at-clearance, the id-ce-targetInformation and id-ce-noRevAvail extensions, and the id-pe-ac-auditIdentity / id-pe-aaControls / id-pe-ac-proxying private extensions — so a parsed attribute certificate's attributes and extensions resolve by name.
|
|
36
|
+
|
|
7
37
|
## v0.1.13 — 2026-07-05
|
|
8
38
|
|
|
9
39
|
An RFC 3161 timestamp parser joins the pki.schema family.
|
package/README.md
CHANGED
|
@@ -195,9 +195,10 @@ is callable today; nothing below is a stub.
|
|
|
195
195
|
| `pki.schema.crl` | Parse DER / PEM X.509 CRLs per RFC 5280 §5 — revoked serials with real-`Date` revocation times, named + partly-decoded extensions, fail-closed — `parse`, `pemDecode` |
|
|
196
196
|
| `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
197
197
|
| `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
|
|
198
|
-
| `pki.schema.cms` | Parse DER / PEM CMS
|
|
198
|
+
| `pki.schema.cms` | Parse DER / PEM CMS per RFC 5652 — SignedData (§5, signer infos + raw signed-attribute bytes for external verification), EnvelopedData (§6, all five RecipientInfo kinds incl. RFC 5753 key-agreement, with the wrapped keys / ciphertext surfaced raw), and EncryptedData (§8); certificates / CRLs kept raw, fail-closed; remaining content types recognized (not decoded) — `parse`, `pemDecode`, `pemEncode` |
|
|
199
199
|
| `pki.schema.ocsp` | Parse DER / PEM OCSP requests and responses per RFC 6960 — per-certificate status (good / revoked / unknown), responder identity, raw tbs bytes for external verification, certificates kept raw, fail-closed; non-basic response types recognized (not decoded) — `parseRequest`, `parseResponse`, `pemDecode` |
|
|
200
200
|
| `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp responses and tokens — the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseTstInfo`, `parseToken`, `pemDecode` |
|
|
201
|
+
| `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes (role / group / clearance) and extensions, with the raw signed region for a verifier; the obsolete v1 form is recognized and deferred, fail-closed — `parse`, `pemDecode` |
|
|
201
202
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` plus the schema combinators |
|
|
202
203
|
| `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
|
|
203
204
|
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError`, each carrying a stable `code` in `domain/reason` form |
|
|
@@ -238,9 +239,12 @@ plus a documentation comment block, not new parse logic.
|
|
|
238
239
|
│ pki.schema.parse — inspect the DER root, route to the matching sibling │
|
|
239
240
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
240
241
|
│
|
|
241
|
-
|
|
242
|
-
│
|
|
243
|
-
|
|
242
|
+
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ Format
|
|
243
|
+
│ x509 │ │ crl │ │ csr │ │ pkcs8 │ parsers
|
|
244
|
+
└────────┘ └────────┘ └────────┘ └────────┘ (siblings)
|
|
245
|
+
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
|
|
246
|
+
│ cms │ │ ocsp │ │ tsp │ │attrcert│
|
|
247
|
+
└────────┘ └────────┘ └────────┘ └────────┘
|
|
244
248
|
│ every sibling composes ↓
|
|
245
249
|
┌─ Shared structure ──────────────────────────────────────────────────────┐
|
|
246
250
|
│ pki.schema.engine — walk + combinators (positional reads, tag order, │
|
|
@@ -272,7 +276,7 @@ bounded version reader, and the single coerce → decode → walk parse-entry th
|
|
|
272
276
|
every format's `parse` is bound to. Because input coercion, the PEM size cap, and
|
|
273
277
|
the DER-decode wrapping live here once, a format cannot diverge on a guard.
|
|
274
278
|
|
|
275
|
-
**Format parsers.** `x509`, `crl`, `csr`, `pkcs8`, `cms`, `ocsp`, and `
|
|
279
|
+
**Format parsers.** `x509`, `crl`, `csr`, `pkcs8`, `cms`, `ocsp`, `tsp`, and `attrcert` are siblings:
|
|
276
280
|
each is a schema declaration composed from the shared pieces, emitting its own
|
|
277
281
|
typed `domain/reason` error codes. `pki.schema.parse` inspects a decoded root and
|
|
278
282
|
detect-and-routes to the matching sibling; the detectors are mutually exclusive by
|
package/index.js
CHANGED
|
@@ -42,13 +42,17 @@ module.exports = {
|
|
|
42
42
|
// `schema` is the family: the L2 structure-schema engine (schema.engine) and
|
|
43
43
|
// the per-format parsers (schema.x509, …) with detect-and-route schema.parse.
|
|
44
44
|
schema: schema,
|
|
45
|
-
// A ready W3C Crypto instance (globalThis.crypto shape)
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Crypto: webcrypto.Crypto,
|
|
50
|
-
SubtleCrypto: webcrypto.SubtleCrypto,
|
|
51
|
-
CryptoKey: webcrypto.CryptoKey,
|
|
52
|
-
WebCryptoError: webcrypto.WebCryptoError,
|
|
53
|
-
},
|
|
45
|
+
// A ready W3C Crypto instance (globalThis.crypto shape) with the classes for
|
|
46
|
+
// constructing more attached under the same namespace (pki.webcrypto.CryptoKey,
|
|
47
|
+
// .SubtleCrypto, .Crypto, .WebCryptoError). PQC-first, classical-capable, zero-dep.
|
|
48
|
+
webcrypto: _webcryptoNamespace(),
|
|
54
49
|
};
|
|
50
|
+
|
|
51
|
+
function _webcryptoNamespace() {
|
|
52
|
+
var wc = webcrypto.webcrypto; // the ready Crypto instance
|
|
53
|
+
wc.Crypto = webcrypto.Crypto;
|
|
54
|
+
wc.SubtleCrypto = webcrypto.SubtleCrypto;
|
|
55
|
+
wc.CryptoKey = webcrypto.CryptoKey;
|
|
56
|
+
wc.WebCryptoError = webcrypto.WebCryptoError;
|
|
57
|
+
return wc;
|
|
58
|
+
}
|
package/lib/asn1-der.js
CHANGED
|
@@ -408,9 +408,10 @@ function readNullImplicit(node, tag) {
|
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
/**
|
|
411
|
-
* @primitive pki.asn1.
|
|
411
|
+
* @primitive pki.asn1.read.oid
|
|
412
412
|
* @signature pki.asn1.read.oid(node) -> "1.2.840.113549.1.1.11"
|
|
413
|
-
* @since 0.1.
|
|
413
|
+
* @since 0.1.15
|
|
414
|
+
* @originated 0.1.0
|
|
414
415
|
* @status stable
|
|
415
416
|
* @spec X.690 §8.19
|
|
416
417
|
* @related pki.oid.name
|
package/lib/framework-error.js
CHANGED
|
@@ -149,6 +149,12 @@ var OcspError = defineClass("OcspError", { withCause: true });
|
|
|
149
149
|
// TSTInfo, or TimeStampResp. Carries the underlying leaf fault as `.cause`.
|
|
150
150
|
var TspError = defineClass("TspError", { withCause: true });
|
|
151
151
|
|
|
152
|
+
// AttrCertError — a byte sequence that is not a well-formed X.509 Attribute
|
|
153
|
+
// Certificate (AttributeCertificate / AttributeCertificateInfo — RFC 5755 §4),
|
|
154
|
+
// or a recognized-and-deferred legacy AttributeCertificateV1. Carries the
|
|
155
|
+
// underlying leaf fault as `.cause`.
|
|
156
|
+
var AttrCertError = defineClass("AttrCertError", { withCause: true });
|
|
157
|
+
|
|
152
158
|
module.exports = {
|
|
153
159
|
PkiError: PkiError,
|
|
154
160
|
defineClass: defineClass,
|
|
@@ -164,4 +170,5 @@ module.exports = {
|
|
|
164
170
|
CmsError: CmsError,
|
|
165
171
|
OcspError: OcspError,
|
|
166
172
|
TspError: TspError,
|
|
173
|
+
AttrCertError: AttrCertError,
|
|
167
174
|
};
|
package/lib/oid.js
CHANGED
|
@@ -49,7 +49,9 @@ var FAMILIES = {
|
|
|
49
49
|
attributeType: { base: [2, 5, 4], of: {
|
|
50
50
|
commonName: 3, surname: 4, serialNumber: 5, countryName: 6,
|
|
51
51
|
localityName: 7, stateOrProvinceName: 8, streetAddress: 9,
|
|
52
|
-
organizationName: 10, organizationalUnitName: 11, title: 12, givenName: 42
|
|
52
|
+
organizationName: 10, organizationalUnitName: 11, title: 12, givenName: 42,
|
|
53
|
+
// RFC 5755 §4.4 attribute-certificate attribute types.
|
|
54
|
+
clearance: 55, role: 72 } },
|
|
53
55
|
|
|
54
56
|
// RFC 5280 certificate extensions.
|
|
55
57
|
certExtension: { base: [2, 5, 29], of: {
|
|
@@ -60,10 +62,28 @@ var FAMILIES = {
|
|
|
60
62
|
issuingDistributionPoint: 28, certificateIssuer: 29,
|
|
61
63
|
nameConstraints: 30, cRLDistributionPoints: 31,
|
|
62
64
|
certificatePolicies: 32, authorityKeyIdentifier: 35, extKeyUsage: 37,
|
|
63
|
-
freshestCRL: 46
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
freshestCRL: 46,
|
|
66
|
+
// RFC 5755 attribute-certificate extensions (§4.3.2, §4.3.6).
|
|
67
|
+
targetInformation: 55, noRevAvail: 56 } },
|
|
68
|
+
|
|
69
|
+
// PKIX private extensions on the id-pe arc (authorityInfoAccess et al, plus the
|
|
70
|
+
// RFC 5755 attribute-certificate id-pe extensions: ac-auditIdentity §4.3.1,
|
|
71
|
+
// aaControls §7.4, ac-proxying §7.2).
|
|
72
|
+
pkixAccess: { base: [1, 3, 6, 1, 5, 5, 7, 1], of: {
|
|
73
|
+
authorityInfoAccess: 1, acAuditIdentity: 4, aaControls: 6, acProxying: 10 } },
|
|
74
|
+
|
|
75
|
+
// id-aca — RFC 5755 attribute-certificate attribute types on the id-pkix 10 arc:
|
|
76
|
+
// authenticationInfo §4.4.1 .. group §4.4.4, plus encAttrs §7.1 (the encrypted-
|
|
77
|
+
// attribute wrapper, syntax ContentInfo). { id-aca 5 } is reserved.
|
|
78
|
+
idAca: { base: [1, 3, 6, 1, 5, 5, 7, 10], of: {
|
|
79
|
+
authenticationInfo: 1, accessIdentity: 2, chargingIdentity: 3, group: 4, encAttrs: 6 } },
|
|
80
|
+
|
|
81
|
+
// The legacy RFC 3281 id-at-clearance on the X.501 selected-attribute-types arc.
|
|
82
|
+
// RFC 5755 §4.4.6 says implementations MUST NOT OUTPUT this form but SHOULD ACCEPT
|
|
83
|
+
// it for decoding, so it is registered as an alias of "clearance". The canonical
|
|
84
|
+
// name -> OID reverse stays the RFC 5755 attributeType-arc 2.5.4.55 (registered
|
|
85
|
+
// first under attributeType, so it wins the reverse mapping).
|
|
86
|
+
selectedAttrType: { base: [2, 5, 1, 5], of: { clearance: 55 } },
|
|
67
87
|
|
|
68
88
|
// PKIX Access Descriptor methods (id-ad, RFC 5280 §4.2.2.1/§4.2.2.2). id-ad-ocsp
|
|
69
89
|
// is the arc the OCSP responder OIDs hang under; id-ad-caIssuers names the AIA
|
package/lib/schema-all.js
CHANGED
|
@@ -39,6 +39,7 @@ var pkcs8 = require("./schema-pkcs8");
|
|
|
39
39
|
var cms = require("./schema-cms");
|
|
40
40
|
var ocsp = require("./schema-ocsp");
|
|
41
41
|
var tsp = require("./schema-tsp");
|
|
42
|
+
var attrcert = require("./schema-attrcert");
|
|
42
43
|
var frameworkError = require("./framework-error");
|
|
43
44
|
|
|
44
45
|
var SchemaError = frameworkError.SchemaError;
|
|
@@ -126,6 +127,27 @@ var FORMATS = [
|
|
|
126
127
|
detect: csr.matches,
|
|
127
128
|
parse: function (input) { return csr.parse(input); },
|
|
128
129
|
},
|
|
130
|
+
{
|
|
131
|
+
// AttributeCertificate ::= SEQUENCE { acinfo, signatureAlgorithm, signatureValue }
|
|
132
|
+
// — the signed-envelope trio, distinguished from a certificate / CRL by an acinfo
|
|
133
|
+
// that LEADS WITH a bare INTEGER version and carries a 2-GeneralizedTime validity
|
|
134
|
+
// at children[5]. Registered ahead of crl / x509 (most-specific-first): a legacy v1
|
|
135
|
+
// AC would otherwise satisfy x509.matches's Validity probe (RFC 5755 §4.1).
|
|
136
|
+
name: "attrcert",
|
|
137
|
+
module: attrcert,
|
|
138
|
+
detect: attrcert.matches,
|
|
139
|
+
parse: function (input) { return attrcert.parse(input); },
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
// AttributeCertificateV1 (X.509-1997, RFC 5652 §10.2) — the obsolete predecessor
|
|
143
|
+
// whose acInfo LEADS WITH the subject CHOICE (context [0]/[1]). Recognized and
|
|
144
|
+
// deferred with a precise attrcert/legacy-v1-not-supported rather than routed to a
|
|
145
|
+
// wrong format. children[1] == SEQUENCE keeps it disjoint from a v3 certificate.
|
|
146
|
+
name: "attrcert-v1",
|
|
147
|
+
module: attrcert,
|
|
148
|
+
detect: attrcert.matchesV1,
|
|
149
|
+
parse: function (input) { return attrcert.parseV1(input); },
|
|
150
|
+
},
|
|
129
151
|
{
|
|
130
152
|
// CertificateList ::= SEQUENCE { tbsCertList, signatureAlgorithm,
|
|
131
153
|
// signatureValue } — the same outer shape as a certificate, distinguished
|
|
@@ -157,7 +179,7 @@ var FORMATS = [
|
|
|
157
179
|
* The names of every registered format, in detection order.
|
|
158
180
|
*
|
|
159
181
|
* @example
|
|
160
|
-
* pki.schema.all(); // → ["cms", "tsp", "ocsp-request", "ocsp-response", "pkcs8", "csr", "crl", "x509"]
|
|
182
|
+
* pki.schema.all(); // → ["cms", "tsp", "ocsp-request", "ocsp-response", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
|
|
161
183
|
*/
|
|
162
184
|
function all() { return FORMATS.map(function (f) { return f.name; }); }
|
|
163
185
|
|
|
@@ -202,6 +224,7 @@ module.exports = {
|
|
|
202
224
|
cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
|
|
203
225
|
ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode },
|
|
204
226
|
tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode },
|
|
227
|
+
attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode },
|
|
205
228
|
all: all,
|
|
206
229
|
parse: parse,
|
|
207
230
|
};
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.schema.attrcert
|
|
6
|
+
* @nav Schema
|
|
7
|
+
* @title Attribute Cert
|
|
8
|
+
* @order 175
|
|
9
|
+
* @slug attrcert
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
12
|
+
* X.509 Attribute Certificate handling per RFC 5755. An attribute certificate
|
|
13
|
+
* binds a holder to a set of privilege attributes (role, group, clearance)
|
|
14
|
+
* without carrying a public key — the authorization counterpart to an identity
|
|
15
|
+
* certificate. `parse` decodes a v2 `AttributeCertificate` into its holder,
|
|
16
|
+
* issuer, validity window (real `Date`s), attributes, and extensions, reusing
|
|
17
|
+
* the shared signed-envelope so the raw `tbsBytes` (the exact signed region) and
|
|
18
|
+
* the `signatureValue` are surfaced for a downstream verifier.
|
|
19
|
+
*
|
|
20
|
+
* The holder and issuer identities are `GeneralName`s, validated on the way in
|
|
21
|
+
* (each alternative's form and content per RFC 5280 §4.2.1.6) and surfaced with
|
|
22
|
+
* their raw bytes; the profile MUSTs above structural scope (a single
|
|
23
|
+
* non-empty `directoryName` issuer, the holder-to-PKC binding, targeting and
|
|
24
|
+
* revocation) are verification-layer concerns. The obsolete X.509-1997
|
|
25
|
+
* `AttributeCertificateV1` is recognized and deferred, not parsed. DER-only,
|
|
26
|
+
* fail-closed.
|
|
27
|
+
*
|
|
28
|
+
* @card
|
|
29
|
+
* Parse DER / PEM RFC 5755 attribute certificates into holder, issuer,
|
|
30
|
+
* validity, attributes, and extensions — validated GeneralNames, raw verifier
|
|
31
|
+
* inputs, legacy v1 recognize-and-defer, fail-closed.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
var asn1 = require("./asn1-der");
|
|
35
|
+
var schema = require("./schema-engine");
|
|
36
|
+
var pkix = require("./schema-pkix");
|
|
37
|
+
var oid = require("./oid");
|
|
38
|
+
var frameworkError = require("./framework-error");
|
|
39
|
+
|
|
40
|
+
var AttrCertError = frameworkError.AttrCertError;
|
|
41
|
+
var PemError = frameworkError.PemError;
|
|
42
|
+
|
|
43
|
+
var NS = pkix.makeNS("attrcert", AttrCertError, oid);
|
|
44
|
+
|
|
45
|
+
var TAGS = asn1.TAGS;
|
|
46
|
+
|
|
47
|
+
var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
|
|
48
|
+
var EXTENSIONS = pkix.extensions(NS);
|
|
49
|
+
|
|
50
|
+
// AttCertVersion ::= INTEGER { v2(1) } — a bare, mandatory INTEGER (not the x509
|
|
51
|
+
// [0] EXPLICIT DEFAULT shape). The only legal value is v2, surfaced as 2.
|
|
52
|
+
var VERSION = pkix.versionReader(NS, { "1": 2 });
|
|
53
|
+
|
|
54
|
+
// ---- shared leaves ---------------------------------------------------
|
|
55
|
+
|
|
56
|
+
// AttCertValidityPeriod times are GeneralizedTime (RFC 5755 §4.2.6) — a UTCTime is
|
|
57
|
+
// rejected. Non-fractional YYYYMMDDHHMMSSZ is enforced by the codec.
|
|
58
|
+
var GENERALIZED_TIME = schema.decode(function (n, ctx) {
|
|
59
|
+
if (n.tagClass !== "universal" || n.tagNumber !== TAGS.GENERALIZED_TIME) {
|
|
60
|
+
throw ctx.E("attrcert/bad-time", "AttCertValidityPeriod times must be GeneralizedTime (RFC 5755 §4.2.6)");
|
|
61
|
+
}
|
|
62
|
+
return asn1.read.time(n);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// digestedObjectType ::= ENUMERATED { publicKey(0), publicKeyCert(1),
|
|
66
|
+
// otherObjectTypes(2) } (RFC 5755 §4.1) — an undefined value is rejected; a
|
|
67
|
+
// non-ENUMERATED tag fails at the codec (asn1/*).
|
|
68
|
+
var ODT_NAMES = { "0": "publicKey", "1": "publicKeyCert", "2": "otherObjectTypes" };
|
|
69
|
+
var DIGESTED_OBJECT_TYPE = schema.decode(function (n, ctx) {
|
|
70
|
+
var v = asn1.read.enumerated(n);
|
|
71
|
+
var k = v.toString();
|
|
72
|
+
if (!Object.prototype.hasOwnProperty.call(ODT_NAMES, k)) {
|
|
73
|
+
throw ctx.E("attrcert/bad-digested-object-type", "digestedObjectType " + k + " is not a defined value (RFC 5755 §4.1)");
|
|
74
|
+
}
|
|
75
|
+
// RFC 5755 §7.3 — a conformant AC MUST NOT use otherObjectTypes(2); the digested
|
|
76
|
+
// object is limited to a public key or a public-key certificate, so reject it
|
|
77
|
+
// fail-closed rather than parse a digest whose object type is unidentifiable.
|
|
78
|
+
if (k === "2") {
|
|
79
|
+
throw ctx.E("attrcert/bad-digested-object-type", "otherObjectTypes(2) MUST NOT be used (RFC 5755 §7.3)");
|
|
80
|
+
}
|
|
81
|
+
return { code: Number(v), name: ODT_NAMES[k] };
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ---- IssuerSerial / ObjectDigestInfo ---------------------------------
|
|
85
|
+
|
|
86
|
+
// IssuerSerial ::= SEQUENCE { issuer GeneralNames, serial CertificateSerialNumber,
|
|
87
|
+
// issuerUID UniqueIdentifier OPTIONAL } (RFC 5755 §4.1). Reached as an IMPLICIT
|
|
88
|
+
// [n] node (Holder [0] / V2Form [0]) whose tag replaces the SEQUENCE tag, so the
|
|
89
|
+
// shape assertion is "constructed" (the tag is pinned by the enclosing trailing);
|
|
90
|
+
// a context [n] PRIMITIVE node has no children and fails closed here.
|
|
91
|
+
var ISSUER_SERIAL = schema.seq([
|
|
92
|
+
schema.field("issuer", pkix.generalNames(NS, { code: "attrcert/bad-issuer-serial" })),
|
|
93
|
+
schema.field("serial", schema.integerLeaf()),
|
|
94
|
+
schema.optional("issuerUID", schema.bitString(), { whenUniversal: [TAGS.BIT_STRING] }),
|
|
95
|
+
], {
|
|
96
|
+
assert: "constructed", code: "attrcert/bad-issuer-serial", what: "IssuerSerial",
|
|
97
|
+
build: function (m) {
|
|
98
|
+
return {
|
|
99
|
+
issuer: m.fields.issuer.value.result,
|
|
100
|
+
serial: m.fields.serial.value,
|
|
101
|
+
serialHex: m.fields.serial.node.content.toString("hex"),
|
|
102
|
+
issuerUID: m.fields.issuerUID.present ? m.fields.issuerUID.value : null,
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ObjectDigestInfo ::= SEQUENCE { digestedObjectType ENUMERATED, otherObjectTypeID
|
|
108
|
+
// OBJECT IDENTIFIER OPTIONAL, digestAlgorithm AlgorithmIdentifier, objectDigest BIT
|
|
109
|
+
// STRING } (RFC 5755 §4.1). otherObjectTypeID is present only for otherObjectTypes(2).
|
|
110
|
+
var OBJECT_DIGEST_INFO = schema.seq([
|
|
111
|
+
schema.field("digestedObjectType", DIGESTED_OBJECT_TYPE),
|
|
112
|
+
schema.optional("otherObjectTypeID", schema.oidLeaf(), { whenUniversal: [TAGS.OBJECT_IDENTIFIER] }),
|
|
113
|
+
schema.field("digestAlgorithm", ALGORITHM_IDENTIFIER),
|
|
114
|
+
schema.field("objectDigest", schema.bitString()),
|
|
115
|
+
], {
|
|
116
|
+
assert: "constructed", code: "attrcert/bad-object-digest-info", what: "ObjectDigestInfo",
|
|
117
|
+
build: function (m, ctx) {
|
|
118
|
+
var t = m.fields.digestedObjectType.value;
|
|
119
|
+
// otherObjectTypeID identifies a non-certificate object type, meaningful only with
|
|
120
|
+
// otherObjectTypes(2) — which RFC 5755 §7.3 forbids (rejected at the leaf above) —
|
|
121
|
+
// so a present otherObjectTypeID is never valid here and is rejected fail-closed.
|
|
122
|
+
if (m.fields.otherObjectTypeID.present) {
|
|
123
|
+
throw ctx.E("attrcert/bad-object-digest-info", "otherObjectTypeID is only valid with otherObjectTypes(2), which RFC 5755 §7.3 forbids");
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
digestedObjectType: t,
|
|
127
|
+
otherObjectTypeID: null,
|
|
128
|
+
digestAlgorithm: m.fields.digestAlgorithm.value.result,
|
|
129
|
+
objectDigest: m.fields.objectDigest.value,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ---- Holder / AttCertIssuer / V2Form ---------------------------------
|
|
135
|
+
|
|
136
|
+
// Holder ::= SEQUENCE { baseCertificateID [0] IMPLICIT IssuerSerial OPTIONAL,
|
|
137
|
+
// entityName [1] IMPLICIT GeneralNames OPTIONAL, objectDigestInfo [2] IMPLICIT
|
|
138
|
+
// ObjectDigestInfo OPTIONAL } (RFC 5755 §4.1). All three are IMPLICIT + OPTIONAL;
|
|
139
|
+
// the profile RECOMMENDS exactly one, but the parser surfaces all three (null when
|
|
140
|
+
// absent) and does not enforce the cardinality.
|
|
141
|
+
var HOLDER = schema.seq([
|
|
142
|
+
schema.trailing([
|
|
143
|
+
{ tag: 0, name: "baseCertificateID", schema: ISSUER_SERIAL },
|
|
144
|
+
{ tag: 1, name: "entityName", schema: pkix.generalNames(NS, { implicitTag: 1, code: "attrcert/bad-entity-name" }) },
|
|
145
|
+
{ tag: 2, name: "objectDigestInfo", schema: OBJECT_DIGEST_INFO },
|
|
146
|
+
], { minTag: 0, maxTag: 2, unexpectedCode: "attrcert/bad-holder", orderCode: "attrcert/bad-holder" }),
|
|
147
|
+
], {
|
|
148
|
+
assert: "sequence", code: "attrcert/bad-holder", what: "Holder",
|
|
149
|
+
build: function (m) {
|
|
150
|
+
return {
|
|
151
|
+
baseCertificateID: m.fields.baseCertificateID.present ? m.fields.baseCertificateID.value.result : null,
|
|
152
|
+
entityName: m.fields.entityName.present ? m.fields.entityName.value.result : null,
|
|
153
|
+
objectDigestInfo: m.fields.objectDigestInfo.present ? m.fields.objectDigestInfo.value.result : null,
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// V2Form ::= SEQUENCE { issuerName GeneralNames OPTIONAL, baseCertificateID [0]
|
|
159
|
+
// IMPLICIT IssuerSerial OPTIONAL, objectDigestInfo [1] IMPLICIT ObjectDigestInfo
|
|
160
|
+
// OPTIONAL } (RFC 5755 §4.1). Reached via the AttCertIssuer CHOICE on a context [0]
|
|
161
|
+
// node, so the shape is "constructed".
|
|
162
|
+
var V2_FORM = schema.seq([
|
|
163
|
+
schema.optional("issuerName", pkix.generalNames(NS, { code: "attrcert/bad-issuer-name" }), { whenUniversal: [TAGS.SEQUENCE] }),
|
|
164
|
+
schema.trailing([
|
|
165
|
+
{ tag: 0, name: "baseCertificateID", schema: ISSUER_SERIAL },
|
|
166
|
+
{ tag: 1, name: "objectDigestInfo", schema: OBJECT_DIGEST_INFO },
|
|
167
|
+
], { minTag: 0, maxTag: 1, unexpectedCode: "attrcert/bad-v2form", orderCode: "attrcert/bad-v2form" }),
|
|
168
|
+
], {
|
|
169
|
+
assert: "constructed", code: "attrcert/bad-v2form", what: "V2Form",
|
|
170
|
+
build: function (m) {
|
|
171
|
+
return {
|
|
172
|
+
issuerName: m.fields.issuerName.present ? m.fields.issuerName.value.result : null,
|
|
173
|
+
baseCertificateID: m.fields.baseCertificateID.present ? m.fields.baseCertificateID.value.result : null,
|
|
174
|
+
objectDigestInfo: m.fields.objectDigestInfo.present ? m.fields.objectDigestInfo.value.result : null,
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// AttCertIssuer ::= CHOICE { v1Form GeneralNames, v2Form [0] IMPLICIT V2Form } (RFC
|
|
180
|
+
// 5755 §4.1). A universal SEQUENCE is the (obsolete but structurally legal) v1Form;
|
|
181
|
+
// a context [0] is v2Form. A conforming AC uses v2Form; the parser surfaces which.
|
|
182
|
+
var ATT_CERT_ISSUER = schema.choice([
|
|
183
|
+
{ when: { tagClass: "universal", tagNumber: TAGS.SEQUENCE }, schema: pkix.generalNames(NS, { code: "attrcert/bad-issuer" }) },
|
|
184
|
+
{ when: { tagClass: "context", tagNumber: 0 }, schema: V2_FORM },
|
|
185
|
+
], { code: "attrcert/bad-issuer" });
|
|
186
|
+
|
|
187
|
+
// ---- validity + attributes -------------------------------------------
|
|
188
|
+
|
|
189
|
+
// AttCertValidityPeriod ::= SEQUENCE { notBeforeTime GeneralizedTime, notAfterTime
|
|
190
|
+
// GeneralizedTime } (RFC 5755 §4.2.6).
|
|
191
|
+
var VALIDITY = schema.seq([
|
|
192
|
+
schema.field("notBeforeTime", GENERALIZED_TIME),
|
|
193
|
+
schema.field("notAfterTime", GENERALIZED_TIME),
|
|
194
|
+
], {
|
|
195
|
+
assert: "sequence", arity: { exact: 2 }, code: "attrcert/bad-validity", what: "AttCertValidityPeriod",
|
|
196
|
+
build: function (m) { return { notBeforeTime: m.fields.notBeforeTime.value, notAfterTime: m.fields.notAfterTime.value }; },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// attributes ::= SEQUENCE OF Attribute (RFC 5755 §4.2.7) — MUST be non-empty and
|
|
200
|
+
// each AttributeType OID unique. Order-preserving (SEQUENCE OF, not SET OF).
|
|
201
|
+
var ATTRIBUTES = schema.seqOf(pkix.attribute(NS), {
|
|
202
|
+
assert: "sequence", min: 1, code: "attrcert/bad-attributes", what: "attributes",
|
|
203
|
+
unique: function (it) { return it.value.result.type; }, dupCode: "attrcert/duplicate-attribute",
|
|
204
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ---- AttributeCertificateInfo / AttributeCertificate -----------------
|
|
208
|
+
|
|
209
|
+
// AttributeCertificateInfo ::= SEQUENCE { version, holder, issuer, signature
|
|
210
|
+
// AlgorithmIdentifier, serialNumber, attrCertValidityPeriod, attributes,
|
|
211
|
+
// issuerUniqueID OPTIONAL, extensions OPTIONAL } (RFC 5755 §4.1). The tbs body the
|
|
212
|
+
// signed envelope wraps; the cross-field invariants live in the envelope build.
|
|
213
|
+
var ACINFO = schema.seq([
|
|
214
|
+
schema.field("version", VERSION),
|
|
215
|
+
schema.field("holder", HOLDER),
|
|
216
|
+
schema.field("issuer", ATT_CERT_ISSUER),
|
|
217
|
+
schema.field("signature", ALGORITHM_IDENTIFIER),
|
|
218
|
+
schema.field("serialNumber", schema.integerLeaf()),
|
|
219
|
+
schema.field("attrCertValidityPeriod", VALIDITY),
|
|
220
|
+
schema.field("attributes", ATTRIBUTES),
|
|
221
|
+
schema.optional("issuerUniqueID", schema.bitString(), { whenUniversal: [TAGS.BIT_STRING] }),
|
|
222
|
+
schema.optional("extensions", EXTENSIONS, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
223
|
+
], { assert: "sequence", code: "attrcert/bad-acinfo", what: "AttributeCertificateInfo" });
|
|
224
|
+
|
|
225
|
+
// AttributeCertificate ::= SEQUENCE { acinfo, signatureAlgorithm, signatureValue
|
|
226
|
+
// BIT STRING } — the shared SIGNED envelope (RFC 5755 §4.1). The AC-specific
|
|
227
|
+
// invariants (§4.2.4 sig-alg agreement, §4.2.5 positive-and-<=20-octet serialNumber)
|
|
228
|
+
// run in the build; the envelope owns the SEQUENCE-of-3 shape + signature extraction.
|
|
229
|
+
var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
|
|
230
|
+
code: "attrcert/not-an-attribute-certificate", what: "AttributeCertificate",
|
|
231
|
+
build: function (e, ctx) {
|
|
232
|
+
var acinfo = e.tbsMatch;
|
|
233
|
+
|
|
234
|
+
// RFC 5755 §4.2.4 — the outer signatureAlgorithm MUST equal acinfo.signature.
|
|
235
|
+
if (!e.outerSignatureAlgorithmBytes.equals(acinfo.fields.signature.node.bytes)) {
|
|
236
|
+
throw ctx.E("attrcert/bad-signature-algorithm", "signatureAlgorithm must match AttributeCertificateInfo.signature (RFC 5755 §4.2.4)");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// RFC 5755 §4.2.5 — serialNumber MUST be a positive INTEGER of at most 20 content
|
|
240
|
+
// octets. Positive excludes both a negative (DER sign bit set) and zero (a minimal
|
|
241
|
+
// INTEGER 0 is a single 0x00 octet — the codec already rejected any non-minimal
|
|
242
|
+
// zero). Surface it lossless (BigInt) + as hex.
|
|
243
|
+
var serialNode = acinfo.fields.serialNumber.node;
|
|
244
|
+
var sc = serialNode.content;
|
|
245
|
+
if (sc.length === 0 || (sc[0] & 0x80) !== 0 || (sc.length === 1 && sc[0] === 0)) {
|
|
246
|
+
throw ctx.E("attrcert/bad-serial-number", "serialNumber must be a positive INTEGER (RFC 5755 §4.2.5)");
|
|
247
|
+
}
|
|
248
|
+
if (sc.length > 20) {
|
|
249
|
+
throw ctx.E("attrcert/bad-serial-number", "serialNumber must not exceed 20 content octets (RFC 5755 §4.2.5)");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// AttCertIssuer is a CHOICE — the chosen arm is read off the issuer node's tag
|
|
253
|
+
// (a universal SEQUENCE is v1Form; a context [0] is v2Form).
|
|
254
|
+
var issuerNode = acinfo.fields.issuer.node;
|
|
255
|
+
var issuerVal = acinfo.fields.issuer.value;
|
|
256
|
+
var issuer = issuerNode.tagClass === "context"
|
|
257
|
+
? { form: "v2Form", v2Form: issuerVal.result, v1Form: null }
|
|
258
|
+
: { form: "v1Form", v1Form: issuerVal.result, v2Form: null };
|
|
259
|
+
|
|
260
|
+
var extField = acinfo.fields.extensions;
|
|
261
|
+
return {
|
|
262
|
+
version: acinfo.fields.version.value, // 2
|
|
263
|
+
holder: acinfo.fields.holder.value.result,
|
|
264
|
+
issuer: issuer,
|
|
265
|
+
signatureAlgorithm: e.signatureAlgorithm,
|
|
266
|
+
tbsSignatureAlgorithm: acinfo.fields.signature.value.result,
|
|
267
|
+
serialNumber: acinfo.fields.serialNumber.value,
|
|
268
|
+
serialNumberHex: sc.toString("hex"),
|
|
269
|
+
validity: acinfo.fields.attrCertValidityPeriod.value.result,
|
|
270
|
+
attributes: acinfo.fields.attributes.value.result,
|
|
271
|
+
issuerUniqueID: acinfo.fields.issuerUniqueID.present ? acinfo.fields.issuerUniqueID.value : null,
|
|
272
|
+
extensions: (extField && extField.present) ? extField.value.result : [],
|
|
273
|
+
tbsBytes: e.tbsBytes,
|
|
274
|
+
signatureValue: e.signatureValue,
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ---- parse -----------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @primitive pki.schema.attrcert.parse
|
|
283
|
+
* @signature pki.schema.attrcert.parse(input) -> attributeCertificate
|
|
284
|
+
* @since 0.1.14
|
|
285
|
+
* @status experimental
|
|
286
|
+
* @spec RFC 5755, X.509
|
|
287
|
+
* @related pki.schema.parse, pki.schema.x509.parse
|
|
288
|
+
*
|
|
289
|
+
* Parse a DER `Buffer` or a PEM string/Buffer into a structured v2 attribute
|
|
290
|
+
* certificate: `{ version, holder, issuer, signatureAlgorithm, serialNumber,
|
|
291
|
+
* serialNumberHex, validity, attributes, issuerUniqueID, extensions, tbsBytes,
|
|
292
|
+
* signatureValue }`. The holder / issuer identities come back as validated
|
|
293
|
+
* `GeneralName`s (each element `{ bytes, tagClass, tagNumber }`); the validity
|
|
294
|
+
* window is real `Date`s; `tbsBytes` is the exact signed byte range for a verifier.
|
|
295
|
+
*
|
|
296
|
+
* Throws `AttrCertError` when the bytes are not a well-formed attribute certificate
|
|
297
|
+
* (an obsolete `AttributeCertificateV1` throws `attrcert/legacy-v1-not-supported`)
|
|
298
|
+
* and `Asn1Error` when the underlying DER is malformed.
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* var ac = pki.schema.attrcert.parse(der);
|
|
302
|
+
* ac.attributes[0].name; // "role"
|
|
303
|
+
* ac.validity.notAfterTime;// Date
|
|
304
|
+
*/
|
|
305
|
+
var PARSE_OPTS = { pemLabel: null, PemError: PemError, ErrorClass: AttrCertError, prefix: "attrcert", what: "attribute certificate" };
|
|
306
|
+
|
|
307
|
+
function _legacyV1Error() {
|
|
308
|
+
return new AttrCertError("attrcert/legacy-v1-not-supported", "AttributeCertificateV1 (X.509-1997) is obsolete and not parsed by this build (RFC 5755 §1)");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function parse(input) {
|
|
312
|
+
var root = pkix.decodeRoot(pkix.coerceToDer(input, PARSE_OPTS), PARSE_OPTS);
|
|
313
|
+
// A well-formed legacy AttributeCertificateV1 gets the advertised, stable
|
|
314
|
+
// attrcert/legacy-v1-not-supported on the DIRECT path too — not a low-level asn1/*
|
|
315
|
+
// tag error from attempting the v2 walk — so a direct caller of this entry sees the
|
|
316
|
+
// same error family for the recognized-but-deferred form as the orchestrator does.
|
|
317
|
+
if (matchesV1(root)) throw _legacyV1Error();
|
|
318
|
+
return schema.walk(ATTRIBUTE_CERTIFICATE, root, NS).result;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// The obsolete X.509-1997 AttributeCertificateV1 (RFC 5652 §10.2) is recognized and
|
|
322
|
+
// deferred: decode the envelope (so a malformed input still fails as bad DER), then
|
|
323
|
+
// throw the precise diagnostic rather than routing it to a wrong format.
|
|
324
|
+
function parseV1(input) {
|
|
325
|
+
pkix.decodeRoot(pkix.coerceToDer(input, PARSE_OPTS), PARSE_OPTS);
|
|
326
|
+
throw _legacyV1Error();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @primitive pki.schema.attrcert.pemDecode
|
|
331
|
+
* @signature pki.schema.attrcert.pemDecode(text, label?) -> Buffer
|
|
332
|
+
* @since 0.1.14
|
|
333
|
+
* @status experimental
|
|
334
|
+
* @spec RFC 7468, RFC 5755
|
|
335
|
+
* @related pki.schema.attrcert.parse
|
|
336
|
+
*
|
|
337
|
+
* Extract the DER bytes from a PEM block (OpenSSL emits
|
|
338
|
+
* `-----BEGIN ATTRIBUTE CERTIFICATE-----`; the first block is taken unless `label`
|
|
339
|
+
* is given). Throws `PemError` on a missing envelope or a non-base64 body.
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* var der = pki.schema.attrcert.pemDecode(pemText);
|
|
343
|
+
*/
|
|
344
|
+
function pemDecode(text, label) { return pkix.pemDecode(text, label || null, PemError); }
|
|
345
|
+
|
|
346
|
+
// matches(root): a v2 AttributeCertificate is the signed-envelope trio whose acinfo
|
|
347
|
+
// LEADS WITH a bare universal INTEGER version (== v2 = 1, NOT the x509 [0] EXPLICIT
|
|
348
|
+
// wrapper) and whose attrCertValidityPeriod at children[5] is a SEQUENCE of exactly
|
|
349
|
+
// two GeneralizedTime (RFC 5755 §4.2.6 forbids UTCTime) — a marker no cert / CRL /
|
|
350
|
+
// CSR presents at that position. Disjoint from matchesV1 by the children[0] tag class.
|
|
351
|
+
function matches(root) {
|
|
352
|
+
var acinfo = pkix.signedEnvelopeTbs(root);
|
|
353
|
+
if (!acinfo) return false;
|
|
354
|
+
var k = acinfo.children;
|
|
355
|
+
if (!k || k.length < 7) return false;
|
|
356
|
+
if (!(k[0].tagClass === "universal" && k[0].tagNumber === TAGS.INTEGER)) return false;
|
|
357
|
+
if (!(k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE && k[1].children)) return false;
|
|
358
|
+
var v = k[5];
|
|
359
|
+
if (!(v && v.tagClass === "universal" && v.tagNumber === TAGS.SEQUENCE && v.children && v.children.length === 2)) return false;
|
|
360
|
+
return v.children.every(function (t) { return t.tagClass === "universal" && t.tagNumber === TAGS.GENERALIZED_TIME; });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// matchesV1(root): the obsolete AttributeCertificateV1 — its acInfo (version DEFAULT
|
|
364
|
+
// v1 OMITTED) LEADS WITH the subject CHOICE (a context [0]/[1]), and its children[1]
|
|
365
|
+
// is the issuer GeneralNames (a universal SEQUENCE). The children[1] == SEQUENCE test
|
|
366
|
+
// keeps it disjoint from a v3 certificate (whose [0] EXPLICIT version is followed by
|
|
367
|
+
// an INTEGER serialNumber), so v1-vs-cert dispatch is order-independent.
|
|
368
|
+
function matchesV1(root) {
|
|
369
|
+
var acinfo = pkix.signedEnvelopeTbs(root);
|
|
370
|
+
if (!acinfo) return false;
|
|
371
|
+
var k = acinfo.children;
|
|
372
|
+
if (!k || k.length < 6) return false;
|
|
373
|
+
if (!(k[0].tagClass === "context" && (k[0].tagNumber === 0 || k[0].tagNumber === 1) && k[0].children)) return false;
|
|
374
|
+
return !!k[1] && k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
module.exports = {
|
|
378
|
+
parse: parse,
|
|
379
|
+
parseV1: parseV1,
|
|
380
|
+
pemDecode: pemDecode,
|
|
381
|
+
matches: matches,
|
|
382
|
+
matchesV1: matchesV1,
|
|
383
|
+
};
|
package/lib/schema-cms.js
CHANGED
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
* @slug cms
|
|
10
10
|
*
|
|
11
11
|
* @intro
|
|
12
|
-
* CMS
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* CMS handling per RFC 5652 (§3 ContentInfo envelope). `parse` turns a DER or PEM
|
|
13
|
+
* (`CMS`) message into a structured object and is an OID-dispatch envelope —
|
|
14
|
+
* ContentInfo reads its `contentType` and structurally decodes SignedData (§5),
|
|
15
|
+
* EnvelopedData (§6, with all five RecipientInfo kinds — key-transport,
|
|
16
|
+
* key-agreement per RFC 5753, KEK, password, and other), and EncryptedData (§8);
|
|
17
|
+
* the remaining PKCS#7 content types are recognized and rejected with a precise
|
|
18
|
+
* `cms/unsupported-content-type` rather than a generic unknown-format error. A
|
|
19
|
+
* SignedData surfaces its version, digest algorithms, encapsulated content,
|
|
20
|
+
* certificate / CRL sets, and signer infos; an EnvelopedData its recipient infos
|
|
21
|
+
* and encrypted content info; an EncryptedData its encrypted content info.
|
|
20
22
|
*
|
|
21
23
|
* CMS is a signed container: the bytes an external verifier must hash are
|
|
22
24
|
* surfaced RAW and never re-serialized. `encapContentInfo.eContent` is the raw
|
|
@@ -53,15 +55,26 @@ var NAME = pkix.name(NS);
|
|
|
53
55
|
// version is {1,3} (RFC 5652 §5.1, §5.3). Wider accept maps than any other format.
|
|
54
56
|
var SIGNED_DATA_VERSION = pkix.versionReader(NS, { "1": 1, "3": 3, "4": 4, "5": 5 });
|
|
55
57
|
var SIGNER_VERSION = pkix.versionReader(NS, { "1": 1, "3": 3 });
|
|
58
|
+
// EnvelopedData §6.1 (0/2/3/4), EncryptedData §8 (0/2), and the per-RecipientInfo
|
|
59
|
+
// versions (RFC 5652 §6.2.1-§6.2.4): ktri {0,2}, kari {3}, kekri {4}, pwri {0}.
|
|
60
|
+
var ENVELOPED_DATA_VERSION = pkix.versionReader(NS, { "0": 0, "2": 2, "3": 3, "4": 4 });
|
|
61
|
+
var ENCRYPTED_DATA_VERSION = pkix.versionReader(NS, { "0": 0, "2": 2 });
|
|
62
|
+
var KTRI_VERSION = pkix.versionReader(NS, { "0": 0, "2": 2 });
|
|
63
|
+
var KARI_VERSION = pkix.versionReader(NS, { "3": 3 });
|
|
64
|
+
var KEKRI_VERSION = pkix.versionReader(NS, { "4": 4 });
|
|
65
|
+
var PWRI_VERSION = pkix.versionReader(NS, { "0": 0 });
|
|
56
66
|
|
|
57
|
-
// id-signedData
|
|
58
|
-
// are recognized-and-deferred (a precise
|
|
59
|
-
// OIDs resolve from the registry (pkcs7 /
|
|
67
|
+
// id-signedData / id-envelopedData / id-encryptedData are the content types this
|
|
68
|
+
// build structurally decodes; the rest are recognized-and-deferred (a precise
|
|
69
|
+
// diagnostic, not a silent unknown-format). OIDs resolve from the registry (pkcs7 /
|
|
70
|
+
// smimeCt families), never dotted literals.
|
|
60
71
|
var OID_SIGNED_DATA = oid.byName("signedData");
|
|
72
|
+
var OID_ENVELOPED_DATA = oid.byName("envelopedData");
|
|
73
|
+
var OID_ENCRYPTED_DATA = oid.byName("encryptedData");
|
|
61
74
|
var OID_DATA = oid.byName("data");
|
|
62
75
|
var DEFERRED = new Set([
|
|
63
|
-
oid.byName("data"), oid.byName("
|
|
64
|
-
oid.byName("digestedData"), oid.byName("
|
|
76
|
+
oid.byName("data"), oid.byName("signedAndEnvelopedData"),
|
|
77
|
+
oid.byName("digestedData"), oid.byName("authData"),
|
|
65
78
|
]);
|
|
66
79
|
// The two mandatory signed-attribute types (RFC 5652 §5.3, §11.1/§11.2).
|
|
67
80
|
var OID_CONTENT_TYPE = oid.byName("contentType");
|
|
@@ -277,11 +290,313 @@ var SIGNED_DATA = schema.seq([
|
|
|
277
290
|
},
|
|
278
291
|
});
|
|
279
292
|
|
|
293
|
+
// ==== EnvelopedData / EncryptedData (RFC 5652 §6/§8, RFC 5753) ========
|
|
294
|
+
var T = asn1.TAGS;
|
|
295
|
+
|
|
296
|
+
// EncryptedContentInfo ::= SEQUENCE { contentType OID, contentEncryptionAlgorithm
|
|
297
|
+
// AlgorithmIdentifier, encryptedContent [0] IMPLICIT OCTET STRING OPTIONAL } (RFC
|
|
298
|
+
// 5652 §6.1). encryptedContent is [0] IMPLICIT (context PRIMITIVE) — its content
|
|
299
|
+
// octets ARE the ciphertext directly, so it reads through implicitOctetString(0),
|
|
300
|
+
// NOT the [0] EXPLICIT shape ENCAP_CONTENT_INFO uses (which would double-strip a
|
|
301
|
+
// length header). The ciphertext + algorithm parameters are surfaced RAW.
|
|
302
|
+
var ENCRYPTED_CONTENT_INFO = schema.seq([
|
|
303
|
+
schema.field("contentType", schema.oidLeaf()),
|
|
304
|
+
schema.field("contentEncryptionAlgorithm", ALGORITHM_IDENTIFIER),
|
|
305
|
+
schema.optional("encryptedContent", schema.implicitOctetString(0), { tag: 0 }),
|
|
306
|
+
], {
|
|
307
|
+
assert: "sequence", arity: { min: 2, max: 3 }, code: "cms/bad-encrypted-content-info", what: "EncryptedContentInfo",
|
|
308
|
+
build: function (m) {
|
|
309
|
+
return {
|
|
310
|
+
contentType: m.fields.contentType.value,
|
|
311
|
+
contentEncryptionAlgorithm: m.fields.contentEncryptionAlgorithm.value.result,
|
|
312
|
+
encryptedContent: m.fields.encryptedContent.present ? m.fields.encryptedContent.value : null,
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// RecipientIdentifier ::= CHOICE { issuerAndSerialNumber, subjectKeyIdentifier [0]
|
|
318
|
+
// IMPLICIT OCTET STRING } (RFC 5652 §6.2.1) — structurally identical to
|
|
319
|
+
// SignerIdentifier; reuse ISSUER_AND_SERIAL + the implicitOctetString(0) leaf.
|
|
320
|
+
var RECIPIENT_IDENTIFIER = schema.choice([
|
|
321
|
+
{ when: { tagClass: "universal", tagNumber: T.SEQUENCE }, schema: ISSUER_AND_SERIAL },
|
|
322
|
+
{ when: { tagClass: "context", tagNumber: 0 }, schema: schema.implicitOctetString(0) },
|
|
323
|
+
], { code: "cms/bad-recipient-identifier", what: "RecipientIdentifier" });
|
|
324
|
+
|
|
325
|
+
// KeyTransRecipientInfo ::= SEQUENCE { version(0|2), rid RecipientIdentifier,
|
|
326
|
+
// keyEncryptionAlgorithm, encryptedKey OCTET STRING } (RFC 5652 §6.2.1).
|
|
327
|
+
var KEY_TRANS_RECIPIENT_INFO = schema.seq([
|
|
328
|
+
schema.field("version", KTRI_VERSION),
|
|
329
|
+
schema.field("rid", RECIPIENT_IDENTIFIER),
|
|
330
|
+
schema.field("keyEncryptionAlgorithm", ALGORITHM_IDENTIFIER),
|
|
331
|
+
schema.field("encryptedKey", schema.octetString()),
|
|
332
|
+
], {
|
|
333
|
+
assert: "sequence", code: "cms/bad-ktri", what: "KeyTransRecipientInfo",
|
|
334
|
+
build: function (m) {
|
|
335
|
+
var version = m.fields.version.value;
|
|
336
|
+
var ridNode = m.fields.rid.node;
|
|
337
|
+
var isSkid = ridNode.tagClass === "context" && ridNode.tagNumber === 0;
|
|
338
|
+
// RFC 5652 §6.2.1 — rid ⇔ version: issuerAndSerialNumber ⇒ 0, subjectKeyIdentifier ⇒ 2.
|
|
339
|
+
if (isSkid && version !== 2) throw NS.E("cms/bad-recipient-version", "a subjectKeyIdentifier recipient identifier requires KeyTransRecipientInfo version 2 (RFC 5652 §6.2.1)");
|
|
340
|
+
if (!isSkid && version !== 0) throw NS.E("cms/bad-recipient-version", "an issuerAndSerialNumber recipient identifier requires KeyTransRecipientInfo version 0 (RFC 5652 §6.2.1)");
|
|
341
|
+
return {
|
|
342
|
+
type: "ktri", version: version,
|
|
343
|
+
rid: isSkid ? { subjectKeyIdentifier: m.fields.rid.value } : m.fields.rid.value.result,
|
|
344
|
+
ridType: isSkid ? "subjectKeyIdentifier" : "issuerAndSerialNumber",
|
|
345
|
+
keyEncryptionAlgorithm: m.fields.keyEncryptionAlgorithm.value.result,
|
|
346
|
+
encryptedKey: m.fields.encryptedKey.value,
|
|
347
|
+
};
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// OriginatorPublicKey ::= SEQUENCE { algorithm, publicKey BIT STRING } (RFC 5753
|
|
352
|
+
// §3.1.1), reached as originatorKey [1] IMPLICIT — SPKI-shaped but cannot reuse
|
|
353
|
+
// pkix.spki (which asserts a universal SEQUENCE), so assert:"constructed".
|
|
354
|
+
var ORIGINATOR_PUBLIC_KEY = schema.seq([
|
|
355
|
+
schema.field("algorithm", ALGORITHM_IDENTIFIER),
|
|
356
|
+
schema.field("publicKey", schema.bitString()),
|
|
357
|
+
], {
|
|
358
|
+
assert: "constructed", code: "cms/bad-originator-public-key", what: "OriginatorPublicKey",
|
|
359
|
+
build: function (m) { return { algorithm: m.fields.algorithm.value.result, publicKey: m.fields.publicKey.value }; },
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// OriginatorIdentifierOrKey ::= CHOICE { issuerAndSerialNumber, subjectKeyIdentifier
|
|
363
|
+
// [0] IMPLICIT OCTET STRING, originatorKey [1] IMPLICIT OriginatorPublicKey }.
|
|
364
|
+
var ORIGINATOR_IDENTIFIER_OR_KEY = schema.choice([
|
|
365
|
+
{ when: { tagClass: "universal", tagNumber: T.SEQUENCE }, schema: ISSUER_AND_SERIAL },
|
|
366
|
+
{ when: { tagClass: "context", tagNumber: 0 }, schema: schema.implicitOctetString(0) },
|
|
367
|
+
{ when: { tagClass: "context", tagNumber: 1 }, schema: ORIGINATOR_PUBLIC_KEY },
|
|
368
|
+
], { code: "cms/bad-originator-identifier", what: "OriginatorIdentifierOrKey" });
|
|
369
|
+
|
|
370
|
+
// RecipientKeyIdentifier (RFC 5652 §6.2.2) and KEKIdentifier (§6.2.3) are one
|
|
371
|
+
// shape — { <keyId> OCTET STRING, date GeneralizedTime OPTIONAL, other
|
|
372
|
+
// OtherKeyAttribute OPTIONAL } — differing only in the key-id field's name and
|
|
373
|
+
// the enclosing tag form. One factory defines both so the OPTIONAL handling
|
|
374
|
+
// (date and the raw-surfaced OtherKeyAttribute) cannot diverge between them.
|
|
375
|
+
function keyIdentifierSchema(keyIdName, assert, code, what) {
|
|
376
|
+
return schema.seq([
|
|
377
|
+
schema.field(keyIdName, schema.octetString()),
|
|
378
|
+
schema.optional("date", schema.time(NS), { whenUniversal: [T.GENERALIZED_TIME] }),
|
|
379
|
+
schema.optional("other", schema.any(), { whenUniversal: [T.SEQUENCE] }),
|
|
380
|
+
], {
|
|
381
|
+
assert: assert, code: code, what: what,
|
|
382
|
+
build: function (m) {
|
|
383
|
+
var out = {};
|
|
384
|
+
out[keyIdName] = m.fields[keyIdName].value;
|
|
385
|
+
out.date = m.fields.date.present ? m.fields.date.value : null;
|
|
386
|
+
out.other = m.fields.other.present ? m.fields.other.node.bytes : null;
|
|
387
|
+
return out;
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Reached as rKeyId [0] IMPLICIT (a SEQUENCE — constructed, unlike ktri's [0] leaf).
|
|
393
|
+
var RECIPIENT_KEY_IDENTIFIER = keyIdentifierSchema("subjectKeyIdentifier",
|
|
394
|
+
"constructed", "cms/bad-recipient-key-identifier", "RecipientKeyIdentifier");
|
|
395
|
+
|
|
396
|
+
// KeyAgreeRecipientIdentifier ::= CHOICE { issuerAndSerialNumber, rKeyId [0] IMPLICIT
|
|
397
|
+
// RecipientKeyIdentifier } (RFC 5652 §6.2.2).
|
|
398
|
+
var KEY_AGREE_RECIPIENT_IDENTIFIER = schema.choice([
|
|
399
|
+
{ when: { tagClass: "universal", tagNumber: T.SEQUENCE }, schema: ISSUER_AND_SERIAL },
|
|
400
|
+
{ when: { tagClass: "context", tagNumber: 0 }, schema: RECIPIENT_KEY_IDENTIFIER },
|
|
401
|
+
], { code: "cms/bad-kari-identifier", what: "KeyAgreeRecipientIdentifier" });
|
|
402
|
+
|
|
403
|
+
// RecipientEncryptedKey ::= SEQUENCE { rid KeyAgreeRecipientIdentifier, encryptedKey
|
|
404
|
+
// OCTET STRING } (RFC 5652 §6.2.2).
|
|
405
|
+
var RECIPIENT_ENCRYPTED_KEY = schema.seq([
|
|
406
|
+
schema.field("rid", KEY_AGREE_RECIPIENT_IDENTIFIER),
|
|
407
|
+
schema.field("encryptedKey", schema.octetString()),
|
|
408
|
+
], {
|
|
409
|
+
assert: "sequence", code: "cms/bad-recipient-encrypted-key", what: "RecipientEncryptedKey",
|
|
410
|
+
build: function (m) {
|
|
411
|
+
var ridNode = m.fields.rid.node;
|
|
412
|
+
var isRkid = ridNode.tagClass === "context" && ridNode.tagNumber === 0;
|
|
413
|
+
return {
|
|
414
|
+
rid: isRkid ? m.fields.rid.value.result : m.fields.rid.value.result,
|
|
415
|
+
encryptedKey: m.fields.encryptedKey.value,
|
|
416
|
+
};
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// KeyAgreeRecipientInfo ::= [1] IMPLICIT SEQUENCE { version(3), originator [0]
|
|
421
|
+
// EXPLICIT OriginatorIdentifierOrKey, ukm [1] EXPLICIT OPTIONAL,
|
|
422
|
+
// keyEncryptionAlgorithm, recipientEncryptedKeys SEQUENCE OF } (RFC 5652 §6.2.2 +
|
|
423
|
+
// RFC 5753 §3.1.1). originator [0] is EXPLICIT (wraps a CHOICE).
|
|
424
|
+
var KEY_AGREE_RECIPIENT_INFO = schema.seq([
|
|
425
|
+
schema.field("version", KARI_VERSION),
|
|
426
|
+
schema.field("originator", schema.explicit(0, ORIGINATOR_IDENTIFIER_OR_KEY, { code: "cms/bad-kari" })),
|
|
427
|
+
schema.optional("ukm", schema.octetString(), { tag: 1, explicit: true, emptyCode: "cms/bad-kari" }),
|
|
428
|
+
schema.field("keyEncryptionAlgorithm", ALGORITHM_IDENTIFIER),
|
|
429
|
+
schema.field("recipientEncryptedKeys", schema.seqOf(RECIPIENT_ENCRYPTED_KEY, { code: "cms/bad-recipient-encrypted-keys", what: "recipientEncryptedKeys" })),
|
|
430
|
+
], {
|
|
431
|
+
assert: "constructed", code: "cms/bad-kari", what: "KeyAgreeRecipientInfo",
|
|
432
|
+
build: function (m) {
|
|
433
|
+
var origNode = m.fields.originator.node.children[0];
|
|
434
|
+
var origForm = origNode.tagClass === "context" ? (origNode.tagNumber === 0 ? "subjectKeyIdentifier" : "originatorKey") : "issuerAndSerialNumber";
|
|
435
|
+
var origVal = m.fields.originator.value;
|
|
436
|
+
return {
|
|
437
|
+
type: "kari", version: m.fields.version.value,
|
|
438
|
+
originator: { form: origForm, value: origForm === "subjectKeyIdentifier" ? origVal : origVal.result },
|
|
439
|
+
ukm: m.fields.ukm.present ? m.fields.ukm.value : null,
|
|
440
|
+
keyEncryptionAlgorithm: m.fields.keyEncryptionAlgorithm.value.result,
|
|
441
|
+
recipientEncryptedKeys: m.fields.recipientEncryptedKeys.value.items.map(function (it) { return it.value.result; }),
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// KEKRecipientInfo ::= [2] IMPLICIT SEQUENCE { version(4), kekid KEKIdentifier,
|
|
447
|
+
// keyEncryptionAlgorithm, encryptedKey } (RFC 5652 §6.2.3).
|
|
448
|
+
var KEK_IDENTIFIER = keyIdentifierSchema("keyIdentifier",
|
|
449
|
+
"sequence", "cms/bad-kek-identifier", "KEKIdentifier");
|
|
450
|
+
var KEK_RECIPIENT_INFO = schema.seq([
|
|
451
|
+
schema.field("version", KEKRI_VERSION),
|
|
452
|
+
schema.field("kekid", KEK_IDENTIFIER),
|
|
453
|
+
schema.field("keyEncryptionAlgorithm", ALGORITHM_IDENTIFIER),
|
|
454
|
+
schema.field("encryptedKey", schema.octetString()),
|
|
455
|
+
], {
|
|
456
|
+
assert: "constructed", code: "cms/bad-kekri", what: "KEKRecipientInfo",
|
|
457
|
+
build: function (m) {
|
|
458
|
+
return {
|
|
459
|
+
type: "kekri", version: m.fields.version.value,
|
|
460
|
+
kekid: m.fields.kekid.value.result,
|
|
461
|
+
keyEncryptionAlgorithm: m.fields.keyEncryptionAlgorithm.value.result,
|
|
462
|
+
encryptedKey: m.fields.encryptedKey.value,
|
|
463
|
+
};
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// PasswordRecipientInfo ::= [3] IMPLICIT SEQUENCE { version(0), keyDerivationAlgorithm
|
|
468
|
+
// [0] IMPLICIT AlgorithmIdentifier OPTIONAL, keyEncryptionAlgorithm, encryptedKey }
|
|
469
|
+
// (RFC 5652 §6.2.4). keyDerivationAlgorithm [0] IMPLICIT is the one field needing
|
|
470
|
+
// the implicitTag AlgorithmIdentifier; present iff the first post-version node is [0].
|
|
471
|
+
var PASSWORD_RECIPIENT_INFO = schema.seq([
|
|
472
|
+
schema.field("version", PWRI_VERSION),
|
|
473
|
+
schema.optional("keyDerivationAlgorithm", pkix.algorithmIdentifier(NS, { implicitTag: 0 }), { tag: 0 }),
|
|
474
|
+
schema.field("keyEncryptionAlgorithm", ALGORITHM_IDENTIFIER),
|
|
475
|
+
schema.field("encryptedKey", schema.octetString()),
|
|
476
|
+
], {
|
|
477
|
+
assert: "constructed", code: "cms/bad-pwri", what: "PasswordRecipientInfo",
|
|
478
|
+
build: function (m) {
|
|
479
|
+
return {
|
|
480
|
+
type: "pwri", version: m.fields.version.value,
|
|
481
|
+
keyDerivationAlgorithm: m.fields.keyDerivationAlgorithm.present ? m.fields.keyDerivationAlgorithm.value.result : null,
|
|
482
|
+
keyEncryptionAlgorithm: m.fields.keyEncryptionAlgorithm.value.result,
|
|
483
|
+
encryptedKey: m.fields.encryptedKey.value,
|
|
484
|
+
};
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// OtherRecipientInfo ::= [4] IMPLICIT SEQUENCE { oriType OID, oriValue ANY } (RFC
|
|
489
|
+
// 5652 §6.2.5) — oriValue opaque, surfaced RAW (the shipped encoder routes
|
|
490
|
+
// KEMRecipientInfo through here).
|
|
491
|
+
var OTHER_RECIPIENT_INFO = schema.seq([
|
|
492
|
+
schema.field("oriType", schema.oidLeaf()),
|
|
493
|
+
schema.field("oriValue", schema.any()),
|
|
494
|
+
], {
|
|
495
|
+
assert: "constructed", code: "cms/bad-ori", what: "OtherRecipientInfo",
|
|
496
|
+
build: function (m) { return { type: "ori", oriType: m.fields.oriType.value, oriValue: m.fields.oriValue.node.bytes }; },
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// RecipientInfo ::= CHOICE { ktri KeyTransRecipientInfo, kari [1], kekri [2], pwri
|
|
500
|
+
// [3], ori [4] } (RFC 5652 §6.2). A bare universal SEQUENCE is ktri (the untagged
|
|
501
|
+
// alternative). An unknown context tag → no arm → cms/bad-recipient-info.
|
|
502
|
+
var RECIPIENT_INFO = schema.choice([
|
|
503
|
+
{ when: { tagClass: "universal", tagNumber: T.SEQUENCE }, schema: KEY_TRANS_RECIPIENT_INFO },
|
|
504
|
+
{ when: { tagClass: "context", tagNumber: 1 }, schema: KEY_AGREE_RECIPIENT_INFO },
|
|
505
|
+
{ when: { tagClass: "context", tagNumber: 2 }, schema: KEK_RECIPIENT_INFO },
|
|
506
|
+
{ when: { tagClass: "context", tagNumber: 3 }, schema: PASSWORD_RECIPIENT_INFO },
|
|
507
|
+
{ when: { tagClass: "context", tagNumber: 4 }, schema: OTHER_RECIPIENT_INFO },
|
|
508
|
+
], { code: "cms/bad-recipient-info", what: "RecipientInfo" });
|
|
509
|
+
|
|
510
|
+
// OriginatorInfo ::= [0] IMPLICIT SEQUENCE { certs [0] IMPLICIT OPTIONAL, crls [1]
|
|
511
|
+
// IMPLICIT OPTIONAL } (RFC 5652 §6.1). Members surfaced RAW (their outer tag feeds
|
|
512
|
+
// the version rule).
|
|
513
|
+
var ORIGINATOR_INFO = schema.seq([
|
|
514
|
+
schema.optional("certs", schema.implicitSetOf(0, schema.any(), { min: 1, code: "cms/bad-originator-certs", what: "certs" }), { tag: 0 }),
|
|
515
|
+
schema.optional("crls", schema.implicitSetOf(1, schema.any(), { min: 1, code: "cms/bad-originator-crls", what: "crls" }), { tag: 1 }),
|
|
516
|
+
], {
|
|
517
|
+
assert: "constructed", code: "cms/bad-originator-info", what: "OriginatorInfo",
|
|
518
|
+
build: function (m) {
|
|
519
|
+
return {
|
|
520
|
+
certs: m.fields.certs.present ? m.fields.certs.value.items.map(rawElement) : [],
|
|
521
|
+
crls: m.fields.crls.present ? m.fields.crls.value.items.map(rawElement) : [],
|
|
522
|
+
};
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// RFC 5652 §6.1 — the exact EnvelopedData CMSVersion, from originatorInfo's raw
|
|
527
|
+
// cert/crl outer tags and the recipient arms (a cert `other` is [3], a v2AttrCert
|
|
528
|
+
// [2]; a crl `other` is [1]; a pwri or ori forces v3; all-ktri-IAS with no
|
|
529
|
+
// originatorInfo/unprotectedAttrs is v0; everything else v2).
|
|
530
|
+
function _expectedEnvelopedDataVersion(originatorInfo, recipientInfos, hasUnprotectedAttrs) {
|
|
531
|
+
function ctx(el, n) { return el.tagClass === "context" && el.tagNumber === n; }
|
|
532
|
+
var hasOrig = !!originatorInfo;
|
|
533
|
+
var certs = hasOrig ? originatorInfo.certs : [];
|
|
534
|
+
var crls = hasOrig ? originatorInfo.crls : [];
|
|
535
|
+
if (hasOrig && (certs.some(function (c) { return ctx(c, 3); }) || crls.some(function (c) { return ctx(c, 1); }))) return 4;
|
|
536
|
+
if ((hasOrig && certs.some(function (c) { return ctx(c, 2); })) ||
|
|
537
|
+
recipientInfos.some(function (r) { return r.type === "pwri" || r.type === "ori"; })) return 3;
|
|
538
|
+
if (!hasOrig && !hasUnprotectedAttrs &&
|
|
539
|
+
recipientInfos.every(function (r) { return r.type === "ktri" && r.ridType === "issuerAndSerialNumber"; })) return 0;
|
|
540
|
+
return 2;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// EnvelopedData ::= SEQUENCE { version, originatorInfo [0] IMPLICIT OPTIONAL,
|
|
544
|
+
// recipientInfos RecipientInfos (SET SIZE 1..MAX), encryptedContentInfo,
|
|
545
|
+
// unprotectedAttrs [1] IMPLICIT OPTIONAL } (RFC 5652 §6.1). recipientInfos is
|
|
546
|
+
// min:1 (an empty SET is non-conformant — the INVERSE of SignedData's degenerate
|
|
547
|
+
// signerInfos).
|
|
548
|
+
var ENVELOPED_DATA = schema.seq([
|
|
549
|
+
schema.field("version", ENVELOPED_DATA_VERSION),
|
|
550
|
+
schema.optional("originatorInfo", ORIGINATOR_INFO, { tag: 0 }),
|
|
551
|
+
schema.field("recipientInfos", schema.setOf(RECIPIENT_INFO, { min: 1, code: "cms/bad-recipient-infos", what: "recipientInfos" })),
|
|
552
|
+
schema.field("encryptedContentInfo", ENCRYPTED_CONTENT_INFO),
|
|
553
|
+
schema.optional("unprotectedAttrs", schema.implicitSetOf(1, ATTRIBUTE, { min: 1, code: "cms/bad-unprotected-attrs", what: "unprotectedAttrs" }), { tag: 1 }),
|
|
554
|
+
], {
|
|
555
|
+
assert: "sequence", code: "cms/bad-enveloped-data", what: "EnvelopedData",
|
|
556
|
+
build: function (m) {
|
|
557
|
+
var version = m.fields.version.value;
|
|
558
|
+
var originatorInfo = m.fields.originatorInfo.present ? m.fields.originatorInfo.value.result : null;
|
|
559
|
+
var recipientInfos = m.fields.recipientInfos.value.items.map(function (it) { return it.value.result; });
|
|
560
|
+
var hasUnprotectedAttrs = m.fields.unprotectedAttrs.present;
|
|
561
|
+
var expected = _expectedEnvelopedDataVersion(originatorInfo, recipientInfos, hasUnprotectedAttrs);
|
|
562
|
+
if (version !== expected) throw NS.E("cms/bad-version", "EnvelopedData version " + version + " does not match its contents (RFC 5652 §6.1 requires v" + expected + ")");
|
|
563
|
+
return {
|
|
564
|
+
version: version,
|
|
565
|
+
originatorInfo: originatorInfo,
|
|
566
|
+
recipientInfos: recipientInfos,
|
|
567
|
+
encryptedContentInfo: m.fields.encryptedContentInfo.value.result,
|
|
568
|
+
unprotectedAttrs: hasUnprotectedAttrs ? m.fields.unprotectedAttrs.value.items.map(function (it) { return it.value.result; }) : null,
|
|
569
|
+
};
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// EncryptedData ::= SEQUENCE { version, encryptedContentInfo, unprotectedAttrs [1]
|
|
574
|
+
// IMPLICIT OPTIONAL } (RFC 5652 §8) — no recipients, no originatorInfo; the CEK is
|
|
575
|
+
// distributed out of band. version is 0, or 2 iff unprotectedAttrs are present.
|
|
576
|
+
var ENCRYPTED_DATA = schema.seq([
|
|
577
|
+
schema.field("version", ENCRYPTED_DATA_VERSION),
|
|
578
|
+
schema.field("encryptedContentInfo", ENCRYPTED_CONTENT_INFO),
|
|
579
|
+
schema.optional("unprotectedAttrs", schema.implicitSetOf(1, ATTRIBUTE, { min: 1, code: "cms/bad-unprotected-attrs", what: "unprotectedAttrs" }), { tag: 1 }),
|
|
580
|
+
], {
|
|
581
|
+
assert: "sequence", code: "cms/bad-encrypted-data", what: "EncryptedData",
|
|
582
|
+
build: function (m) {
|
|
583
|
+
var version = m.fields.version.value;
|
|
584
|
+
var hasUnprotectedAttrs = m.fields.unprotectedAttrs.present;
|
|
585
|
+
var expected = hasUnprotectedAttrs ? 2 : 0;
|
|
586
|
+
if (version !== expected) throw NS.E("cms/bad-version", "EncryptedData version " + version + " does not match its contents (RFC 5652 §8 requires v" + expected + ")");
|
|
587
|
+
return {
|
|
588
|
+
version: version,
|
|
589
|
+
encryptedContentInfo: m.fields.encryptedContentInfo.value.result,
|
|
590
|
+
unprotectedAttrs: hasUnprotectedAttrs ? m.fields.unprotectedAttrs.value.items.map(function (it) { return it.value.result; }) : null,
|
|
591
|
+
};
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
|
|
280
595
|
// ContentInfo ::= SEQUENCE { contentType OID, content [0] EXPLICIT ANY DEFINED BY
|
|
281
596
|
// contentType } (RFC 5652 §3). The content is captured raw (explicit(0, any()))
|
|
282
597
|
// and re-dispatched by contentType inside the build: id-signedData walks
|
|
283
|
-
// SIGNED_DATA,
|
|
284
|
-
// are rejected.
|
|
598
|
+
// SIGNED_DATA, id-envelopedData / id-encryptedData walk their schemas, the other
|
|
599
|
+
// PKCS#7 types are recognized-and-deferred, unknown OIDs are rejected.
|
|
285
600
|
var CONTENT_INFO = schema.seq([
|
|
286
601
|
schema.field("contentType", schema.oidLeaf()),
|
|
287
602
|
schema.field("content", schema.explicit(0, schema.any(), { code: "cms/not-a-content-info" })),
|
|
@@ -290,6 +605,8 @@ var CONTENT_INFO = schema.seq([
|
|
|
290
605
|
build: function (m, ctx) {
|
|
291
606
|
var ct = m.fields.contentType.value;
|
|
292
607
|
if (ct === OID_SIGNED_DATA) return schema.walk(SIGNED_DATA, m.fields.content.value, ctx).result;
|
|
608
|
+
if (ct === OID_ENVELOPED_DATA) return schema.walk(ENVELOPED_DATA, m.fields.content.value, ctx).result;
|
|
609
|
+
if (ct === OID_ENCRYPTED_DATA) return schema.walk(ENCRYPTED_DATA, m.fields.content.value, ctx).result;
|
|
293
610
|
if (DEFERRED.has(ct)) {
|
|
294
611
|
throw NS.E("cms/unsupported-content-type", (ctx.oid.name(ct) || ct) + " is recognized but not parsed by this build");
|
|
295
612
|
}
|
package/lib/schema-crl.js
CHANGED
|
@@ -121,6 +121,11 @@ var REVOKED_LIST = schema.seqOf(REVOKED_ENTRY, {
|
|
|
121
121
|
// revokedCertificates=SEQUENCE) are disambiguated by their universal tag;
|
|
122
122
|
// crlExtensions is modeled as a trailing [0]..[0] so a stray non-[0] trailing
|
|
123
123
|
// context tag is REJECTED (crl/bad-tbs), not silently ignored.
|
|
124
|
+
// `signature` is consumed by the CERTIFICATE_LIST build (the §5.1.1.2
|
|
125
|
+
// outer==inner agreement check reads tbsMatch.fields.signature.node.bytes);
|
|
126
|
+
// the operator reads the surfaced outer signatureAlgorithm, which that
|
|
127
|
+
// check proves byte-identical.
|
|
128
|
+
// allow:schema-build-drops-parsed-field
|
|
124
129
|
var TBS_CERTLIST = schema.seq([
|
|
125
130
|
schema.optional("version", CRL_VERSION, { whenUniversal: [TAGS.INTEGER] }),
|
|
126
131
|
schema.field("signature", ALGORITHM_IDENTIFIER),
|
package/lib/schema-engine.js
CHANGED
|
@@ -63,11 +63,12 @@ function _assertShape(schema, node, ctx) {
|
|
|
63
63
|
_fail(ctx, schema.code, (schema.what || "value") + " must be a constructed value");
|
|
64
64
|
}
|
|
65
65
|
} else if (mode === "implicit") {
|
|
66
|
-
// IMPLICIT [tag] SET/SEQUENCE OF
|
|
67
|
-
// tag, so the node is a context-class
|
|
68
|
-
// children are the items
|
|
66
|
+
// IMPLICIT [tag] SET/SEQUENCE OF or a fixed-field IMPLICIT [tag] SEQUENCE body:
|
|
67
|
+
// the context tag replaces the universal tag, so the node is a context-class
|
|
68
|
+
// constructed [tag] and its direct children are the items / positional fields
|
|
69
|
+
// (no inner universal tag, no EXPLICIT unwrap).
|
|
69
70
|
if (node.tagClass !== "context" || node.tagNumber !== schema.implicitTag || !node.children) {
|
|
70
|
-
_fail(ctx, schema.code, (schema.what || "value") + " must be an IMPLICIT [" + schema.implicitTag + "]
|
|
71
|
+
_fail(ctx, schema.code, (schema.what || "value") + " must be an IMPLICIT [" + schema.implicitTag + "] constructed value");
|
|
71
72
|
}
|
|
72
73
|
} else {
|
|
73
74
|
_fail(ctx, schema.code, "unknown assert mode " + JSON.stringify(mode));
|
|
@@ -163,7 +164,7 @@ function trailing(members, opts) {
|
|
|
163
164
|
|
|
164
165
|
function seq(fields, opts) {
|
|
165
166
|
opts = opts || {};
|
|
166
|
-
return { kind: "seq", fields: fields, assert: opts.assert || "sequence", arity: opts.arity,
|
|
167
|
+
return { kind: "seq", fields: fields, assert: opts.assert || "sequence", implicitTag: opts.implicitTag, arity: opts.arity,
|
|
167
168
|
code: opts.code, what: opts.what, build: opts.build, checks: opts.checks || [] };
|
|
168
169
|
}
|
|
169
170
|
|
package/lib/schema-pkix.js
CHANGED
|
@@ -129,12 +129,18 @@ function versionReader(ns, accept) {
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
// opts.implicitTag (optional): read the AlgorithmIdentifier as a [tag] IMPLICIT
|
|
133
|
+
// SEQUENCE (a context-class constructed node whose children are algorithm + parameters),
|
|
134
|
+
// for the RFC 5652 §6.2.4 pwri.keyDerivationAlgorithm [0]. With no opts the shape is a
|
|
135
|
+
// universal SEQUENCE, byte-identical to every existing caller.
|
|
136
|
+
function algorithmIdentifier(ns, opts) {
|
|
137
|
+
opts = opts || {};
|
|
133
138
|
return schema.seq([
|
|
134
139
|
schema.field("algorithm", schema.oidLeaf()),
|
|
135
140
|
schema.optional("parameters", schema.any(), { whenAny: true }),
|
|
136
141
|
], {
|
|
137
|
-
assert:
|
|
142
|
+
assert: opts.implicitTag != null ? "implicit" : "sequence", implicitTag: opts.implicitTag,
|
|
143
|
+
arity: { min: 1 }, code: ns.prefix + "/bad-algorithm-identifier", what: "AlgorithmIdentifier",
|
|
138
144
|
build: function (m, ctx) {
|
|
139
145
|
var dotted = m.fields.algorithm.value;
|
|
140
146
|
return { oid: dotted, name: ctx.oid.name(dotted) || null, parameters: m.fields.parameters.present ? m.fields.parameters.node.bytes : null };
|
|
@@ -264,6 +270,26 @@ function generalName(ns, opts) {
|
|
|
264
270
|
});
|
|
265
271
|
}
|
|
266
272
|
|
|
273
|
+
// GeneralNames ::= SEQUENCE OF GeneralName (RFC 5280 §4.2.1.6), SIZE (1..MAX). Every
|
|
274
|
+
// element is validated by generalName (its CHOICE alternative's form + content) and
|
|
275
|
+
// surfaced raw, so a caller carrying a GeneralNames field cannot accept a malformed
|
|
276
|
+
// element by treating the whole sequence as opaque bytes. Returns { names: [{ bytes,
|
|
277
|
+
// tagClass, tagNumber }], bytes } (the decoded element list + the raw outer DER).
|
|
278
|
+
// `opts.implicitTag` handles a [tag] IMPLICIT GeneralNames — the context tag REPLACES
|
|
279
|
+
// the universal SEQUENCE tag (RFC 5755 Holder.entityName [1]); otherwise it is a bare
|
|
280
|
+
// universal SEQUENCE OF. `opts.code` is the caller's error code. Shared so the x509 /
|
|
281
|
+
// attribute-certificate / (future) CRMF parsers validate a GeneralNames identically.
|
|
282
|
+
function generalNames(ns, opts) {
|
|
283
|
+
opts = opts || {};
|
|
284
|
+
var code = opts.code || (ns.prefix + "/bad-general-names");
|
|
285
|
+
var gn = generalName(ns, { code: code });
|
|
286
|
+
function build(m) { return { names: m.items.map(function (it) { return it.value; }), bytes: m.node.bytes }; }
|
|
287
|
+
if (opts.implicitTag != null) {
|
|
288
|
+
return schema.implicitSeqOf(opts.implicitTag, gn, { min: 1, code: code, what: opts.what || "GeneralNames", build: build });
|
|
289
|
+
}
|
|
290
|
+
return schema.seqOf(gn, { assert: "sequence", min: 1, code: code, what: opts.what || "GeneralNames", build: build });
|
|
291
|
+
}
|
|
292
|
+
|
|
267
293
|
// Extension ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE,
|
|
268
294
|
// extnValue OCTET STRING }. `critical` is a universal BOOLEAN present-by-count
|
|
269
295
|
// (not context-tagged), so the per-extension decode handles the 2-vs-3-child
|
|
@@ -419,6 +445,7 @@ module.exports = {
|
|
|
419
445
|
relativeDistinguishedName: relativeDistinguishedName,
|
|
420
446
|
name: name,
|
|
421
447
|
generalName: generalName,
|
|
448
|
+
generalNames: generalNames,
|
|
422
449
|
attribute: attribute,
|
|
423
450
|
extension: extension,
|
|
424
451
|
extensions: extensions,
|
package/lib/webcrypto.js
CHANGED
|
@@ -531,6 +531,7 @@ function _curveFromKey(ko) {
|
|
|
531
531
|
* point). Throws unless the key was created `extractable`.
|
|
532
532
|
*
|
|
533
533
|
* @example
|
|
534
|
+
* var keyPair = await pki.webcrypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);
|
|
534
535
|
* var spki = await pki.webcrypto.subtle.exportKey("spki", keyPair.publicKey);
|
|
535
536
|
*/
|
|
536
537
|
SubtleCrypto.prototype.exportKey = async function exportKey(format, key) {
|
|
@@ -566,7 +567,7 @@ function _rawPublic(key) {
|
|
|
566
567
|
*
|
|
567
568
|
* A ready `Crypto` instance (the shape of `globalThis.crypto`) exposing
|
|
568
569
|
* `getRandomValues`, `randomUUID`, and `subtle`. Construct additional
|
|
569
|
-
* instances with `new pki.
|
|
570
|
+
* instances with `new pki.webcrypto.Crypto()`.
|
|
570
571
|
*
|
|
571
572
|
* @example
|
|
572
573
|
* var iv = pki.webcrypto.getRandomValues(new Uint8Array(12));
|
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:d18de1ff-7ebd-406f-91cb-801da4286f4f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-06T08:32:53.685Z",
|
|
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.1.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.1.15",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
25
|
+
"version": "0.1.15",
|
|
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.1.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.1.15",
|
|
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.1.
|
|
57
|
+
"ref": "@blamejs/pki@0.1.15",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|