@blamejs/pki 0.1.16 → 0.1.17
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 -1
- package/lib/asn1-der.js +13 -0
- package/lib/framework-error.js +6 -0
- package/lib/oid.js +9 -1
- package/lib/schema-all.js +16 -1
- package/lib/schema-cms.js +12 -3
- package/lib/schema-crl.js +2 -2
- package/lib/schema-crmf.js +487 -0
- package/lib/schema-csr.js +3 -3
- package/lib/schema-engine.js +148 -14
- package/lib/schema-ocsp.js +3 -3
- package/lib/schema-pkcs8.js +4 -4
- package/lib/schema-pkix.js +58 -26
- 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.1.17 — 2026-07-07
|
|
8
|
+
|
|
9
|
+
An RFC 4211 certificate-request-message parser joins the pki.schema family.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.crmf.parse(input) — RFC 4211 CertReqMessages parsing. It decodes a DER Buffer or PEM into { messages: [ { certReq, popo, regInfo } ] }, where each certReq is { certReqId, certReqIdHex, certTemplate, controls, certReqBytes } and certTemplate carries the requestable fields version, issuer, validity, subject, publicKey, and extensions (each null when absent). The fields RFC 4211 §5 requires a request to omit — serialNumber and signingAlg (assigned by the CA) and issuerUID and subjectUID (deprecated) — are rejected, not returned, so a requester cannot dictate a CA-assigned value. issuer and subject Names are accepted in both the EXPLICIT and the IMPLICIT wire encodings; the OptionalValidity times are EXPLICIT UTCTime or GeneralizedTime; a supplied CertTemplate version must be 2; certReqId is an unbounded signed integer. popo is null, a raVerified marker, a decoded signature proof (with its poposkInput and signature surfaced raw), or a raw key-encipherment / key-agreement arm; for a signature proof, poposkInput's presence is checked against the template per §4.1. certReqBytes is the exact CertRequest byte range a proof-of-possession verifier hashes. Malformed input fails closed with a typed crmf/* or asn1/* code.
|
|
14
|
+
- pki.schema.crmf.pemDecode(text, label?) — extract the DER bytes from a PEM block (the first block unless a label is given).
|
|
15
|
+
- The OID registry gains the RFC 4211 registration-control (id-regCtrl) and registration-info (id-regInfo) identifiers on the id-pkip arc, so a parsed control or info entry resolves to its name (oldCertID, pkiArchiveOptions, utf8Pairs, and the rest).
|
|
16
|
+
- The error taxonomy gains CrmfError, carrying a stable crmf/* code.
|
|
17
|
+
- pki.schema.engine.encode(schema, value) — the constructor direction of the schema engine. A single declarative schema now drives both decode (walk, bytes to value) and encode (canonical DER, value to bytes): every leaf carries a read and a write, and EXPLICIT wrapping and IMPLICIT context-tag retagging are applied in one place, so an encoder can no longer emit a different tag than the decoder reads. A round-trip test proves walk(decode(encode(value))) recovers the value across universal, IMPLICIT, EXPLICIT, and SET-OF-ordered shapes, and the CRMF request format is proven to round-trip end to end.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- pki.schema.ocsp request and response parsing (parseRequest, parseResponse, pemDecode) is now stable.
|
|
22
|
+
- An experimental primitive is surfaced for a graduation review once it has been experimental for a fixed number of releases; the review is recorded as a graduation to stable or a dated keep-experimental decision, so the experimental-to-stable-to-deprecated transition is driven on a schedule (see LTS-CALENDAR.md).
|
|
23
|
+
|
|
7
24
|
## v0.1.16 — 2026-07-06
|
|
8
25
|
|
|
9
26
|
Certification path validation joins the toolkit — RFC 5280 section 6, as a pure re-entrant algorithm.
|
package/README.md
CHANGED
|
@@ -199,10 +199,11 @@ is callable today; nothing below is a stub.
|
|
|
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
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` |
|
|
202
|
+
| `pki.schema.crmf` | Parse DER / PEM RFC 4211 certificate request messages (CertReqMessages — the CMP / EST enrollment body) — the requested-certificate template (subject, public key, validity, extensions), proof-of-possession, and registration controls, with the raw `CertRequest` region a POP verifier hashes; names dual-accepted (IMPLICIT and EXPLICIT), fail-closed — `parse`, `pemDecode` |
|
|
202
203
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` plus the schema combinators |
|
|
203
204
|
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining, 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; `crlChecker` supplies CRL-based revocation. Pure and re-entrant, fail-closed — `validate`, `crlChecker` |
|
|
204
205
|
| `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
|
|
205
|
-
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `PathError`, each carrying a stable `code` in `domain/reason` form |
|
|
206
|
+
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `PathError`, each carrying a stable `code` in `domain/reason` form |
|
|
206
207
|
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
|
|
207
208
|
|
|
208
209
|
### CLI
|
package/lib/asn1-der.js
CHANGED
|
@@ -627,6 +627,18 @@ function encodeLength(n) {
|
|
|
627
627
|
return Buffer.concat([Buffer.from([0x80 | bytes.length]), Buffer.from(bytes)]);
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
+
// The DER of a universal SEQUENCE recovered from a decoded IMPLICIT [n] SEQUENCE
|
|
631
|
+
// `node`, whose context tag replaced the universal 0x30 on the wire (e.g. an RFC
|
|
632
|
+
// 4211 CertTemplate publicKey [6] SubjectPublicKeyInfo, or a POPOSigningKey
|
|
633
|
+
// poposkInput [0]): the field's value is imported / signed as a standalone SEQUENCE,
|
|
634
|
+
// not as the context-tagged wire form. A constructed node exposes its content as
|
|
635
|
+
// `children` (not `content`), so the content is those children's DER concatenated;
|
|
636
|
+
// the identifier becomes the SEQUENCE tag and the length is canonical DER.
|
|
637
|
+
function sequenceTlv(node) {
|
|
638
|
+
var content = node.content != null ? node.content : Buffer.concat((node.children || []).map(function (c) { return c.bytes; }));
|
|
639
|
+
return Buffer.concat([Buffer.from([TAGS.SEQUENCE | CONSTRUCTED_BIT]), encodeLength(content.length), content]);
|
|
640
|
+
}
|
|
641
|
+
|
|
630
642
|
function encodeIdentifier(classBits, constructed, tagNumber) {
|
|
631
643
|
var lead = classBits | (constructed ? CONSTRUCTED_BIT : 0);
|
|
632
644
|
if (tagNumber < 0x1f) return Buffer.from([lead | tagNumber]);
|
|
@@ -848,6 +860,7 @@ module.exports = {
|
|
|
848
860
|
encode: encodeTLV,
|
|
849
861
|
encodeTLV: encodeTLV,
|
|
850
862
|
encodeLength: encodeLength,
|
|
863
|
+
sequenceTlv: sequenceTlv,
|
|
851
864
|
encodeIdentifier: encodeIdentifier,
|
|
852
865
|
decodeOidContent: decodeOidContent,
|
|
853
866
|
encodeOidContent: encodeOidContent,
|
package/lib/framework-error.js
CHANGED
|
@@ -155,6 +155,11 @@ var TspError = defineClass("TspError", { withCause: true });
|
|
|
155
155
|
// underlying leaf fault as `.cause`.
|
|
156
156
|
var AttrCertError = defineClass("AttrCertError", { withCause: true });
|
|
157
157
|
|
|
158
|
+
// CrmfError — a byte sequence that is not a well-formed RFC 4211 CertReqMessages
|
|
159
|
+
// / CertReqMsg / CertRequest / CertTemplate. Carries the underlying leaf fault as
|
|
160
|
+
// `.cause`.
|
|
161
|
+
var CrmfError = defineClass("CrmfError", { withCause: true });
|
|
162
|
+
|
|
158
163
|
// PathError — a certification path that fails RFC 5280 §6 validation, or a
|
|
159
164
|
// malformed input handed to the validator (an extension value that does not
|
|
160
165
|
// decode, an empty path, an unsupported signature algorithm). Carries the
|
|
@@ -178,5 +183,6 @@ module.exports = {
|
|
|
178
183
|
OcspError: OcspError,
|
|
179
184
|
TspError: TspError,
|
|
180
185
|
AttrCertError: AttrCertError,
|
|
186
|
+
CrmfError: CrmfError,
|
|
181
187
|
PathError: PathError,
|
|
182
188
|
};
|
package/lib/oid.js
CHANGED
|
@@ -103,6 +103,14 @@ var FAMILIES = {
|
|
|
103
103
|
ocspBasic: 1, ocspNonce: 2, ocspCrl: 3, ocspResponse: 4, ocspNoCheck: 5,
|
|
104
104
|
ocspArchiveCutoff: 6, ocspServiceLocator: 7, ocspPrefSigAlgs: 8, ocspExtendedRevoke: 9 } },
|
|
105
105
|
|
|
106
|
+
// CRMF (RFC 4211) registration controls (§6) and registration info (§7) on the
|
|
107
|
+
// id-pkip arc (id-pkix 5). id-regCtrl (id-pkip 1) names the control types a
|
|
108
|
+
// CertRequest carries; id-regInfo (id-pkip 2) names the registration-info types.
|
|
109
|
+
// The parser surfaces each control/info value RAW keyed by these names.
|
|
110
|
+
regCtrl: { base: [1, 3, 6, 1, 5, 5, 7, 5, 1], of: {
|
|
111
|
+
regToken: 1, authenticator: 2, pkiPublicationInfo: 3, pkiArchiveOptions: 4, oldCertID: 5, protocolEncrKey: 6 } },
|
|
112
|
+
regInfo: { base: [1, 3, 6, 1, 5, 5, 7, 5, 2], of: { utf8Pairs: 1, certReq: 2 } },
|
|
113
|
+
|
|
106
114
|
// PKIX extended key purposes (id-kp, RFC 5280 §4.2.1.12). timeStamping is required
|
|
107
115
|
// — critical and sole — on an RFC 3161 TSA signing certificate (§2.3).
|
|
108
116
|
pkixKp: { base: [1, 3, 6, 1, 5, 5, 7, 3], of: {
|
|
@@ -126,7 +134,7 @@ var FAMILIES = {
|
|
|
126
134
|
challengePassword: 7, extensionRequest: 14 } },
|
|
127
135
|
|
|
128
136
|
// S/MIME content types on the PKCS#9 smime arc (RFC 5652, RFC 3161): id-ct.
|
|
129
|
-
smimeCt: { base: [1, 2, 840, 113549, 1, 9, 16, 1], of: { authData: 2, tSTInfo: 4 } },
|
|
137
|
+
smimeCt: { base: [1, 2, 840, 113549, 1, 9, 16, 1], of: { authData: 2, tSTInfo: 4, encKeyWithID: 21 } },
|
|
130
138
|
|
|
131
139
|
// S/MIME authenticated attributes (id-aa, RFC 2634 / RFC 5035 / RFC 5816). The ESS
|
|
132
140
|
// signing-certificate attributes bind a CMS / TSP SignerInfo to its signing cert;
|
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 crmf = require("./schema-crmf");
|
|
42
43
|
var attrcert = require("./schema-attrcert");
|
|
43
44
|
var frameworkError = require("./framework-error");
|
|
44
45
|
|
|
@@ -83,6 +84,19 @@ var FORMATS = [
|
|
|
83
84
|
detect: tsp.matches,
|
|
84
85
|
parse: function (input) { return tsp.parse(input); },
|
|
85
86
|
},
|
|
87
|
+
{
|
|
88
|
+
// CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg (RFC 4211 §3) — a
|
|
89
|
+
// SEQUENCE whose first CertReqMsg's CertRequest leads with a universal INTEGER
|
|
90
|
+
// (certReqId) then a universal SEQUENCE (certTemplate). Leads with a SEQUENCE
|
|
91
|
+
// like the signed-envelope trio and the ocsp-request, but the INTEGER-then-
|
|
92
|
+
// SEQUENCE pair at that depth is disjoint from both. Registered AHEAD of
|
|
93
|
+
// ocsp-request: a single-message CertReqMessages is the only remaining overlap
|
|
94
|
+
// with the ocsp-request detector, and crmf's deeper probe is the more specific.
|
|
95
|
+
name: "crmf",
|
|
96
|
+
module: crmf,
|
|
97
|
+
detect: crmf.matches,
|
|
98
|
+
parse: function (input) { return crmf.parse(input); },
|
|
99
|
+
},
|
|
86
100
|
{
|
|
87
101
|
// OCSPRequest ::= SEQUENCE { tbsRequest SEQUENCE, optionalSignature [0] EXPLICIT
|
|
88
102
|
// OPTIONAL } — a SEQUENCE of 1-2 whose first child is the tbsRequest SEQUENCE.
|
|
@@ -179,7 +193,7 @@ var FORMATS = [
|
|
|
179
193
|
* The names of every registered format, in detection order.
|
|
180
194
|
*
|
|
181
195
|
* @example
|
|
182
|
-
* pki.schema.all(); // → ["cms", "tsp", "ocsp-request", "ocsp-response", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
|
|
196
|
+
* pki.schema.all(); // → ["cms", "tsp", "crmf", "ocsp-request", "ocsp-response", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
|
|
183
197
|
*/
|
|
184
198
|
function all() { return FORMATS.map(function (f) { return f.name; }); }
|
|
185
199
|
|
|
@@ -224,6 +238,7 @@ module.exports = {
|
|
|
224
238
|
cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
|
|
225
239
|
ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode },
|
|
226
240
|
tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode },
|
|
241
|
+
crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode },
|
|
227
242
|
attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode },
|
|
228
243
|
all: all,
|
|
229
244
|
parse: parse,
|
package/lib/schema-cms.js
CHANGED
|
@@ -618,7 +618,7 @@ var CONTENT_INFO = schema.seq([
|
|
|
618
618
|
* @primitive pki.schema.cms.parse
|
|
619
619
|
* @signature pki.schema.cms.parse(input) -> signedData
|
|
620
620
|
* @since 0.1.10
|
|
621
|
-
* @status
|
|
621
|
+
* @status stable
|
|
622
622
|
* @spec RFC 5652
|
|
623
623
|
* @related pki.schema.parse, pki.schema.x509.parse
|
|
624
624
|
*
|
|
@@ -642,7 +642,7 @@ var parse = pkix.makeParser({ pemLabel: "CMS", PemError: PemError, ErrorClass: C
|
|
|
642
642
|
* @primitive pki.schema.cms.pemDecode
|
|
643
643
|
* @signature pki.schema.cms.pemDecode(text, label?) -> Buffer
|
|
644
644
|
* @since 0.1.10
|
|
645
|
-
* @status
|
|
645
|
+
* @status stable
|
|
646
646
|
* @spec RFC 7468, RFC 5652
|
|
647
647
|
* @related pki.schema.cms.parse
|
|
648
648
|
*
|
|
@@ -658,7 +658,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CMS", Pe
|
|
|
658
658
|
* @primitive pki.schema.cms.pemEncode
|
|
659
659
|
* @signature pki.schema.cms.pemEncode(der, label?) -> string
|
|
660
660
|
* @since 0.1.10
|
|
661
|
-
* @status
|
|
661
|
+
* @status stable
|
|
662
662
|
* @spec RFC 7468
|
|
663
663
|
* @related pki.schema.cms.pemDecode
|
|
664
664
|
*
|
|
@@ -684,9 +684,18 @@ function matches(root) {
|
|
|
684
684
|
return true;
|
|
685
685
|
}
|
|
686
686
|
|
|
687
|
+
// Validate a bare EnvelopedData (RFC 5652 §6.1) — a universal SEQUENCE node —
|
|
688
|
+
// returning its structured value; throws a typed cms/* error on a malformed
|
|
689
|
+
// structure. Exposed for a composer that carries a bare EnvelopedData OUTSIDE a
|
|
690
|
+
// ContentInfo (an RFC 4211 CRMF encryptedKey [4] POPOPrivKey arm), which the
|
|
691
|
+
// content-type dispatch in `parse` cannot reach. The node MUST already be a
|
|
692
|
+
// universal SEQUENCE (an IMPLICIT [n] EnvelopedData is retagged by the caller).
|
|
693
|
+
function walkEnvelopedData(node) { return schema.walk(ENVELOPED_DATA, node, NS).result; }
|
|
694
|
+
|
|
687
695
|
module.exports = {
|
|
688
696
|
parse: parse,
|
|
689
697
|
pemDecode: pemDecode,
|
|
690
698
|
pemEncode: pemEncode,
|
|
691
699
|
matches: matches,
|
|
700
|
+
walkEnvelopedData: walkEnvelopedData,
|
|
692
701
|
};
|
package/lib/schema-crl.js
CHANGED
|
@@ -190,7 +190,7 @@ var CERTIFICATE_LIST = pkix.signedEnvelope(NS, TBS_CERTLIST, {
|
|
|
190
190
|
* @primitive pki.schema.crl.parse
|
|
191
191
|
* @signature pki.schema.crl.parse(input) -> crl
|
|
192
192
|
* @since 0.1.7
|
|
193
|
-
* @status
|
|
193
|
+
* @status stable
|
|
194
194
|
* @spec RFC 5280
|
|
195
195
|
* @related pki.schema.x509.parse, pki.schema.parse
|
|
196
196
|
*
|
|
@@ -210,7 +210,7 @@ var parse = pkix.makeParser({ pemLabel: "X509 CRL", PemError: PemError, ErrorCla
|
|
|
210
210
|
* @primitive pki.schema.crl.pemDecode
|
|
211
211
|
* @signature pki.schema.crl.pemDecode(text, label?) -> Buffer
|
|
212
212
|
* @since 0.1.7
|
|
213
|
-
* @status
|
|
213
|
+
* @status stable
|
|
214
214
|
* @spec RFC 7468, RFC 5280
|
|
215
215
|
* @related pki.schema.crl.parse
|
|
216
216
|
*
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.schema.crmf
|
|
6
|
+
* @nav Schema
|
|
7
|
+
* @title CRMF
|
|
8
|
+
* @order 180
|
|
9
|
+
* @slug crmf
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
12
|
+
* Certificate Request Message Format handling per RFC 4211. `parse` decodes a
|
|
13
|
+
* `CertReqMessages` — the request body CMP and EST enrollment carry — into an
|
|
14
|
+
* array of messages, each with its `CertRequest` (certReqId, a `CertTemplate`
|
|
15
|
+
* of the requested certificate fields, and any registration controls), an
|
|
16
|
+
* optional proof-of-possession, and optional registration info.
|
|
17
|
+
*
|
|
18
|
+
* RFC 4211 Appendix B is an IMPLICIT TAGS module, so the whole `CertTemplate`
|
|
19
|
+
* body is one ascending run of IMPLICIT context tags `[0]`..`[9]`, every field
|
|
20
|
+
* OPTIONAL. Two fields break IMPLICIT because their base type is a CHOICE and
|
|
21
|
+
* X.680 forces EXPLICIT: `issuer [3]` / `subject [5]` are `Name`, dual-accepted
|
|
22
|
+
* here (the standards-compliant EXPLICIT encoding and the dominant IMPLICIT one
|
|
23
|
+
* real tooling emits); and the `OptionalValidity` times are EXPLICIT. version,
|
|
24
|
+
* when supplied, MUST be 2 (RFC 4211 §5); certReqId is an unbounded signed
|
|
25
|
+
* INTEGER (the RFC 9483 `-1` sentinel is legal). The `CertRequest` byte range
|
|
26
|
+
* the proof-of-possession signature covers, and each `poposkInput`, are surfaced
|
|
27
|
+
* RAW for a downstream verifier; registration controls / info values and the
|
|
28
|
+
* keyEncipherment / keyAgreement POP arms are surfaced RAW rather than recursed.
|
|
29
|
+
* DER-only, fail-closed.
|
|
30
|
+
*
|
|
31
|
+
* @card
|
|
32
|
+
* Parse DER / PEM RFC 4211 CertReqMessages into requested-certificate templates,
|
|
33
|
+
* proof-of-possession, and registration controls — dual-accepted names, raw
|
|
34
|
+
* verifier inputs, fail-closed.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
var asn1 = require("./asn1-der");
|
|
38
|
+
var schema = require("./schema-engine");
|
|
39
|
+
var pkix = require("./schema-pkix");
|
|
40
|
+
var cms = require("./schema-cms");
|
|
41
|
+
var oid = require("./oid");
|
|
42
|
+
var frameworkError = require("./framework-error");
|
|
43
|
+
|
|
44
|
+
var CrmfError = frameworkError.CrmfError;
|
|
45
|
+
var PemError = frameworkError.PemError;
|
|
46
|
+
|
|
47
|
+
var NS = pkix.makeNS("crmf", CrmfError, oid);
|
|
48
|
+
|
|
49
|
+
var TAGS = asn1.TAGS;
|
|
50
|
+
|
|
51
|
+
// ---- dual-accept Name (issuer [3] / subject [5]) ---------------------
|
|
52
|
+
// RFC 4211 Appendix B is IMPLICIT TAGS, but Name is a CHOICE so X.680 §31.2.7
|
|
53
|
+
// forces EXPLICIT tagging — the standards-compliant wire is a context [tag]
|
|
54
|
+
// EXPLICIT wrapper around a universal RDNSequence. The dominant tooling
|
|
55
|
+
// (pyasn1-modules, BouncyCastle) instead encodes IMPLICIT (the [tag] REPLACES the
|
|
56
|
+
// RDNSequence 0x30, so the children ARE the RDN SETs). Both occur in real CMP /
|
|
57
|
+
// EST, so accept either, disambiguated by the first inner element's tag: a
|
|
58
|
+
// universal SET leads the IMPLICIT form; a single universal SEQUENCE is the
|
|
59
|
+
// EXPLICIT wrapper. Anything else fails closed.
|
|
60
|
+
function crmfName(tag) {
|
|
61
|
+
var NAME = pkix.name(NS); // EXPLICIT arm walks a universal RDNSequence
|
|
62
|
+
var INAME = pkix.name(NS, { implicitTag: tag }); // IMPLICIT arm: [tag] children ARE the RDN SETs
|
|
63
|
+
return schema.decode(function (n, ctx) {
|
|
64
|
+
if (n.tagClass !== "context" || n.tagNumber !== tag || !n.children) {
|
|
65
|
+
throw ctx.E("crmf/bad-name", "issuer/subject [" + tag + "] must be a Name (RFC 4211 §5)");
|
|
66
|
+
}
|
|
67
|
+
if (n.children.length === 0) return schema.walk(INAME, n, ctx).result; // IMPLICIT empty RDNSequence
|
|
68
|
+
var c0 = n.children[0];
|
|
69
|
+
if (c0.tagClass === "universal" && c0.tagNumber === TAGS.SET) {
|
|
70
|
+
return schema.walk(INAME, n, ctx).result; // IMPLICIT: children ARE RDN SETs
|
|
71
|
+
}
|
|
72
|
+
if (n.children.length === 1 && c0.tagClass === "universal" && c0.tagNumber === TAGS.SEQUENCE) {
|
|
73
|
+
return schema.walk(NAME, c0, ctx).result; // EXPLICIT: [tag] wraps RDNSequence
|
|
74
|
+
}
|
|
75
|
+
throw ctx.E("crmf/bad-name", "issuer/subject [" + tag + "] Name must be an IMPLICIT RDNSequence (SET-led) or an EXPLICIT-wrapped RDNSequence");
|
|
76
|
+
}, function (value) {
|
|
77
|
+
// encode the IMPLICIT form (the dominant wire encoding): the [tag] whose children
|
|
78
|
+
// ARE the RDN SETs, built from the decoded `rdns` via the shared Name schema.
|
|
79
|
+
return schema.encode(INAME, value.rdns, NS);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- OptionalValidity (§5) -------------------------------------------
|
|
84
|
+
// validity [4] IMPLICIT SEQUENCE { notBefore [0] Time OPTIONAL, notAfter [1] Time
|
|
85
|
+
// OPTIONAL }. Time is a CHOICE of UTCTime / GeneralizedTime, so [0]/[1] are
|
|
86
|
+
// EXPLICIT (an IMPLICIT tag would erase the UTCTime-vs-GeneralizedTime
|
|
87
|
+
// discriminator). RFC 4211 §5 requires at least one of the two to be present.
|
|
88
|
+
var OPTIONAL_VALIDITY = schema.seq([
|
|
89
|
+
schema.trailing([
|
|
90
|
+
{ tag: 0, name: "notBefore", schema: schema.time(NS), explicit: true, emptyCode: "crmf/bad-validity" },
|
|
91
|
+
{ tag: 1, name: "notAfter", schema: schema.time(NS), explicit: true, emptyCode: "crmf/bad-validity" },
|
|
92
|
+
], { minTag: 0, maxTag: 1, unexpectedCode: "crmf/bad-validity", orderCode: "crmf/bad-validity" }),
|
|
93
|
+
], {
|
|
94
|
+
assert: "implicit", implicitTag: 4, code: "crmf/bad-validity", what: "OptionalValidity",
|
|
95
|
+
build: function (m, ctx) {
|
|
96
|
+
var f = m.fields;
|
|
97
|
+
if (!f.notBefore.present && !f.notAfter.present) {
|
|
98
|
+
throw ctx.E("crmf/bad-validity", "OptionalValidity must contain notBefore or notAfter (RFC 4211 §5)");
|
|
99
|
+
}
|
|
100
|
+
return { notBefore: f.notBefore.present ? f.notBefore.value : null, notAfter: f.notAfter.present ? f.notAfter.value : null };
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ---- ProofOfPossession (§4) ------------------------------------------
|
|
105
|
+
// raVerified [0] NULL — the RA has already verified POP out of band.
|
|
106
|
+
var POPO_RAVERIFIED = schema.decode(function (n, ctx) {
|
|
107
|
+
try { asn1.read.nullImplicit(n, 0); }
|
|
108
|
+
catch (e) { throw ctx.E("crmf/bad-popo", "raVerified [0] must be an IMPLICIT NULL", e); }
|
|
109
|
+
return { type: "raVerified" };
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// When poposkInput is present the POP signature is over the DER of the
|
|
113
|
+
// POPOSigningKeyInput VALUE (RFC 4211 §4.1), which is a SEQUENCE — but on the wire
|
|
114
|
+
// poposkInput is an IMPLICIT [0] field, so its bytes lead with the context tag
|
|
115
|
+
// (0xA0) rather than the SEQUENCE tag a verifier hashes. `asn1.sequenceTlv` recovers
|
|
116
|
+
// the SEQUENCE-tagged signed region from the field's content; `signedBytes` is that
|
|
117
|
+
// region and `bytes` keeps the raw wire [0] TLV, so a verifier can use either
|
|
118
|
+
// encoding its peer produced.
|
|
119
|
+
|
|
120
|
+
// POPOSigningKeyInput ::= SEQUENCE { authInfo CHOICE { sender [0] GeneralName,
|
|
121
|
+
// publicKeyMAC PKMACValue }, publicKey SubjectPublicKeyInfo } (§4.1). Structurally
|
|
122
|
+
// validated (not deferred raw), so an empty or malformed poposkInput fails closed
|
|
123
|
+
// rather than surfacing a signed region that was never a well-formed POP input.
|
|
124
|
+
// PKMACValue ::= SEQUENCE { algId AlgorithmIdentifier, value BIT STRING }.
|
|
125
|
+
var PKMAC_VALUE = schema.seq([
|
|
126
|
+
schema.field("algId", pkix.algorithmIdentifier(NS)),
|
|
127
|
+
schema.field("value", schema.bitString()),
|
|
128
|
+
], { assert: "sequence", arity: { exact: 2 }, code: "crmf/bad-popo", what: "PKMACValue" });
|
|
129
|
+
|
|
130
|
+
// authInfo CHOICE: sender [0] GeneralName is EXPLICIT (GeneralName is itself a
|
|
131
|
+
// CHOICE); publicKeyMAC is a bare PKMACValue SEQUENCE — disjoint by tag class.
|
|
132
|
+
var POPOSK_AUTH_INFO = schema.choice([
|
|
133
|
+
{ when: { tagClass: "context", tagNumber: 0 }, schema: schema.explicit(0, pkix.generalName(NS, { code: "crmf/bad-popo" }), { code: "crmf/bad-popo" }) },
|
|
134
|
+
{ when: { tagClass: "universal", tagNumber: TAGS.SEQUENCE }, schema: PKMAC_VALUE },
|
|
135
|
+
], { code: "crmf/bad-popo" });
|
|
136
|
+
|
|
137
|
+
var POPOSK_INPUT = schema.seq([
|
|
138
|
+
schema.field("authInfo", POPOSK_AUTH_INFO),
|
|
139
|
+
schema.field("publicKey", pkix.spki(NS)),
|
|
140
|
+
], { assert: "implicit", implicitTag: 0, arity: { exact: 2 }, code: "crmf/bad-popo", what: "POPOSigningKeyInput" });
|
|
141
|
+
|
|
142
|
+
// POPOSigningKey ::= SEQUENCE { poposkInput [0] POPOSigningKeyInput OPTIONAL,
|
|
143
|
+
// algorithmIdentifier AlgorithmIdentifier, signature BIT STRING } — IMPLICIT [1].
|
|
144
|
+
var POPO_SIGNING_KEY = schema.seq([
|
|
145
|
+
schema.optional("poposkInput", schema.decode(function (n, ctx) {
|
|
146
|
+
// Validate the full POPOSigningKeyInput structure fail-closed, then surface the
|
|
147
|
+
// raw [0] node plus its decoded publicKey (for the §4.1 template-match check).
|
|
148
|
+
var m = schema.walk(POPOSK_INPUT, n, ctx);
|
|
149
|
+
return { node: n, publicKey: m.fields.publicKey.value.result.bytes };
|
|
150
|
+
}), { tag: 0 }),
|
|
151
|
+
schema.field("algorithmIdentifier", pkix.algorithmIdentifier(NS)),
|
|
152
|
+
schema.field("signature", schema.bitString()),
|
|
153
|
+
], {
|
|
154
|
+
assert: "implicit", implicitTag: 1, code: "crmf/bad-popo", what: "POPOSigningKey",
|
|
155
|
+
build: function (m) {
|
|
156
|
+
var pin = m.fields.poposkInput;
|
|
157
|
+
return {
|
|
158
|
+
type: "signature",
|
|
159
|
+
poposkInput: pin.present ? { bytes: pin.node.bytes, signedBytes: asn1.sequenceTlv(pin.node), publicKey: pin.value.publicKey } : null,
|
|
160
|
+
algorithmIdentifier: m.fields.algorithmIdentifier.value.result,
|
|
161
|
+
signature: { unusedBits: m.fields.signature.value.unusedBits, bytes: m.fields.signature.value.bytes },
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// keyEncipherment [2] / keyAgreement [3] POPOPrivKey (§4). POPOPrivKey is a
|
|
167
|
+
// 5-arm CHOICE { thisMessage [0], subsequentMessage [1], dhMAC [2], agreeMAC [3],
|
|
168
|
+
// encryptedKey [4] }, so X.680 §31.2.7 forces the outer [2]/[3] tag to be EXPLICIT
|
|
169
|
+
// (a CHOICE has no single tag to replace). Validate that shell — a constructed
|
|
170
|
+
// wrapper around exactly one inner context [0]..[4] alternative — before surfacing
|
|
171
|
+
// the arm RAW, so a primitive / empty / mis-tagged node is rejected rather than
|
|
172
|
+
// reported to a verifier as a well-formed POP. The inner alternative's own value
|
|
173
|
+
// (incl. the encryptedKey [4] EnvelopedData) stays deferred to the verify layer.
|
|
174
|
+
// POPOPrivKey ::= CHOICE { thisMessage [0] BIT STRING (deprecated), subsequentMessage
|
|
175
|
+
// [1] SubsequentMessage, dhMAC [2] BIT STRING (deprecated), agreeMAC [3] PKMACValue,
|
|
176
|
+
// encryptedKey [4] EnvelopedData } (RFC 4211 §4.2). The deprecated arms are still
|
|
177
|
+
// parsed. The inner VALUE stays deferred raw (the EnvelopedData / MAC decode is a
|
|
178
|
+
// verify-layer concern), but the alternative's shape is validated fail-closed.
|
|
179
|
+
var POPOPRIVKEY_METHODS = { 0: "thisMessage", 1: "subsequentMessage", 2: "dhMAC", 3: "agreeMAC", 4: "encryptedKey" };
|
|
180
|
+
function popoPrivKey(type) {
|
|
181
|
+
return schema.decode(function (n, ctx) {
|
|
182
|
+
if (!n.children || n.children.length !== 1) {
|
|
183
|
+
throw ctx.E("crmf/bad-popo", type + " [" + n.tagNumber + "] POPOPrivKey must be an EXPLICIT wrapper around one alternative (RFC 4211 §4)");
|
|
184
|
+
}
|
|
185
|
+
var inner = n.children[0];
|
|
186
|
+
if (inner.tagClass !== "context" || inner.tagNumber < 0 || inner.tagNumber > 4) {
|
|
187
|
+
throw ctx.E("crmf/bad-popo", type + " POPOPrivKey alternative must be a context [0]..[4] CHOICE element (RFC 4211 §4)");
|
|
188
|
+
}
|
|
189
|
+
// RFC 4211 §4.2/§4.3 — the MAC alternatives dhMAC [2] / agreeMAC [3] are the
|
|
190
|
+
// key-agreement "fourth method"; key-encipherment POP uses only the
|
|
191
|
+
// thisMessage / subsequentMessage / encryptedKey methods, so a MAC arm under
|
|
192
|
+
// keyEncipherment is non-conforming.
|
|
193
|
+
if (type === "keyEncipherment" && (inner.tagNumber === 2 || inner.tagNumber === 3)) {
|
|
194
|
+
throw ctx.E("crmf/bad-popo", "keyEncipherment POP cannot use the MAC alternative " + POPOPRIVKEY_METHODS[inner.tagNumber] + " (RFC 4211 §4.2)");
|
|
195
|
+
}
|
|
196
|
+
// FORM: thisMessage [0] / subsequentMessage [1] / dhMAC [2] are primitive (BIT
|
|
197
|
+
// STRING / INTEGER / BIT STRING); agreeMAC [3] PKMACValue and encryptedKey [4]
|
|
198
|
+
// EnvelopedData are constructed (SEQUENCE). A wrong-form node is malformed.
|
|
199
|
+
var mustBeConstructed = inner.tagNumber === 3 || inner.tagNumber === 4;
|
|
200
|
+
if (mustBeConstructed !== !!inner.children) {
|
|
201
|
+
throw ctx.E("crmf/bad-popo", type + " POPOPrivKey [" + inner.tagNumber + "] has the wrong primitive/constructed form (RFC 4211 §4)");
|
|
202
|
+
}
|
|
203
|
+
// Each alternative's PAYLOAD is decoded fail-closed, not just its form:
|
|
204
|
+
// thisMessage [0] / dhMAC [2] are BIT STRINGs; subsequentMessage [1] an INTEGER
|
|
205
|
+
// in {0,1}; agreeMAC [3] a PKMACValue SEQUENCE; encryptedKey [4] an EnvelopedData.
|
|
206
|
+
if (inner.tagNumber === 0 || inner.tagNumber === 2) {
|
|
207
|
+
try { asn1.read.bitStringImplicit(inner, inner.tagNumber); }
|
|
208
|
+
catch (e) { throw ctx.E("crmf/bad-popo", POPOPRIVKEY_METHODS[inner.tagNumber] + " [" + inner.tagNumber + "] must be a BIT STRING (RFC 4211 §4.2)", e); }
|
|
209
|
+
}
|
|
210
|
+
if (inner.tagNumber === 1) {
|
|
211
|
+
var v;
|
|
212
|
+
try { v = asn1.read.integerImplicit(inner, 1); }
|
|
213
|
+
catch (e) { throw ctx.E("crmf/bad-popo", "subsequentMessage must be an INTEGER", e); }
|
|
214
|
+
if (v !== 0n && v !== 1n) {
|
|
215
|
+
throw ctx.E("crmf/bad-popo", "SubsequentMessage must be encrCert(0) or challengeResp(1) (RFC 4211 §4.2)");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (inner.tagNumber === 3) {
|
|
219
|
+
try { schema.walk(PKMAC_VALUE, asn1.decode(asn1.sequenceTlv(inner)), NS); }
|
|
220
|
+
catch (e) { throw ctx.E("crmf/bad-popo", "agreeMAC [3] must be a PKMACValue SEQUENCE { algId, BIT STRING } (RFC 4211 §4.2)", e); }
|
|
221
|
+
}
|
|
222
|
+
// encryptedKey [4] EnvelopedData — validate the CMS structure fail-closed by
|
|
223
|
+
// composing the shared EnvelopedData decoder (retag the IMPLICIT [4] to the
|
|
224
|
+
// universal SEQUENCE it decodes as). The EncKeyWithID identity check lives inside
|
|
225
|
+
// the ENCRYPTED content, so it stays a verify-layer concern.
|
|
226
|
+
if (inner.tagNumber === 4) {
|
|
227
|
+
var env;
|
|
228
|
+
try { env = cms.walkEnvelopedData(asn1.decode(asn1.sequenceTlv(inner))); }
|
|
229
|
+
catch (e) { throw ctx.E("crmf/bad-popo", "encryptedKey [4] must be a well-formed EnvelopedData (RFC 4211 §4.2, RFC 5652 §6.1)", e); }
|
|
230
|
+
// RFC 4211 §4.2 — the enveloped content type MUST be id-ct-encKeyWithID (the
|
|
231
|
+
// ContentType OID is in the clear even though the content itself is encrypted).
|
|
232
|
+
if (env.encryptedContentInfo.contentType !== oid.byName("encKeyWithID")) {
|
|
233
|
+
throw ctx.E("crmf/bad-popo", "encryptedKey [4] EnvelopedData content type MUST be id-ct-encKeyWithID (RFC 4211 §4.2)");
|
|
234
|
+
}
|
|
235
|
+
// The encrypted key material MUST be present — CMS allows a detached
|
|
236
|
+
// EnvelopedData (encryptedContent OPTIONAL), but a POP with no key to verify
|
|
237
|
+
// or archive is meaningless.
|
|
238
|
+
if (env.encryptedContentInfo.encryptedContent == null) {
|
|
239
|
+
throw ctx.E("crmf/bad-popo", "encryptedKey [4] EnvelopedData MUST carry the encrypted key in encryptedContent (RFC 4211 §4.2)");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { type: type, method: POPOPRIVKEY_METHODS[inner.tagNumber], bytes: n.bytes };
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ProofOfPossession ::= CHOICE { raVerified [0] NULL, signature [1] POPOSigningKey,
|
|
247
|
+
// keyEncipherment [2] POPOPrivKey, keyAgreement [3] POPOPrivKey }. Each arm yields
|
|
248
|
+
// the final object so the CertReqMsg build reads one uniform popo shape.
|
|
249
|
+
var PROOF_OF_POSSESSION = schema.choice([
|
|
250
|
+
{ when: { tagClass: "context", tagNumber: 0 }, schema: POPO_RAVERIFIED },
|
|
251
|
+
{ when: { tagClass: "context", tagNumber: 1 }, schema: schema.decode(function (n, ctx) { return schema.walk(POPO_SIGNING_KEY, n, ctx).result; }) },
|
|
252
|
+
{ when: { tagClass: "context", tagNumber: 2 }, schema: popoPrivKey("keyEncipherment") },
|
|
253
|
+
{ when: { tagClass: "context", tagNumber: 3 }, schema: popoPrivKey("keyAgreement") },
|
|
254
|
+
], { code: "crmf/bad-popo" });
|
|
255
|
+
|
|
256
|
+
// ---- Controls / regInfo (§5, §6, §7) ---------------------------------
|
|
257
|
+
// AttributeTypeAndValue ::= SEQUENCE { type OBJECT IDENTIFIER, value ANY DEFINED BY
|
|
258
|
+
// type }. The list is decoded; each value stays RAW (the per-OID value semantics
|
|
259
|
+
// are deferred). Shared shape for both controls and regInfo.
|
|
260
|
+
var CONTROL = schema.seq([
|
|
261
|
+
schema.field("type", schema.oidLeaf()),
|
|
262
|
+
schema.field("value", schema.any()),
|
|
263
|
+
], {
|
|
264
|
+
assert: "sequence", arity: { exact: 2 }, code: "crmf/bad-control", what: "AttributeTypeAndValue",
|
|
265
|
+
build: function (m, ctx) {
|
|
266
|
+
var t = m.fields.type.value;
|
|
267
|
+
return { type: t, name: ctx.oid.name(t) || null, value: m.fields.value.node.bytes };
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
function mapControls(m) { return m.items.map(function (it) { return it.value.result; }); }
|
|
271
|
+
// Controls ::= SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue (§5).
|
|
272
|
+
var CONTROLS = schema.seqOf(CONTROL, { assert: "sequence", min: 1, code: "crmf/bad-controls", what: "Controls", build: mapControls });
|
|
273
|
+
// regInfo ::= SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue (§7).
|
|
274
|
+
var REG_INFO = schema.seqOf(CONTROL, { assert: "sequence", min: 1, code: "crmf/bad-reg-info", what: "regInfo", build: mapControls });
|
|
275
|
+
|
|
276
|
+
// ---- CertTemplate (§5) — the IMPLICIT-TAGS core ----------------------
|
|
277
|
+
// CertTemplate ::= SEQUENCE { version [0], serialNumber [1], signingAlg [2],
|
|
278
|
+
// issuer [3] Name, validity [4] OptionalValidity, subject [5] Name, publicKey [6]
|
|
279
|
+
// SubjectPublicKeyInfo, issuerUID [7], subjectUID [8], extensions [9] } — every
|
|
280
|
+
// field IMPLICIT and OPTIONAL, so the whole body is one ascending [0..9] trailing
|
|
281
|
+
// run. issuer/subject Name and the [4] validity times are the CHOICE exceptions.
|
|
282
|
+
var CERT_TEMPLATE = schema.seq([
|
|
283
|
+
schema.trailing([
|
|
284
|
+
{ tag: 0, name: "version", schema: schema.implicitInteger(0) },
|
|
285
|
+
{ tag: 1, name: "serialNumber", schema: schema.implicitInteger(1) },
|
|
286
|
+
{ tag: 2, name: "signingAlg", schema: pkix.algorithmIdentifier(NS, { implicitTag: 2 }) },
|
|
287
|
+
{ tag: 3, name: "issuer", schema: crmfName(3) },
|
|
288
|
+
{ tag: 4, name: "validity", schema: OPTIONAL_VALIDITY },
|
|
289
|
+
{ tag: 5, name: "subject", schema: crmfName(5) },
|
|
290
|
+
{ tag: 6, name: "publicKey", schema: pkix.spki(NS, { implicitTag: 6 }) },
|
|
291
|
+
{ tag: 7, name: "issuerUID", schema: schema.implicitBitString(7) },
|
|
292
|
+
{ tag: 8, name: "subjectUID", schema: schema.implicitBitString(8) },
|
|
293
|
+
{ tag: 9, name: "extensions", schema: pkix.extensions(NS, { implicitTag: 9 }) },
|
|
294
|
+
], { minTag: 0, maxTag: 9, unexpectedCode: "crmf/bad-cert-template", orderCode: "crmf/bad-cert-template" }),
|
|
295
|
+
], {
|
|
296
|
+
assert: "sequence", code: "crmf/bad-cert-template", what: "CertTemplate",
|
|
297
|
+
build: function (m, ctx) {
|
|
298
|
+
var f = m.fields;
|
|
299
|
+
// RFC 4211 §5 — serialNumber, signingAlg, issuerUID, and subjectUID MUST be
|
|
300
|
+
// omitted from a CertTemplate: serialNumber and signingAlg are assigned by the
|
|
301
|
+
// CA, and the UID pair is deprecated. A requester must not dictate a
|
|
302
|
+
// CA-assigned value (a requester-chosen serialNumber is a real hazard), so a
|
|
303
|
+
// template that sets any of them is rejected fail-closed, not surfaced as
|
|
304
|
+
// acceptable. They stay in the field map (parsed, then rejected) so the
|
|
305
|
+
// diagnostic names the offending field rather than a bare tag number.
|
|
306
|
+
var caAssigned = ["serialNumber", "signingAlg", "issuerUID", "subjectUID"];
|
|
307
|
+
for (var i = 0; i < caAssigned.length; i++) {
|
|
308
|
+
if (f[caAssigned[i]].present) {
|
|
309
|
+
throw ctx.E("crmf/bad-cert-template", "CertTemplate " + caAssigned[i] + " MUST be omitted — it is CA-assigned or deprecated (RFC 4211 §5)");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// version MUST be 2 (v3) when supplied; SHOULD be omitted (RFC 4211 §5).
|
|
313
|
+
if (f.version.present && f.version.value !== 2n) {
|
|
314
|
+
throw ctx.E("crmf/bad-version", "CertTemplate version MUST be 2 (v3) if supplied (RFC 4211 §5)");
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
version: f.version.present ? f.version.value : null,
|
|
318
|
+
issuer: f.issuer.present ? f.issuer.value : null,
|
|
319
|
+
validity: f.validity.present ? f.validity.value.result : null,
|
|
320
|
+
subject: f.subject.present ? f.subject.value : null,
|
|
321
|
+
publicKey: f.publicKey.present ? f.publicKey.value.result : null,
|
|
322
|
+
extensions: f.extensions.present ? f.extensions.value.result : null,
|
|
323
|
+
};
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// ---- CertRequest / CertReqMsg / CertReqMessages (§3, §5) --------------
|
|
328
|
+
// CertRequest ::= SEQUENCE { certReqId INTEGER, certTemplate CertTemplate,
|
|
329
|
+
// controls Controls OPTIONAL }. certReqId is a SIGNED INTEGER with no value
|
|
330
|
+
// constraint — a negative value (the RFC 9483 -1 sentinel) is legal.
|
|
331
|
+
var CERT_REQUEST = schema.seq([
|
|
332
|
+
schema.field("certReqId", schema.integerLeaf()),
|
|
333
|
+
schema.field("certTemplate", CERT_TEMPLATE),
|
|
334
|
+
schema.optional("controls", CONTROLS, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
335
|
+
], {
|
|
336
|
+
assert: "sequence", code: "crmf/bad-cert-request", what: "CertRequest",
|
|
337
|
+
build: function (m) {
|
|
338
|
+
return {
|
|
339
|
+
certReqId: m.fields.certReqId.value,
|
|
340
|
+
certReqIdHex: m.fields.certReqId.node.content.toString("hex"),
|
|
341
|
+
certTemplate: m.fields.certTemplate.value.result,
|
|
342
|
+
controls: m.fields.controls.present ? m.fields.controls.value.result : null,
|
|
343
|
+
certReqBytes: m.node.bytes, // the exact CertRequest TLV the POP signature covers (§4.1)
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// CertReqMsg ::= SEQUENCE { certReq CertRequest, popo ProofOfPossession OPTIONAL,
|
|
349
|
+
// regInfo SEQUENCE OF AttributeTypeAndValue OPTIONAL }. popo is a CHOICE of context
|
|
350
|
+
// [0]..[3]; regInfo is a bare universal SEQUENCE, so the two OPTIONAL positions are
|
|
351
|
+
// disambiguated by tag class.
|
|
352
|
+
var CERT_REQ_MSG = schema.seq([
|
|
353
|
+
schema.field("certReq", CERT_REQUEST),
|
|
354
|
+
schema.optional("popo", PROOF_OF_POSSESSION, { tags: [0, 1, 2, 3] }),
|
|
355
|
+
schema.optional("regInfo", REG_INFO, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
356
|
+
], {
|
|
357
|
+
assert: "sequence", code: "crmf/bad-cert-req-msg", what: "CertReqMsg",
|
|
358
|
+
build: function (m, ctx) {
|
|
359
|
+
var certReq = m.fields.certReq.value.result;
|
|
360
|
+
var popo = m.fields.popo.present ? m.fields.popo.value : null;
|
|
361
|
+
// A POP whose signature/MAC covers the certReq requires the CertTemplate to
|
|
362
|
+
// contain both subject and publicKey — otherwise there is no complete request
|
|
363
|
+
// to sign / MAC over, and a verifier would be bound to the wrong identity/key.
|
|
364
|
+
var complete = certReq.certTemplate.subject !== null && certReq.certTemplate.publicKey !== null;
|
|
365
|
+
// RFC 4211 §4.1 — for a signature POP, poposkInput's presence is fixed by the
|
|
366
|
+
// CertTemplate: it MUST be omitted when the template carries BOTH subject and
|
|
367
|
+
// publicKey (the signature is then over the DER of the CertRequest), and MUST be
|
|
368
|
+
// present otherwise.
|
|
369
|
+
if (popo && popo.type === "signature") {
|
|
370
|
+
if (complete && popo.poposkInput !== null) {
|
|
371
|
+
throw ctx.E("crmf/bad-popo", "poposkInput MUST be omitted when the CertTemplate contains both subject and publicKey (RFC 4211 §4.1)");
|
|
372
|
+
}
|
|
373
|
+
if (!complete && popo.poposkInput === null) {
|
|
374
|
+
throw ctx.E("crmf/bad-popo", "poposkInput MUST be present when the CertTemplate lacks subject or publicKey (RFC 4211 §4.1)");
|
|
375
|
+
}
|
|
376
|
+
// RFC 4211 §4.1 — the POPOSigningKeyInput publicKey MUST be exactly the template
|
|
377
|
+
// publicKey (both surfaced as canonical SubjectPublicKeyInfo DER), so a request
|
|
378
|
+
// cannot sign one key while asking for a certificate over a different key.
|
|
379
|
+
if (popo.poposkInput !== null && certReq.certTemplate.publicKey !== null &&
|
|
380
|
+
!popo.poposkInput.publicKey.equals(certReq.certTemplate.publicKey.bytes)) {
|
|
381
|
+
throw ctx.E("crmf/bad-popo", "POPOSigningKeyInput publicKey MUST equal the CertTemplate publicKey (RFC 4211 §4.1)");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// RFC 4211 §4.2/§4.3 — a MAC-based POPOPrivKey (dhMAC / agreeMAC) proves
|
|
385
|
+
// possession by MACing the certReq, which MUST include both subject and publicKey.
|
|
386
|
+
if (popo && (popo.type === "keyEncipherment" || popo.type === "keyAgreement") &&
|
|
387
|
+
(popo.method === "dhMAC" || popo.method === "agreeMAC") && !complete) {
|
|
388
|
+
throw ctx.E("crmf/bad-popo", "a MAC-based key-agreement/encipherment POP (dhMAC/agreeMAC) requires the CertTemplate to contain both subject and publicKey (RFC 4211 §4.2/§4.3)");
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
certReq: certReq,
|
|
392
|
+
popo: popo,
|
|
393
|
+
regInfo: m.fields.regInfo.present ? m.fields.regInfo.value.result : null,
|
|
394
|
+
};
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg (§3) — an empty sequence
|
|
399
|
+
// is malformed.
|
|
400
|
+
var CERT_REQ_MESSAGES = schema.seqOf(CERT_REQ_MSG, {
|
|
401
|
+
assert: "sequence", min: 1, code: "crmf/bad-cert-req-messages", what: "CertReqMessages",
|
|
402
|
+
build: function (m) { return { messages: m.items.map(function (it) { return it.value.result; }) }; },
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// ---- parse -----------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* @primitive pki.schema.crmf.parse
|
|
409
|
+
* @signature pki.schema.crmf.parse(input) -> certReqMessages
|
|
410
|
+
* @since 0.1.17
|
|
411
|
+
* @status experimental
|
|
412
|
+
* @spec RFC 4211
|
|
413
|
+
* @related pki.schema.parse, pki.schema.csr.parse
|
|
414
|
+
*
|
|
415
|
+
* Parse a DER `Buffer` or a PEM string/Buffer into a structured `CertReqMessages`:
|
|
416
|
+
* `{ messages: [ { certReq, popo, regInfo } ] }`. Each `certReq` is
|
|
417
|
+
* `{ certReqId, certReqIdHex, certTemplate, controls, certReqBytes }`, and
|
|
418
|
+
* `certTemplate` carries the requestable certificate fields (`version`, `issuer`,
|
|
419
|
+
* `validity`, `subject`, `publicKey`, `extensions` — each `null` when absent). The
|
|
420
|
+
* CA-assigned / deprecated fields RFC 4211 §5 requires a request to omit
|
|
421
|
+
* (`serialNumber`, `signingAlg`, `issuerUID`, `subjectUID`) are rejected, not
|
|
422
|
+
* surfaced. `popo` is
|
|
423
|
+
* `null`, `{ type: "raVerified" }`, `{ type: "signature", poposkInput,
|
|
424
|
+
* algorithmIdentifier, signature }`, or `{ type: "keyEncipherment" |
|
|
425
|
+
* "keyAgreement", method, bytes }` (where `method` is the POPOPrivKey alternative —
|
|
426
|
+
* `thisMessage` / `subsequentMessage` / `dhMAC` / `agreeMAC` / `encryptedKey` — each
|
|
427
|
+
* structurally validated, the `encryptedKey` EnvelopedData included). When present, `poposkInput` is `{ bytes, signedBytes }`
|
|
428
|
+
* — `bytes` is the raw wire `[0]` TLV and `signedBytes` is the `POPOSigningKeyInput`
|
|
429
|
+
* re-tagged to the `SEQUENCE` DER the signature actually covers (RFC 4211 §4.1).
|
|
430
|
+
* `certReqBytes` is the exact `CertRequest` byte range a proof-of-possession
|
|
431
|
+
* verifier hashes when `poposkInput` is absent.
|
|
432
|
+
*
|
|
433
|
+
* Throws `CrmfError` when the bytes are not a well-formed `CertReqMessages`, and
|
|
434
|
+
* `Asn1Error` when the underlying DER is malformed.
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* var m = pki.schema.crmf.parse(der);
|
|
438
|
+
* m.messages[0].certReq.certTemplate.subject.dn; // "CN=req.example"
|
|
439
|
+
*/
|
|
440
|
+
var parse = pkix.makeParser({
|
|
441
|
+
pemLabel: null, PemError: PemError, ErrorClass: CrmfError, prefix: "crmf",
|
|
442
|
+
what: "certificate request message", topSchema: CERT_REQ_MESSAGES, ns: NS,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @primitive pki.schema.crmf.pemDecode
|
|
447
|
+
* @signature pki.schema.crmf.pemDecode(text, label?) -> Buffer
|
|
448
|
+
* @since 0.1.17
|
|
449
|
+
* @status experimental
|
|
450
|
+
* @spec RFC 7468, RFC 4211
|
|
451
|
+
* @related pki.schema.crmf.parse
|
|
452
|
+
*
|
|
453
|
+
* Extract the DER bytes from a PEM block (RFC 4211 registers no RFC 7468 label, so
|
|
454
|
+
* the first block is taken unless `label` is given — CRMF rides inside CMP / EST as
|
|
455
|
+
* DER in practice). Throws `PemError` on a missing envelope or a non-base64 body.
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* var der = pki.schema.crmf.pemDecode(pemText);
|
|
459
|
+
*/
|
|
460
|
+
function pemDecode(text, label) { return pkix.pemDecode(text, label || null, PemError); }
|
|
461
|
+
|
|
462
|
+
// matches(root): a CertReqMessages is a universal SEQUENCE whose first CertReqMsg
|
|
463
|
+
// is a SEQUENCE whose first CertRequest is a SEQUENCE leading with a universal
|
|
464
|
+
// INTEGER (certReqId) then a universal SEQUENCE (certTemplate). This 3-level probe
|
|
465
|
+
// is disjoint from every registered root PROVIDED crmf is checked before
|
|
466
|
+
// ocsp-request: an OCSPRequest's tbsRequest never leads with the INTEGER-then-
|
|
467
|
+
// SEQUENCE pair (its children[0] is a context [0] version or a requestList).
|
|
468
|
+
function matches(root) {
|
|
469
|
+
if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE || !root.children || root.children.length < 1) return false;
|
|
470
|
+
var msg = root.children[0];
|
|
471
|
+
if (!msg.children || msg.tagClass !== "universal" || msg.tagNumber !== TAGS.SEQUENCE || msg.children.length < 1) return false;
|
|
472
|
+
var certReq = msg.children[0];
|
|
473
|
+
if (!certReq.children || certReq.tagClass !== "universal" || certReq.tagNumber !== TAGS.SEQUENCE || certReq.children.length < 2) return false;
|
|
474
|
+
var id = certReq.children[0], tpl = certReq.children[1];
|
|
475
|
+
return id.tagClass === "universal" && id.tagNumber === TAGS.INTEGER &&
|
|
476
|
+
tpl.tagClass === "universal" && tpl.tagNumber === TAGS.SEQUENCE && !!tpl.children;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
module.exports = {
|
|
480
|
+
parse: parse,
|
|
481
|
+
pemDecode: pemDecode,
|
|
482
|
+
matches: matches,
|
|
483
|
+
// The top schema, exposed (not on the curated pki.schema.crmf surface) so a
|
|
484
|
+
// round-trip test can prove schema.encode(schema, v) -> decode -> parse recovers
|
|
485
|
+
// v — the invariant that structurally validates the IMPLICIT/EXPLICIT tag handling.
|
|
486
|
+
certReqMessagesSchema: CERT_REQ_MESSAGES,
|
|
487
|
+
};
|
package/lib/schema-csr.js
CHANGED
|
@@ -106,7 +106,7 @@ var CERTIFICATION_REQUEST = pkix.signedEnvelope(NS, CERTIFICATION_REQUEST_INFO,
|
|
|
106
106
|
* @primitive pki.schema.csr.parse
|
|
107
107
|
* @signature pki.schema.csr.parse(input) -> csr
|
|
108
108
|
* @since 0.1.8
|
|
109
|
-
* @status
|
|
109
|
+
* @status stable
|
|
110
110
|
* @spec RFC 2986
|
|
111
111
|
* @related pki.schema.x509.parse, pki.schema.parse
|
|
112
112
|
*
|
|
@@ -129,7 +129,7 @@ var parse = pkix.makeParser({ pemLabel: "CERTIFICATE REQUEST", PemError: PemErro
|
|
|
129
129
|
* @primitive pki.schema.csr.pemDecode
|
|
130
130
|
* @signature pki.schema.csr.pemDecode(text, label?) -> Buffer
|
|
131
131
|
* @since 0.1.8
|
|
132
|
-
* @status
|
|
132
|
+
* @status stable
|
|
133
133
|
* @spec RFC 7468, RFC 2986
|
|
134
134
|
* @related pki.schema.csr.parse
|
|
135
135
|
*
|
|
@@ -146,7 +146,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CERTIFIC
|
|
|
146
146
|
* @primitive pki.schema.csr.pemEncode
|
|
147
147
|
* @signature pki.schema.csr.pemEncode(der, label?) -> string
|
|
148
148
|
* @since 0.1.8
|
|
149
|
-
* @status
|
|
149
|
+
* @status stable
|
|
150
150
|
* @spec RFC 7468
|
|
151
151
|
* @related pki.schema.csr.pemDecode
|
|
152
152
|
*
|
package/lib/schema-engine.js
CHANGED
|
@@ -93,39 +93,49 @@ function _assertArity(schema, kids, ctx) {
|
|
|
93
93
|
// the OID / INTEGER / string reads). `decode(fn)` hands the whole node to fn,
|
|
94
94
|
// which owns its try/catch and code mapping. `any()` yields the node itself.
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
function
|
|
100
|
-
function
|
|
96
|
+
// Leaves carry BOTH directions: `read(node) -> value` and `write(value) -> DER TLV`,
|
|
97
|
+
// so a single leaf definition drives decode (walk) and encode (build). A [tag]
|
|
98
|
+
// IMPLICIT leaf's write emits the context-tagged TLV directly, mirroring its read.
|
|
99
|
+
function _implicitIntContent(v) { return asn1.decode(asn1.build.integer(v)).content; }
|
|
100
|
+
function oidLeaf() { return { kind: "leaf", read: asn1.read.oid, write: function (v) { return asn1.build.oid(v); } }; }
|
|
101
|
+
function integerLeaf() { return { kind: "leaf", read: asn1.read.integer, write: function (v) { return asn1.build.integer(v); } }; }
|
|
102
|
+
function boolean() { return { kind: "leaf", read: asn1.read.boolean, write: function (v) { return asn1.build.boolean(v); } }; }
|
|
103
|
+
function octetString() { return { kind: "leaf", read: asn1.read.octetString, write: function (v) { return asn1.build.octetString(v); } }; }
|
|
104
|
+
function bitString() { return { kind: "leaf", read: function (n) { var b = asn1.read.bitString(n); return { unusedBits: b.unusedBits, bytes: b.bytes }; }, write: function (v) { return asn1.build.bitString(v.bytes, v.unusedBits); } }; }
|
|
101
105
|
// A [tag] IMPLICIT BIT STRING leaf (context-class primitive) — the primitive-leaf
|
|
102
106
|
// counterpart to implicitSetOf, for e.g. the PKCS#8 publicKey [1] (RFC 5958 §2).
|
|
103
|
-
function implicitBitString(tag) { return { kind: "leaf", read: function (n) { var b = asn1.read.bitStringImplicit(n, tag); return { unusedBits: b.unusedBits, bytes: b.bytes }; } }; }
|
|
107
|
+
function implicitBitString(tag) { return { kind: "leaf", read: function (n) { var b = asn1.read.bitStringImplicit(n, tag); return { unusedBits: b.unusedBits, bytes: b.bytes }; }, write: function (v) { return asn1.build.contextPrimitive(tag, Buffer.concat([Buffer.from([v.unusedBits]), v.bytes])); } }; }
|
|
104
108
|
// A [tag] IMPLICIT OCTET STRING leaf (context-class primitive) — the sibling of
|
|
105
109
|
// implicitBitString, for e.g. the CMS SignerIdentifier subjectKeyIdentifier [0]
|
|
106
110
|
// (RFC 5652 §5.3). Yields the raw content bytes.
|
|
107
|
-
function implicitOctetString(tag) { return { kind: "leaf", read: function (n) { return asn1.read.octetStringImplicit(n, tag); } }; }
|
|
111
|
+
function implicitOctetString(tag) { return { kind: "leaf", read: function (n) { return asn1.read.octetStringImplicit(n, tag); }, write: function (v) { return asn1.build.contextPrimitive(tag, v); } }; }
|
|
108
112
|
// A [tag] IMPLICIT NULL leaf (context-class primitive, empty content) — the sibling
|
|
109
113
|
// of implicitBitString/implicitOctetString, for e.g. the OCSP CertStatus good [0] /
|
|
110
114
|
// unknown [2] arms (RFC 6960 §4.2.1). Yields null; rejects a constructed or
|
|
111
115
|
// non-empty [tag] node fail-closed.
|
|
112
|
-
function implicitNull(tag) { return { kind: "leaf", read: function (n) { return asn1.read.nullImplicit(n, tag); } }; }
|
|
116
|
+
function implicitNull(tag) { return { kind: "leaf", read: function (n) { return asn1.read.nullImplicit(n, tag); }, write: function () { return asn1.build.contextPrimitive(tag, Buffer.alloc(0)); } }; }
|
|
113
117
|
// A [tag] IMPLICIT INTEGER leaf (context-class primitive) — the integer sibling of
|
|
114
118
|
// implicitBitString, for the RFC 3161 Accuracy millis [0] / micros [1] fields
|
|
115
119
|
// (context-tagged primitive INTEGERs). Yields a BigInt; a constructed or wrong-tag
|
|
116
120
|
// node fails asn1/* at the codec.
|
|
117
|
-
function implicitInteger(tag) { return { kind: "leaf", read: function (n) { return asn1.read.integerImplicit(n, tag); } }; }
|
|
121
|
+
function implicitInteger(tag) { return { kind: "leaf", read: function (n) { return asn1.read.integerImplicit(n, tag); }, write: function (v) { return asn1.build.contextPrimitive(tag, _implicitIntContent(v)); } }; }
|
|
118
122
|
function any() { return { kind: "any" }; }
|
|
119
|
-
|
|
123
|
+
// decode(fn, write?): a custom decode leaf; `write` is its optional paired encoder
|
|
124
|
+
// (value -> DER TLV) so the same leaf drives both directions.
|
|
125
|
+
function decode(fn, write) { return { kind: "decode", fn: fn, write: write }; }
|
|
120
126
|
|
|
121
127
|
// time(ns): a UTCTime / GeneralizedTime value, asserting the tag before the
|
|
122
|
-
// codec reads it (mirrors _parseValidityTime's x509/bad-time guard).
|
|
128
|
+
// codec reads it (mirrors _parseValidityTime's x509/bad-time guard). Encodes as
|
|
129
|
+
// UTCTime for years 1950..2049 and GeneralizedTime otherwise (RFC 5280 §4.1.2.5).
|
|
123
130
|
function time(ns) {
|
|
124
131
|
return decode(function (n, ctx) {
|
|
125
132
|
if (n.tagClass !== "universal" || (n.tagNumber !== TAGS.UTC_TIME && n.tagNumber !== TAGS.GENERALIZED_TIME)) {
|
|
126
133
|
_fail(ctx, ns.prefix + "/bad-time", "time must be UTCTime or GeneralizedTime");
|
|
127
134
|
}
|
|
128
135
|
return asn1.read.time(n);
|
|
136
|
+
}, function (date) {
|
|
137
|
+
var y = date.getUTCFullYear();
|
|
138
|
+
return (y >= 1950 && y < 2050) ? asn1.build.utcTime(date) : asn1.build.generalizedTime(date);
|
|
129
139
|
});
|
|
130
140
|
}
|
|
131
141
|
|
|
@@ -136,6 +146,10 @@ function optional(name, schema, opts) {
|
|
|
136
146
|
opts = opts || {};
|
|
137
147
|
// How the optional field is recognized at its position:
|
|
138
148
|
// - default: a context [tag] (the certificate version [0] shape).
|
|
149
|
+
// - tags: a context tag in the set — an OPTIONAL field whose type is
|
|
150
|
+
// itself a CHOICE of several context alternatives, e.g. the CRMF CertReqMsg
|
|
151
|
+
// popo ProofOfPossession ([0]..[3]) sitting before the universal-SEQUENCE
|
|
152
|
+
// regInfo (RFC 4211 §3), where ANY of the CHOICE's tags marks it present.
|
|
139
153
|
// - whenUniversal: the next element iff its UNIVERSAL tag is in the set —
|
|
140
154
|
// the CRL TBSCertList shape (bare INTEGER version, Time nextUpdate,
|
|
141
155
|
// SEQUENCE revokedCertificates), disambiguated by tag, not a context [n].
|
|
@@ -147,7 +161,9 @@ function optional(name, schema, opts) {
|
|
|
147
161
|
? function () { return true; }
|
|
148
162
|
: opts.whenUniversal
|
|
149
163
|
? function (n) { return n.tagClass === "universal" && opts.whenUniversal.indexOf(n.tagNumber) !== -1; }
|
|
150
|
-
:
|
|
164
|
+
: opts.tags
|
|
165
|
+
? function (n) { return n.tagClass === "context" && opts.tags.indexOf(n.tagNumber) !== -1; }
|
|
166
|
+
: function (n) { return n.tagClass === "context" && n.tagNumber === opts.tag; };
|
|
151
167
|
return { fkind: "optional", name: name, schema: schema, tag: opts.tag, match: match,
|
|
152
168
|
explicit: !!opts.explicit, emptyCode: opts.emptyCode, hasDefault: ("default" in opts), def: opts.default };
|
|
153
169
|
}
|
|
@@ -218,7 +234,7 @@ function implicitSeqOf(tag, item, opts) {
|
|
|
218
234
|
* @primitive pki.schema.engine.walk
|
|
219
235
|
* @signature pki.schema.engine.walk(schema, node, ctx) -> value
|
|
220
236
|
* @since 0.1.7
|
|
221
|
-
* @status
|
|
237
|
+
* @status stable
|
|
222
238
|
* @spec X.690, X.680
|
|
223
239
|
* @related pki.asn1.decode, pki.schema.x509.parse
|
|
224
240
|
*
|
|
@@ -389,6 +405,124 @@ function _consumeTrailing(fld, kids, start, fields, ctx) {
|
|
|
389
405
|
}
|
|
390
406
|
}
|
|
391
407
|
|
|
408
|
+
// ---- the encode engine (the inverse of walk) -------------------------
|
|
409
|
+
// encode(schema, value) is the constructor direction of the SAME schema walk
|
|
410
|
+
// decodes: it interprets the schema against a plain structural value and emits
|
|
411
|
+
// canonical DER. A single schema definition therefore drives both directions, so
|
|
412
|
+
// context-tag (EXPLICIT/IMPLICIT) handling cannot diverge between them — the class
|
|
413
|
+
// of bug where a decoder reads a [tag] IMPLICIT field but an ad-hoc encoder emits
|
|
414
|
+
// the wrong tag is structurally retired. Encode errors are authoring faults (a bad
|
|
415
|
+
// value / an un-encodable schema), thrown as plain Errors, not parse verdicts.
|
|
416
|
+
|
|
417
|
+
function _encFail(message) { throw new Error("schema.encode: " + message); }
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* @primitive pki.schema.engine.encode
|
|
421
|
+
* @signature pki.schema.engine.encode(schema, value, ctx) -> Buffer
|
|
422
|
+
* @since 0.1.17
|
|
423
|
+
* @status experimental
|
|
424
|
+
* @spec X.690, X.680
|
|
425
|
+
* @related pki.schema.engine.walk
|
|
426
|
+
*
|
|
427
|
+
* Encode a structural value to canonical DER by interpreting the SAME schema
|
|
428
|
+
* `walk` decodes — the constructor direction. `value` mirrors the schema: a `seq`
|
|
429
|
+
* takes `{ fieldName: value }`, a leaf its natural JS value (an OID string, a
|
|
430
|
+
* BigInt, a `{ unusedBits, bytes }` BIT STRING, a `Date`), a `repeat` an array, a
|
|
431
|
+
* `choice` `{ arm, value }`. EXPLICIT wrappers and IMPLICIT `[tag]` retagging are
|
|
432
|
+
* applied by the engine, so `walk(schema, decode(encode(schema, v)))` round-trips.
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* var S = pki.schema.engine;
|
|
436
|
+
* var der = S.encode(S.seq([S.field("n", S.integerLeaf())]), { n: 42n });
|
|
437
|
+
*/
|
|
438
|
+
function encode(schema, value, ctx) {
|
|
439
|
+
switch (schema.kind) {
|
|
440
|
+
case "leaf": return schema.write(value);
|
|
441
|
+
case "any": return Buffer.isBuffer(value) ? value : value.bytes;
|
|
442
|
+
case "decode":
|
|
443
|
+
if (typeof schema.write !== "function") _encFail("this decode leaf has no paired encoder (decode(fn, write))");
|
|
444
|
+
return schema.write(value, ctx);
|
|
445
|
+
case "seq": return _encodeSeq(schema, value, ctx);
|
|
446
|
+
case "explicit": return asn1.build.explicit(schema.tag, encode(schema.schema, value, ctx));
|
|
447
|
+
case "repeat": return _encodeRepeat(schema, value, ctx);
|
|
448
|
+
case "choice": return _encodeChoice(schema, value, ctx);
|
|
449
|
+
default: _encFail("cannot encode schema kind " + JSON.stringify(schema.kind));
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function _wrapConstructed(schema, parts) {
|
|
455
|
+
if (schema.implicitTag != null) return asn1.build.contextConstructed(schema.implicitTag, Buffer.concat(parts));
|
|
456
|
+
var mode = schema.assert || "sequence";
|
|
457
|
+
if (mode === "set") return asn1.build.set(parts);
|
|
458
|
+
// "constructed" is a DECODE-time leniency (accept any constructed shape); the
|
|
459
|
+
// canonical encoding is a universal SEQUENCE, same as "sequence".
|
|
460
|
+
if (mode === "sequence" || mode === "constructed") return asn1.build.sequence(parts);
|
|
461
|
+
_encFail("cannot encode a seq/repeat with assert " + JSON.stringify(mode));
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function _present(v) { return v !== undefined && v !== null; }
|
|
466
|
+
|
|
467
|
+
function _encodeSeq(schema, value, ctx) {
|
|
468
|
+
value = value || {};
|
|
469
|
+
var parts = [];
|
|
470
|
+
for (var f = 0; f < schema.fields.length; f++) {
|
|
471
|
+
var fld = schema.fields[f];
|
|
472
|
+
if (fld.fkind === "required") {
|
|
473
|
+
parts.push(encode(fld.schema, value[fld.name], ctx));
|
|
474
|
+
} else if (fld.fkind === "optional") {
|
|
475
|
+
if (_present(value[fld.name])) {
|
|
476
|
+
var enc = encode(fld.schema, value[fld.name], ctx);
|
|
477
|
+
parts.push(fld.explicit ? asn1.build.explicit(fld.tag, enc) : enc);
|
|
478
|
+
}
|
|
479
|
+
} else if (fld.fkind === "trailing") {
|
|
480
|
+
// Emit present members in strictly-increasing tag order (the walk's rule).
|
|
481
|
+
var members = fld.members.slice().sort(function (a, b) { return a.tag - b.tag; });
|
|
482
|
+
for (var i = 0; i < members.length; i++) {
|
|
483
|
+
var mm = members[i];
|
|
484
|
+
if (_present(value[mm.name])) {
|
|
485
|
+
var menc = encode(mm.schema, value[mm.name], ctx);
|
|
486
|
+
parts.push(mm.explicit ? asn1.build.explicit(mm.tag, menc) : menc);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return _wrapConstructed(schema, parts);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function _encodeRepeat(schema, items, ctx) {
|
|
495
|
+
if (!Array.isArray(items)) _encFail("a repeat value must be an array");
|
|
496
|
+
// Enforce the SAME constraints walk enforces, so encode never emits DER its own
|
|
497
|
+
// decoder rejects (the walk(decode(encode)) invariant): the SIZE minimum, and
|
|
498
|
+
// semantic uniqueness (the key computed exactly as walk does, off the round-tripped
|
|
499
|
+
// item — a duplicate is an authoring fault, not a parse verdict).
|
|
500
|
+
if (schema.min != null && items.length < schema.min) {
|
|
501
|
+
_encFail("this repeat requires at least " + schema.min + " element(s) but got " + items.length);
|
|
502
|
+
}
|
|
503
|
+
var parts = items.map(function (it) { return encode(schema.item, it, ctx); });
|
|
504
|
+
if (schema.unique) {
|
|
505
|
+
var seen = new Set();
|
|
506
|
+
parts.forEach(function (tlv) {
|
|
507
|
+
var item = { node: asn1.decode(tlv) };
|
|
508
|
+
item.value = walk(schema.item, item.node, ctx);
|
|
509
|
+
var key = schema.unique(item);
|
|
510
|
+
if (seen.has(key)) _encFail("this repeat requires unique elements but got a duplicate key: " + key);
|
|
511
|
+
seen.add(key);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (schema.derSetOrder) parts = parts.slice().sort(Buffer.compare);
|
|
515
|
+
if (schema.implicitTag != null) return asn1.build.contextConstructed(schema.implicitTag, Buffer.concat(parts));
|
|
516
|
+
return schema.assert === "set" ? asn1.build.set(parts) : asn1.build.sequence(parts);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function _encodeChoice(schema, value, ctx) {
|
|
520
|
+
if (!value || typeof value.arm !== "number" || value.arm < 0 || value.arm >= schema.alts.length) {
|
|
521
|
+
_encFail("a choice value must be { arm: <index>, value: <arm value> }");
|
|
522
|
+
}
|
|
523
|
+
return encode(schema.alts[value.arm].schema, value.value, ctx);
|
|
524
|
+
}
|
|
525
|
+
|
|
392
526
|
module.exports = {
|
|
393
527
|
// structural
|
|
394
528
|
seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
|
|
@@ -399,5 +533,5 @@ module.exports = {
|
|
|
399
533
|
implicitNull: implicitNull, implicitInteger: implicitInteger,
|
|
400
534
|
any: any, decode: decode, time: time,
|
|
401
535
|
// engine
|
|
402
|
-
walk: walk,
|
|
536
|
+
walk: walk, encode: encode,
|
|
403
537
|
};
|
package/lib/schema-ocsp.js
CHANGED
|
@@ -425,7 +425,7 @@ var OCSP_REQUEST = schema.seq([
|
|
|
425
425
|
* @primitive pki.schema.ocsp.parseRequest
|
|
426
426
|
* @signature pki.schema.ocsp.parseRequest(input) -> ocspRequest
|
|
427
427
|
* @since 0.1.11
|
|
428
|
-
* @status
|
|
428
|
+
* @status stable
|
|
429
429
|
* @spec RFC 6960
|
|
430
430
|
* @related pki.schema.ocsp.parseResponse, pki.schema.parse
|
|
431
431
|
*
|
|
@@ -447,7 +447,7 @@ var parseRequest = pkix.makeParser({ pemLabel: "OCSP REQUEST", PemError: PemErro
|
|
|
447
447
|
* @primitive pki.schema.ocsp.parseResponse
|
|
448
448
|
* @signature pki.schema.ocsp.parseResponse(input) -> ocspResponse
|
|
449
449
|
* @since 0.1.11
|
|
450
|
-
* @status
|
|
450
|
+
* @status stable
|
|
451
451
|
* @spec RFC 6960
|
|
452
452
|
* @related pki.schema.ocsp.parseRequest, pki.schema.parse
|
|
453
453
|
*
|
|
@@ -471,7 +471,7 @@ var parseResponse = pkix.makeParser({ pemLabel: "OCSP RESPONSE", PemError: PemEr
|
|
|
471
471
|
* @primitive pki.schema.ocsp.pemDecode
|
|
472
472
|
* @signature pki.schema.ocsp.pemDecode(text, label?) -> Buffer
|
|
473
473
|
* @since 0.1.11
|
|
474
|
-
* @status
|
|
474
|
+
* @status stable
|
|
475
475
|
* @spec RFC 7468, RFC 6960
|
|
476
476
|
* @related pki.schema.ocsp.parseResponse
|
|
477
477
|
*
|
package/lib/schema-pkcs8.js
CHANGED
|
@@ -102,7 +102,7 @@ var ENCRYPTED_PRIVATE_KEY_INFO = schema.seq([
|
|
|
102
102
|
* @primitive pki.schema.pkcs8.parse
|
|
103
103
|
* @signature pki.schema.pkcs8.parse(input) -> privateKey
|
|
104
104
|
* @since 0.1.9
|
|
105
|
-
* @status
|
|
105
|
+
* @status stable
|
|
106
106
|
* @spec RFC 5208, RFC 5958
|
|
107
107
|
* @related pki.schema.parse, pki.schema.x509.parse
|
|
108
108
|
*
|
|
@@ -124,7 +124,7 @@ var parse = pkix.makeParser({ pemLabel: "PRIVATE KEY", PemError: PemError, Error
|
|
|
124
124
|
* @primitive pki.schema.pkcs8.parseEncrypted
|
|
125
125
|
* @signature pki.schema.pkcs8.parseEncrypted(input) -> encrypted
|
|
126
126
|
* @since 0.1.9
|
|
127
|
-
* @status
|
|
127
|
+
* @status stable
|
|
128
128
|
* @spec RFC 5958, RFC 5208
|
|
129
129
|
* @related pki.schema.pkcs8.parse
|
|
130
130
|
*
|
|
@@ -143,7 +143,7 @@ var parseEncrypted = pkix.makeParser({ pemLabel: "ENCRYPTED PRIVATE KEY", PemErr
|
|
|
143
143
|
* @primitive pki.schema.pkcs8.pemDecode
|
|
144
144
|
* @signature pki.schema.pkcs8.pemDecode(text, label?) -> Buffer
|
|
145
145
|
* @since 0.1.9
|
|
146
|
-
* @status
|
|
146
|
+
* @status stable
|
|
147
147
|
* @spec RFC 7468, RFC 5958
|
|
148
148
|
* @related pki.schema.pkcs8.parse
|
|
149
149
|
*
|
|
@@ -160,7 +160,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "PRIVATE
|
|
|
160
160
|
* @primitive pki.schema.pkcs8.pemEncode
|
|
161
161
|
* @signature pki.schema.pkcs8.pemEncode(der, label?) -> string
|
|
162
162
|
* @since 0.1.9
|
|
163
|
-
* @status
|
|
163
|
+
* @status stable
|
|
164
164
|
* @spec RFC 7468
|
|
165
165
|
* @related pki.schema.pkcs8.pemDecode
|
|
166
166
|
*
|
package/lib/schema-pkix.js
CHANGED
|
@@ -166,6 +166,12 @@ function attrValueToString(ns) {
|
|
|
166
166
|
}
|
|
167
167
|
return "#" + node.bytes.toString("hex");
|
|
168
168
|
}
|
|
169
|
+
}, function (value) {
|
|
170
|
+
// encode: a "#hex" escape (RFC 4514 §2.4) round-trips its raw DER verbatim; any
|
|
171
|
+
// other string encodes as UTF8String (the decode does not preserve the original
|
|
172
|
+
// string type, so this is the canonical re-encoding, not a byte-exact one).
|
|
173
|
+
if (typeof value === "string" && value.charAt(0) === "#") return Buffer.from(value.slice(1), "hex");
|
|
174
|
+
return asn1.build.utf8(value);
|
|
169
175
|
});
|
|
170
176
|
}
|
|
171
177
|
|
|
@@ -193,25 +199,34 @@ function relativeDistinguishedName(ns) {
|
|
|
193
199
|
// RelativeDistinguishedName ::= SET SIZE (1..MAX) — an empty SET {} is malformed.
|
|
194
200
|
return schema.setOf(attributeTypeAndValue(ns), { assert: "set", min: 1, code: ns.prefix + "/bad-rdn", what: "RelativeDistinguishedName" });
|
|
195
201
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
202
|
+
// opts.implicitTag (optional): read the Name as a [tag] IMPLICIT RDNSequence — the
|
|
203
|
+
// context tag REPLACES the universal SEQUENCE tag, so the node is a context-class
|
|
204
|
+
// constructed [tag] whose children ARE the RDN SETs (the CRMF CertTemplate issuer
|
|
205
|
+
// [3] / subject [5] IMPLICIT arm, RFC 4211 §5). With no opts the shape is a bare
|
|
206
|
+
// universal SEQUENCE OF, byte-identical to every existing caller.
|
|
207
|
+
function name(ns, opts) {
|
|
208
|
+
opts = opts || {};
|
|
209
|
+
function nameBuild(m) {
|
|
210
|
+
var rdns = [], parts = [];
|
|
211
|
+
m.items.forEach(function (rdnItem) {
|
|
212
|
+
var atvs = [], atvParts = [];
|
|
213
|
+
rdnItem.value.items.forEach(function (atvItem) {
|
|
214
|
+
var a = atvItem.value.result; // atvItem.value = the atv seq-match; .result = its build result
|
|
215
|
+
atvs.push(a);
|
|
216
|
+
var label = (a.name && DN_SHORT[a.name]) || a.name || a.type;
|
|
217
|
+
atvParts.push(label + "=" + _escapeDnValue(a.value));
|
|
211
218
|
});
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
rdns.push(atvs);
|
|
220
|
+
parts.push(atvParts.join("+"));
|
|
221
|
+
});
|
|
222
|
+
return { rdns: rdns, dn: parts.join(", ") };
|
|
223
|
+
}
|
|
224
|
+
if (opts.implicitTag != null) {
|
|
225
|
+
return schema.implicitSeqOf(opts.implicitTag, relativeDistinguishedName(ns), {
|
|
226
|
+
code: ns.prefix + "/bad-name", what: "Name", build: nameBuild });
|
|
227
|
+
}
|
|
228
|
+
return schema.seqOf(relativeDistinguishedName(ns), {
|
|
229
|
+
assert: "sequence", code: ns.prefix + "/bad-name", what: "Name", build: nameBuild });
|
|
215
230
|
}
|
|
216
231
|
|
|
217
232
|
// GeneralName ::= CHOICE (RFC 5280 §4.2.1.6). A validate-and-surface-raw leaf for a
|
|
@@ -623,12 +638,20 @@ function extension(ns) {
|
|
|
623
638
|
return { oid: extnID, name: ns.oid.name(extnID) || null, critical: critical, value: asn1.read.octetString(valueNode) };
|
|
624
639
|
});
|
|
625
640
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
641
|
+
// opts.implicitTag (optional): read Extensions as a [tag] IMPLICIT SEQUENCE OF
|
|
642
|
+
// Extension — the context tag REPLACES the universal SEQUENCE tag, for the CRMF
|
|
643
|
+
// CertTemplate extensions [9] (RFC 4211 §5). With no opts the shape is a bare
|
|
644
|
+
// universal SEQUENCE OF, byte-identical to every existing caller.
|
|
645
|
+
function extensions(ns, opts) {
|
|
646
|
+
opts = opts || {};
|
|
647
|
+
var EXT = extension(ns);
|
|
648
|
+
var extOpts = {
|
|
649
|
+
min: 1, code: ns.prefix + "/bad-extensions", what: "Extensions",
|
|
650
|
+
unique: function (it) { return it.value.oid; }, dupCode: ns.prefix + "/duplicate-extension",
|
|
630
651
|
build: function (m) { return m.items.map(function (it) { return it.value; }); },
|
|
631
|
-
}
|
|
652
|
+
};
|
|
653
|
+
if (opts.implicitTag != null) return schema.implicitSeqOf(opts.implicitTag, EXT, extOpts);
|
|
654
|
+
return schema.seqOf(EXT, Object.assign({ assert: "sequence" }, extOpts));
|
|
632
655
|
}
|
|
633
656
|
|
|
634
657
|
// SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier,
|
|
@@ -636,17 +659,26 @@ function extensions(ns) {
|
|
|
636
659
|
// universal SEQUENCE — a context-tagged or SET-tagged constructed node carrying
|
|
637
660
|
// two well-formed children is NOT a SubjectPublicKeyInfo. Shared by the
|
|
638
661
|
// certificate and CSR parsers.
|
|
639
|
-
|
|
662
|
+
// opts.implicitTag (optional): read the SubjectPublicKeyInfo as a [tag] IMPLICIT
|
|
663
|
+
// SEQUENCE (a context-class constructed node whose children are algorithm +
|
|
664
|
+
// subjectPublicKey), for the CRMF CertTemplate publicKey [6] (RFC 4211 §5). With no
|
|
665
|
+
// opts the shape is a universal SEQUENCE, byte-identical to every existing caller.
|
|
666
|
+
function spki(ns, opts) {
|
|
667
|
+
opts = opts || {};
|
|
640
668
|
return schema.seq([
|
|
641
669
|
schema.field("algorithm", algorithmIdentifier(ns)),
|
|
642
670
|
schema.field("subjectPublicKey", schema.bitString()),
|
|
643
671
|
], {
|
|
644
|
-
assert:
|
|
672
|
+
assert: opts.implicitTag != null ? "implicit" : "sequence", implicitTag: opts.implicitTag,
|
|
673
|
+
arity: { exact: 2 }, code: ns.prefix + "/bad-spki", what: "SubjectPublicKeyInfo",
|
|
645
674
|
build: function (m) {
|
|
646
675
|
return {
|
|
647
676
|
algorithm: m.fields.algorithm.value.result,
|
|
648
677
|
publicKey: { unusedBits: m.fields.subjectPublicKey.value.unusedBits, bytes: m.fields.subjectPublicKey.value.bytes },
|
|
649
|
-
bytes
|
|
678
|
+
// `bytes` is the importable SubjectPublicKeyInfo DER. In IMPLICIT [tag]
|
|
679
|
+
// mode the wire node leads with the context tag, so recover the SEQUENCE
|
|
680
|
+
// encoding a consumer imports / hashes rather than the [tag] wire form.
|
|
681
|
+
bytes: opts.implicitTag != null ? asn1.sequenceTlv(m.node) : m.node.bytes,
|
|
650
682
|
};
|
|
651
683
|
},
|
|
652
684
|
});
|
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:b6db5d53-2a3e-46f9-9187-9c2f6fd9249b",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-07T05:30:55.753Z",
|
|
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.17",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
25
|
+
"version": "0.1.17",
|
|
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.17",
|
|
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.17",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|