@blamejs/pki 0.1.8 → 0.1.10
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 +25 -1
- package/README.md +4 -2
- package/lib/asn1-der.js +38 -3
- package/lib/framework-error.js +10 -0
- package/lib/oid.js +13 -2
- package/lib/schema-all.js +31 -2
- package/lib/schema-cms.js +375 -0
- package/lib/schema-crl.js +2 -7
- package/lib/schema-csr.js +6 -24
- package/lib/schema-engine.js +9 -1
- package/lib/schema-pkcs8.js +211 -0
- package/lib/schema-pkix.js +49 -0
- package/lib/schema-x509.js +5 -11
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,31 @@ 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.
|
|
7
|
+
## v0.1.10 — 2026-07-05
|
|
8
|
+
|
|
9
|
+
A CMS SignedData parser joins the pki.schema family; coverage-guided fuzzing now runs in CI.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.cms.parse — a CMS SignedData parser per RFC 5652 (§3 ContentInfo + §5 SignedData). It turns a DER Buffer or a 'CMS' PEM string into { version, digestAlgorithms, encapContentInfo, certificates, crls, signerInfos }. encapContentInfo.eContent is the raw content (or null when the signature is detached); each SignerInfo carries its raw signature and, when signed attributes are present, the on-wire signedAttrsBytes for external verification. The signer identifier is an issuerAndSerialNumber or a subjectKeyIdentifier, with the version-to-identifier rule (RFC 5652 §5.3) enforced, and a degenerate certificates-only SignedData (empty digest algorithms and signer infos) is accepted. A ContentInfo whose content type is not id-signedData throws cms/unsupported-content-type (a recognized PKCS#7 type) or cms/unknown-content-type; a malformed structure throws a typed CmsError (cms/*) and a leaf-level codec fault surfaces as asn1/*. pki.schema.cms.pemDecode / pemEncode handle the PEM envelope, and pki.schema.parse detect-and-routes a CMS message.
|
|
14
|
+
- pki.asn1.read.octetStringImplicit and the pki.schema.engine.implicitOctetString(tag) leaf — read a context-tagged IMPLICIT OCTET STRING (the shape the CMS SignerIdentifier subjectKeyIdentifier [0] takes), the primitive-leaf sibling of implicitBitString.
|
|
15
|
+
- Coverage-guided fuzzing in CI — a ClusterFuzzLite integration (.clusterfuzzlite/) plus pull-request and nightly-batch workflows run jazzer.js + libFuzzer against the pki.asn1.decode, pki.schema.x509.parse and pki.schema.cms.parse harnesses. A finding fails the run with the reproducer inline and uploads the crash input as an artifact. The integration is detectable by OpenSSF Scorecard's Fuzzing check.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- The OpenSSL interop runner counted a cross-check the oracle could not perform (for example, an OpenSSL predating ML-DSA) as a pass. Such a cross-check is now recorded as a skip and reported separately, so the interop pass count is not inflated by checks that never executed.
|
|
20
|
+
|
|
21
|
+
## v0.1.9 — 2026-07-05
|
|
22
|
+
|
|
23
|
+
A PKCS#8 private-key parser joins the pki.schema family.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- pki.schema.pkcs8.parse — a PKCS#8 PrivateKeyInfo / OneAsymmetricKey parser per RFC 5208 §5 and RFC 5958 §2. It turns a DER Buffer or a 'PRIVATE KEY' PEM string into { version, privateKeyAlgorithm, privateKey, attributes, publicKey }, where privateKey is the raw OCTET STRING content (the inner RSA/EC/curve key, decoded by the caller via privateKeyAlgorithm.oid) and publicKey is present only for a v2 key. The version must be v1 (0) or v2 (1), and a [1] public key is permitted only in a v2 key (both directions enforced). A malformed key throws a typed Pkcs8Error (pkcs8/*); a leaf-level codec fault surfaces as asn1/*. pki.schema.pkcs8.pemDecode / pemEncode handle the PEM envelope.
|
|
28
|
+
- pki.schema.pkcs8.parseEncrypted — recognizes an EncryptedPrivateKeyInfo ('ENCRYPTED PRIVATE KEY') and surfaces its encryptionAlgorithm and raw encryptedData. Decryption (PBES2/PBKDF2 + a passphrase) is a separate concern and is not performed here. This is an explicit call — an EncryptedPrivateKeyInfo shares its SEQUENCE{SEQUENCE, OCTET STRING} shape with a PKCS#1 DigestInfo, so pki.schema.parse does not auto-route it (structure alone cannot classify it without a validated encryption-algorithm discriminator).
|
|
29
|
+
- pki.asn1.read.enumerated's sibling pki.asn1.read.bitStringImplicit and the pki.schema.engine.implicitBitString(tag) leaf — read a context-tagged IMPLICIT BIT STRING (the shape a PKCS#8 OneAsymmetricKey public key [1] takes).
|
|
30
|
+
|
|
31
|
+
## v0.1.8 — 2026-07-04
|
|
8
32
|
|
|
9
33
|
A PKCS#10 certification-request parser joins the pki.schema family.
|
|
10
34
|
|
package/README.md
CHANGED
|
@@ -194,9 +194,11 @@ is callable today; nothing below is a stub.
|
|
|
194
194
|
| `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields — `parse`, `pemDecode`, `pemEncode` |
|
|
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
|
+
| `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 SignedData per RFC 5652 — encapsulated content, signer infos, raw signed-attribute bytes for external verification, certificates / CRLs kept raw, fail-closed; non-SignedData content types recognized (not decoded) — `parse`, `pemDecode`, `pemEncode` |
|
|
197
199
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` plus the schema combinators |
|
|
198
200
|
| `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
|
|
199
|
-
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError`, each carrying a stable `code` in `domain/reason` form |
|
|
201
|
+
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError`, each carrying a stable `code` in `domain/reason` form |
|
|
200
202
|
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
|
|
201
203
|
|
|
202
204
|
### CLI
|
|
@@ -212,7 +214,7 @@ pki parse cert.pem # structured JSON summary of a certific
|
|
|
212
214
|
|
|
213
215
|
Certificate build/sign/verify, certification-path
|
|
214
216
|
validation, CMS (SignedData / EnvelopedData / EncryptedData / AuthenticatedData),
|
|
215
|
-
OCSP, RFC 3161 timestamping, PKCS#8
|
|
217
|
+
OCSP, RFC 3161 timestamping, PKCS#8 decryption (PBES2) / SPKI / PKCS#12, and the
|
|
216
218
|
post-quantum certificate and CMS surface (ML-DSA / ML-KEM / SLH-DSA and hybrid
|
|
217
219
|
composites) are on the roadmap and ride this same core. See
|
|
218
220
|
[ROADMAP.md](ROADMAP.md) for the full plan and current status of each area, and
|
package/lib/asn1-der.js
CHANGED
|
@@ -318,9 +318,11 @@ function readEnumerated(node) {
|
|
|
318
318
|
return _readIntegerLikeContent(node, "ENUMERATED", "readEnumerated");
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
321
|
+
// The BIT STRING content decoder (the leading unused-bit octet + body, with the
|
|
322
|
+
// DER zero-pad rule). Shared by the universal reader and the IMPLICIT
|
|
323
|
+
// context-tagged reader — the caller asserts the tag first.
|
|
324
|
+
function _readBitStringContent(node, who) {
|
|
325
|
+
_expectPrimitive(node, who);
|
|
324
326
|
var c = node.content;
|
|
325
327
|
if (c.length === 0) throw new Asn1Error("asn1/bad-bit-string", "BIT STRING must have >= 1 content octet");
|
|
326
328
|
var unusedBits = c[0];
|
|
@@ -335,12 +337,43 @@ function readBitString(node) {
|
|
|
335
337
|
return { unusedBits: unusedBits, bytes: c.subarray(1) };
|
|
336
338
|
}
|
|
337
339
|
|
|
340
|
+
function readBitString(node) {
|
|
341
|
+
_expectUniversal(node, TAGS.BIT_STRING, "readBitString");
|
|
342
|
+
return _readBitStringContent(node, "readBitString");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Read a [tag] IMPLICIT BIT STRING — a context-class PRIMITIVE node whose content
|
|
346
|
+
// is a bit-string body (identical content rules to a universal BIT STRING). The
|
|
347
|
+
// IMPLICIT tag replaces the universal one, so there is no inner universal node.
|
|
348
|
+
// Used for the PKCS#8 OneAsymmetricKey publicKey [1] (RFC 5958 §2).
|
|
349
|
+
function readBitStringImplicit(node, tag) {
|
|
350
|
+
if (node.tagClass !== "context" || node.tagNumber !== tag) {
|
|
351
|
+
throw new Asn1Error("asn1/unexpected-tag", "readBitStringImplicit: expected context tag [" + tag +
|
|
352
|
+
"], got " + node.tagClass + "/" + node.tagNumber);
|
|
353
|
+
}
|
|
354
|
+
return _readBitStringContent(node, "readBitStringImplicit");
|
|
355
|
+
}
|
|
356
|
+
|
|
338
357
|
function readOctetString(node) {
|
|
339
358
|
_expectUniversal(node, TAGS.OCTET_STRING, "readOctetString");
|
|
340
359
|
_expectPrimitive(node, "readOctetString");
|
|
341
360
|
return node.content;
|
|
342
361
|
}
|
|
343
362
|
|
|
363
|
+
// Read a [tag] IMPLICIT OCTET STRING — a context-class PRIMITIVE node whose
|
|
364
|
+
// content is the octet-string body (the IMPLICIT tag replaces the universal one,
|
|
365
|
+
// so there is no inner universal node). Primitive-only, like its universal
|
|
366
|
+
// counterpart, so the BER constructed/streamed form is rejected. Used for the CMS
|
|
367
|
+
// SignerIdentifier subjectKeyIdentifier [0] (RFC 5652 §5.3).
|
|
368
|
+
function readOctetStringImplicit(node, tag) {
|
|
369
|
+
if (node.tagClass !== "context" || node.tagNumber !== tag) {
|
|
370
|
+
throw new Asn1Error("asn1/unexpected-tag", "readOctetStringImplicit: expected context tag [" + tag +
|
|
371
|
+
"], got " + node.tagClass + "/" + node.tagNumber);
|
|
372
|
+
}
|
|
373
|
+
_expectPrimitive(node, "readOctetStringImplicit");
|
|
374
|
+
return node.content;
|
|
375
|
+
}
|
|
376
|
+
|
|
344
377
|
function readNull(node) {
|
|
345
378
|
_expectUniversal(node, TAGS.NULL, "readNull");
|
|
346
379
|
_expectPrimitive(node, "readNull");
|
|
@@ -773,7 +806,9 @@ module.exports = {
|
|
|
773
806
|
integer: readInteger,
|
|
774
807
|
enumerated: readEnumerated,
|
|
775
808
|
bitString: readBitString,
|
|
809
|
+
bitStringImplicit: readBitStringImplicit,
|
|
776
810
|
octetString: readOctetString,
|
|
811
|
+
octetStringImplicit: readOctetStringImplicit,
|
|
777
812
|
nullValue: readNull,
|
|
778
813
|
oid: readOid,
|
|
779
814
|
string: readString,
|
package/lib/framework-error.js
CHANGED
|
@@ -133,6 +133,14 @@ var SchemaError = defineClass("SchemaError", { withCause: true });
|
|
|
133
133
|
// (RFC 2986).
|
|
134
134
|
var CsrError = defineClass("CsrError", { withCause: true });
|
|
135
135
|
|
|
136
|
+
// Pkcs8Error — a byte sequence that is not a well-formed PKCS#8 PrivateKeyInfo /
|
|
137
|
+
// OneAsymmetricKey or EncryptedPrivateKeyInfo (RFC 5208 §5, RFC 5958 §2/§3).
|
|
138
|
+
var Pkcs8Error = defineClass("Pkcs8Error", { withCause: true });
|
|
139
|
+
|
|
140
|
+
// CmsError — a byte sequence that is not a well-formed CMS ContentInfo /
|
|
141
|
+
// SignedData (RFC 5652). Carries the underlying leaf fault as `.cause`.
|
|
142
|
+
var CmsError = defineClass("CmsError", { withCause: true });
|
|
143
|
+
|
|
136
144
|
module.exports = {
|
|
137
145
|
PkiError: PkiError,
|
|
138
146
|
defineClass: defineClass,
|
|
@@ -144,4 +152,6 @@ module.exports = {
|
|
|
144
152
|
CrlError: CrlError,
|
|
145
153
|
SchemaError: SchemaError,
|
|
146
154
|
CsrError: CsrError,
|
|
155
|
+
Pkcs8Error: Pkcs8Error,
|
|
156
|
+
CmsError: CmsError,
|
|
147
157
|
};
|
package/lib/oid.js
CHANGED
|
@@ -70,8 +70,19 @@ var FAMILIES = {
|
|
|
70
70
|
rsaEncryption: 1, rsassaPss: 10, sha256WithRSAEncryption: 11,
|
|
71
71
|
sha384WithRSAEncryption: 12, sha512WithRSAEncryption: 13 } },
|
|
72
72
|
|
|
73
|
-
// PKCS#
|
|
74
|
-
|
|
73
|
+
// PKCS#7 / CMS content types (RFC 5652 §4, RFC 2315). id-signedData is the one
|
|
74
|
+
// this toolkit structurally decodes; the rest are recognized-and-deferred.
|
|
75
|
+
pkcs7: { base: [1, 2, 840, 113549, 1, 7], of: {
|
|
76
|
+
data: 1, signedData: 2, envelopedData: 3, signedAndEnvelopedData: 4,
|
|
77
|
+
digestedData: 5, encryptedData: 6 } },
|
|
78
|
+
|
|
79
|
+
// PKCS#9 attribute types — incl. the CMS signed-attribute OIDs (RFC 5652 §11).
|
|
80
|
+
pkcs9: { base: [1, 2, 840, 113549, 1, 9], of: {
|
|
81
|
+
emailAddress: 1, contentType: 3, messageDigest: 4, signingTime: 5,
|
|
82
|
+
challengePassword: 7, extensionRequest: 14 } },
|
|
83
|
+
|
|
84
|
+
// S/MIME content types on the PKCS#9 smime arc (RFC 5652, RFC 3161): id-ct.
|
|
85
|
+
smimeCt: { base: [1, 2, 840, 113549, 1, 9, 16, 1], of: { authData: 2, tSTInfo: 4 } },
|
|
75
86
|
|
|
76
87
|
// ANSI X9.62 EC public key, named curve, and ECDSA signatures.
|
|
77
88
|
ansiX962: { base: [1, 2, 840, 10045], of: {
|
package/lib/schema-all.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* @intro
|
|
13
13
|
* The schema family: a declarative ASN.1 structure-schema engine and the
|
|
14
14
|
* per-format parsers built on it. Every format (X.509 certificates, CRLs,
|
|
15
|
-
* PKCS#10 certification requests,
|
|
15
|
+
* PKCS#10 certification requests, PKCS#8 private keys, and CMS SignedData) is a
|
|
16
16
|
* member that COMPOSES the
|
|
17
17
|
* shared engine and the shared PKIX sub-schemas (AlgorithmIdentifier, Name,
|
|
18
18
|
* Extension), so a structural rule — bounds-checked positional reads,
|
|
@@ -35,6 +35,8 @@ var pkix = require("./schema-pkix");
|
|
|
35
35
|
var x509 = require("./schema-x509");
|
|
36
36
|
var crl = require("./schema-crl");
|
|
37
37
|
var csr = require("./schema-csr");
|
|
38
|
+
var pkcs8 = require("./schema-pkcs8");
|
|
39
|
+
var cms = require("./schema-cms");
|
|
38
40
|
var frameworkError = require("./framework-error");
|
|
39
41
|
|
|
40
42
|
var SchemaError = frameworkError.SchemaError;
|
|
@@ -51,6 +53,31 @@ var ENTRY = { pemLabel: null, PemError: PemError, ErrorClass: SchemaError, prefi
|
|
|
51
53
|
// logic. Order matters: the most specific detector wins, so a new member is
|
|
52
54
|
// inserted ahead of any more-permissive one.
|
|
53
55
|
var FORMATS = [
|
|
56
|
+
{
|
|
57
|
+
// CMS ContentInfo (RFC 5652 §3) — the only registered root that leads with an
|
|
58
|
+
// OBJECT IDENTIFIER child (a SEQUENCE of exactly 2: contentType OID + a
|
|
59
|
+
// context [0] EXPLICIT content wrapper). Disjoint from the INTEGER-first pkcs8
|
|
60
|
+
// and the tbs-SEQUENCE-first signed-envelope trio, so it detects unambiguously.
|
|
61
|
+
// A non-SignedData content type routes here and gets a precise
|
|
62
|
+
// cms/unsupported-content-type rather than schema/unknown-format.
|
|
63
|
+
name: "cms",
|
|
64
|
+
module: cms,
|
|
65
|
+
detect: cms.matches,
|
|
66
|
+
parse: function (input) { return cms.parse(input); },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
// PKCS#8 PrivateKeyInfo / OneAsymmetricKey — SEQUENCE whose first child is an
|
|
70
|
+
// INTEGER (version) and third an OCTET STRING (privateKey); disjoint from the
|
|
71
|
+
// signed-envelope trio. (EncryptedPrivateKeyInfo is deliberately NOT
|
|
72
|
+
// auto-routed: its SEQUENCE{SEQUENCE, OCTET STRING} shape is ambiguous — a
|
|
73
|
+
// PKCS#1 DigestInfo is identical — so structural detection cannot classify it
|
|
74
|
+
// without a validated encryption-algorithm discriminator, which arrives with
|
|
75
|
+
// the PBES layer. It is reached explicitly via pki.schema.pkcs8.parseEncrypted.)
|
|
76
|
+
name: "pkcs8",
|
|
77
|
+
module: pkcs8,
|
|
78
|
+
detect: pkcs8.matches,
|
|
79
|
+
parse: function (input) { return pkcs8.parse(input); },
|
|
80
|
+
},
|
|
54
81
|
{
|
|
55
82
|
// CertificationRequest ::= SEQUENCE { certificationRequestInfo,
|
|
56
83
|
// signatureAlgorithm, signature } — the same outer 3-element shape,
|
|
@@ -93,7 +120,7 @@ var FORMATS = [
|
|
|
93
120
|
* The names of every registered format, in detection order.
|
|
94
121
|
*
|
|
95
122
|
* @example
|
|
96
|
-
* pki.schema.all(); // → ["csr", "crl", "x509"]
|
|
123
|
+
* pki.schema.all(); // → ["cms", "pkcs8", "csr", "crl", "x509"]
|
|
97
124
|
*/
|
|
98
125
|
function all() { return FORMATS.map(function (f) { return f.name; }); }
|
|
99
126
|
|
|
@@ -134,6 +161,8 @@ module.exports = {
|
|
|
134
161
|
x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode },
|
|
135
162
|
crl: { parse: crl.parse, pemDecode: crl.pemDecode },
|
|
136
163
|
csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
|
|
164
|
+
pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
|
|
165
|
+
cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
|
|
137
166
|
all: all,
|
|
138
167
|
parse: parse,
|
|
139
168
|
};
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.schema.cms
|
|
6
|
+
* @nav Schema
|
|
7
|
+
* @title CMS
|
|
8
|
+
* @order 160
|
|
9
|
+
* @slug cms
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
12
|
+
* CMS SignedData handling per RFC 5652 (§3 ContentInfo envelope, §5 SignedData).
|
|
13
|
+
* `parse` turns a DER or PEM (`CMS`) message into a structured object: the
|
|
14
|
+
* SignedData version, the digest algorithms, the encapsulated content, the
|
|
15
|
+
* certificate / CRL sets, and the signer infos. It is an OID-dispatch envelope —
|
|
16
|
+
* ContentInfo reads its `contentType` and structurally decodes only
|
|
17
|
+
* `id-signedData`; the other PKCS#7 content types (EnvelopedData, EncryptedData,
|
|
18
|
+
* …) are recognized and rejected with a precise `cms/unsupported-content-type`
|
|
19
|
+
* rather than a generic unknown-format error.
|
|
20
|
+
*
|
|
21
|
+
* CMS is a signed container: the bytes an external verifier must hash are
|
|
22
|
+
* surfaced RAW and never re-serialized. `encapContentInfo.eContent` is the raw
|
|
23
|
+
* content (or `null` for a detached signature); each SignerInfo's `signature` is
|
|
24
|
+
* raw, and `signedAttrsBytes` is the on-wire `[0]` SignedAttributes TLV so a
|
|
25
|
+
* verifier can re-tag it to the universal SET the signature is computed over
|
|
26
|
+
* (§5.4). Embedded certificates and CRLs are surfaced as raw DER + their outer
|
|
27
|
+
* tag, so an obsolete or unknown alternative never fails the parse. DER-only,
|
|
28
|
+
* fail-closed.
|
|
29
|
+
*
|
|
30
|
+
* @card
|
|
31
|
+
* Parse DER / PEM CMS SignedData (RFC 5652) into structured, validated fields —
|
|
32
|
+
* encapsulated content, signer infos, raw signed-attribute bytes for external
|
|
33
|
+
* verification, certificates/CRLs kept raw, fail-closed.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
var asn1 = require("./asn1-der");
|
|
37
|
+
var schema = require("./schema-engine");
|
|
38
|
+
var pkix = require("./schema-pkix");
|
|
39
|
+
var oid = require("./oid");
|
|
40
|
+
var frameworkError = require("./framework-error");
|
|
41
|
+
|
|
42
|
+
var CmsError = frameworkError.CmsError;
|
|
43
|
+
var PemError = frameworkError.PemError;
|
|
44
|
+
|
|
45
|
+
// The cms error namespace the schema engine walks under.
|
|
46
|
+
var NS = pkix.makeNS("cms", CmsError, oid);
|
|
47
|
+
|
|
48
|
+
var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
|
|
49
|
+
var ATTRIBUTE = pkix.attribute(NS);
|
|
50
|
+
var NAME = pkix.name(NS);
|
|
51
|
+
|
|
52
|
+
// CMSVersion ::= INTEGER — the SignedData version is {1,3,4,5} and the SignerInfo
|
|
53
|
+
// version is {1,3} (RFC 5652 §5.1, §5.3). Wider accept maps than any other format.
|
|
54
|
+
var SIGNED_DATA_VERSION = pkix.versionReader(NS, { "1": 1, "3": 3, "4": 4, "5": 5 });
|
|
55
|
+
var SIGNER_VERSION = pkix.versionReader(NS, { "1": 1, "3": 3 });
|
|
56
|
+
|
|
57
|
+
// id-signedData is the one content type this build structurally decodes; the rest
|
|
58
|
+
// are recognized-and-deferred (a precise diagnostic, not a silent unknown-format).
|
|
59
|
+
// OIDs resolve from the registry (pkcs7 / smimeCt families), never dotted literals.
|
|
60
|
+
var OID_SIGNED_DATA = oid.byName("signedData");
|
|
61
|
+
var OID_DATA = oid.byName("data");
|
|
62
|
+
var DEFERRED = new Set([
|
|
63
|
+
oid.byName("data"), oid.byName("envelopedData"), oid.byName("signedAndEnvelopedData"),
|
|
64
|
+
oid.byName("digestedData"), oid.byName("encryptedData"), oid.byName("authData"),
|
|
65
|
+
]);
|
|
66
|
+
// The two mandatory signed-attribute types (RFC 5652 §5.3, §11.1/§11.2).
|
|
67
|
+
var OID_CONTENT_TYPE = oid.byName("contentType");
|
|
68
|
+
var OID_MESSAGE_DIGEST = oid.byName("messageDigest");
|
|
69
|
+
|
|
70
|
+
// Enforce the SignedAttributes value constraints (RFC 5652 §5.3, §11) — if
|
|
71
|
+
// signedAttrs is present it MUST contain exactly one content-type attribute and
|
|
72
|
+
// exactly one message-digest attribute, each single-valued. The content-type value
|
|
73
|
+
// == eContentType and messageDigest == the actual content hash are §5.6
|
|
74
|
+
// VERIFICATION concerns (surfaced, checked by the verifier), not structural-parse
|
|
75
|
+
// concerns.
|
|
76
|
+
function _checkSignedAttrs(attrs) {
|
|
77
|
+
var ct = 0, md = 0, seen = Object.create(null);
|
|
78
|
+
for (var i = 0; i < attrs.length; i++) {
|
|
79
|
+
var a = attrs[i];
|
|
80
|
+
// RFC 5652 §5.3 — a signerInfo MUST NOT include multiple instances of the
|
|
81
|
+
// same signed-attribute type (specific codes for the two mandatory types).
|
|
82
|
+
if (seen[a.type]) {
|
|
83
|
+
if (a.type === OID_CONTENT_TYPE) throw NS.E("cms/duplicate-content-type", "signedAttrs must not repeat the content-type attribute");
|
|
84
|
+
if (a.type === OID_MESSAGE_DIGEST) throw NS.E("cms/duplicate-message-digest", "signedAttrs must not repeat the message-digest attribute");
|
|
85
|
+
throw NS.E("cms/duplicate-signed-attr", "signedAttrs must not include multiple instances of the same attribute type (" + a.type + ")");
|
|
86
|
+
}
|
|
87
|
+
seen[a.type] = true;
|
|
88
|
+
if (a.type === OID_CONTENT_TYPE) {
|
|
89
|
+
ct += 1;
|
|
90
|
+
if (a.values.length !== 1) throw NS.E("cms/bad-content-type-attr", "the content-type signed attribute must be single-valued");
|
|
91
|
+
// ContentType ::= OBJECT IDENTIFIER (RFC 5652 §11.1) — validate the value's
|
|
92
|
+
// full syntax (tag AND minimal base-128 OID content), not just the tag, so a
|
|
93
|
+
// truncated / non-minimal subidentifier is rejected here, not at verify time.
|
|
94
|
+
try { asn1.read.oid(asn1.decode(a.values[0])); }
|
|
95
|
+
catch (e) { throw NS.E("cms/bad-content-type-attr", "the content-type signed attribute value must be a valid OBJECT IDENTIFIER", e); }
|
|
96
|
+
} else if (a.type === OID_MESSAGE_DIGEST) {
|
|
97
|
+
md += 1;
|
|
98
|
+
if (a.values.length !== 1) throw NS.E("cms/bad-message-digest-attr", "the message-digest signed attribute must be single-valued");
|
|
99
|
+
// MessageDigest ::= OCTET STRING (RFC 5652 §11.2) — validate the full syntax.
|
|
100
|
+
try { asn1.read.octetString(asn1.decode(a.values[0])); }
|
|
101
|
+
catch (e) { throw NS.E("cms/bad-message-digest-attr", "the message-digest signed attribute value must be an OCTET STRING", e); }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Duplicates are rejected in the loop (the seen-set); here only presence.
|
|
105
|
+
if (ct === 0) throw NS.E("cms/missing-content-type", "signedAttrs must contain a content-type attribute (RFC 5652 §11.1)");
|
|
106
|
+
if (md === 0) throw NS.E("cms/missing-message-digest", "signedAttrs must contain a message-digest attribute (RFC 5652 §11.2)");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// A CertificateChoices / RevocationInfoChoice element, surfaced RAW (its DER +
|
|
110
|
+
// outer tag) rather than recursively parsed — the obsolete CHOICE alternatives
|
|
111
|
+
// (extendedCertificate, attribute certs, otherRevocationInfo) never fail the
|
|
112
|
+
// parse, and a caller re-parses a `certificate`/`CertificateList` element itself.
|
|
113
|
+
function rawElement(item) {
|
|
114
|
+
return { bytes: item.node.bytes, tagClass: item.node.tagClass, tagNumber: item.node.tagNumber };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// RFC 5652 §5.1 — the exact SignedData CMSVersion, computed from the raw
|
|
118
|
+
// CertificateChoices / RevocationInfoChoice outer tags (a certificate `other` is
|
|
119
|
+
// [3], a v2AttrCert [2], a v1AttrCert [1]; a RevocationInfoChoice `other` is [1]),
|
|
120
|
+
// the SignerInfo versions, and whether the content type is id-data.
|
|
121
|
+
function _expectedSignedDataVersion(certificates, crls, signerInfos, eContentType) {
|
|
122
|
+
function ctx(el, n) { return el.tagClass === "context" && el.tagNumber === n; }
|
|
123
|
+
var otherCert = certificates.some(function (c) { return ctx(c, 3); });
|
|
124
|
+
var otherCrl = crls.some(function (c) { return ctx(c, 1); });
|
|
125
|
+
if (otherCert || otherCrl) return 5;
|
|
126
|
+
if (certificates.some(function (c) { return ctx(c, 2); })) return 4; // v2AttrCert [2]
|
|
127
|
+
if (certificates.some(function (c) { return ctx(c, 1); }) || // v1AttrCert [1]
|
|
128
|
+
signerInfos.some(function (si) { return si.version === 3; }) ||
|
|
129
|
+
eContentType !== OID_DATA) return 3;
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// EncapsulatedContentInfo ::= SEQUENCE { eContentType OID,
|
|
134
|
+
// eContent [0] EXPLICIT OCTET STRING OPTIONAL } (RFC 5652 §5.2). Absent eContent
|
|
135
|
+
// is a detached signature (surfaced as null); present is the raw content bytes.
|
|
136
|
+
var ENCAP_CONTENT_INFO = schema.seq([
|
|
137
|
+
schema.field("eContentType", schema.oidLeaf()),
|
|
138
|
+
schema.optional("eContent", schema.octetString(), { tag: 0, explicit: true, emptyCode: "cms/bad-econtent" }),
|
|
139
|
+
], {
|
|
140
|
+
assert: "sequence", arity: { min: 1, max: 2 }, code: "cms/bad-encap-content-info", what: "EncapsulatedContentInfo",
|
|
141
|
+
build: function (m) {
|
|
142
|
+
return {
|
|
143
|
+
eContentType: m.fields.eContentType.value,
|
|
144
|
+
eContent: m.fields.eContent.present ? m.fields.eContent.value : null,
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// IssuerAndSerialNumber ::= SEQUENCE { issuer Name, serialNumber INTEGER }
|
|
150
|
+
// (RFC 5652 §10.2.4). serialNumberHex preserves the DER sign padding.
|
|
151
|
+
var ISSUER_AND_SERIAL = schema.seq([
|
|
152
|
+
schema.field("issuer", NAME),
|
|
153
|
+
schema.field("serialNumber", schema.integerLeaf()),
|
|
154
|
+
], {
|
|
155
|
+
assert: "sequence", arity: { exact: 2 }, code: "cms/bad-issuer-and-serial", what: "IssuerAndSerialNumber",
|
|
156
|
+
build: function (m) {
|
|
157
|
+
return {
|
|
158
|
+
issuer: m.fields.issuer.value.result,
|
|
159
|
+
serialNumber: m.fields.serialNumber.value,
|
|
160
|
+
serialNumberHex: m.fields.serialNumber.node.content.toString("hex"),
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// SignerIdentifier ::= CHOICE { issuerAndSerialNumber IssuerAndSerialNumber,
|
|
166
|
+
// subjectKeyIdentifier [0] IMPLICIT OCTET STRING } (RFC 5652 §5.3) — the arm is
|
|
167
|
+
// disambiguated by tag (universal SEQUENCE vs context [0]).
|
|
168
|
+
var SIGNER_IDENTIFIER = schema.choice([
|
|
169
|
+
{ when: { tagClass: "universal", tagNumber: asn1.TAGS.SEQUENCE }, schema: ISSUER_AND_SERIAL },
|
|
170
|
+
{ when: { tagClass: "context", tagNumber: 0 }, schema: schema.implicitOctetString(0) },
|
|
171
|
+
], { code: "cms/bad-signer-identifier", what: "SignerIdentifier" });
|
|
172
|
+
|
|
173
|
+
// SignerInfo ::= SEQUENCE { version, sid SignerIdentifier, digestAlgorithm,
|
|
174
|
+
// signedAttrs [0] IMPLICIT OPTIONAL, signatureAlgorithm, signature OCTET STRING,
|
|
175
|
+
// unsignedAttrs [1] IMPLICIT OPTIONAL } (RFC 5652 §5.3). signedAttrs/unsignedAttrs
|
|
176
|
+
// are positional optionals (a required signatureAlgorithm sits between them, so
|
|
177
|
+
// they cannot be a trailing block).
|
|
178
|
+
var SIGNER_INFO = schema.seq([
|
|
179
|
+
schema.field("version", SIGNER_VERSION),
|
|
180
|
+
schema.field("sid", SIGNER_IDENTIFIER),
|
|
181
|
+
schema.field("digestAlgorithm", ALGORITHM_IDENTIFIER),
|
|
182
|
+
schema.optional("signedAttrs", schema.implicitSetOf(0, ATTRIBUTE, { min: 1, code: "cms/bad-signed-attrs", what: "signedAttrs" }), { tag: 0 }),
|
|
183
|
+
schema.field("signatureAlgorithm", ALGORITHM_IDENTIFIER),
|
|
184
|
+
schema.field("signature", schema.octetString()),
|
|
185
|
+
schema.optional("unsignedAttrs", schema.implicitSetOf(1, ATTRIBUTE, { min: 1, code: "cms/bad-unsigned-attrs", what: "unsignedAttrs" }), { tag: 1 }),
|
|
186
|
+
], {
|
|
187
|
+
assert: "sequence", code: "cms/bad-signer-info", what: "SignerInfo",
|
|
188
|
+
build: function (m) {
|
|
189
|
+
var version = m.fields.version.value;
|
|
190
|
+
var sidNode = m.fields.sid.node;
|
|
191
|
+
var isSkid = sidNode.tagClass === "context" && sidNode.tagNumber === 0;
|
|
192
|
+
var sid;
|
|
193
|
+
if (isSkid) {
|
|
194
|
+
// RFC 5652 §5.3 — a subjectKeyIdentifier sid forces SignerInfo version 3.
|
|
195
|
+
if (version !== 3) throw NS.E("cms/bad-signer-version", "a subjectKeyIdentifier signer identifier requires SignerInfo version 3");
|
|
196
|
+
sid = { subjectKeyIdentifier: m.fields.sid.value };
|
|
197
|
+
} else {
|
|
198
|
+
// RFC 5652 §5.3 — an issuerAndSerialNumber sid forces SignerInfo version 1.
|
|
199
|
+
if (version !== 1) throw NS.E("cms/bad-signer-version", "an issuerAndSerialNumber signer identifier requires SignerInfo version 1");
|
|
200
|
+
sid = m.fields.sid.value.result;
|
|
201
|
+
}
|
|
202
|
+
var signedAttrs = null, signedAttrsBytes = null;
|
|
203
|
+
if (m.fields.signedAttrs.present) {
|
|
204
|
+
signedAttrs = m.fields.signedAttrs.value.items.map(function (it) { return it.value.result; });
|
|
205
|
+
_checkSignedAttrs(signedAttrs);
|
|
206
|
+
// The raw on-wire signedAttrs bytes (leading 0xA0) so a verifier can re-tag to
|
|
207
|
+
// a universal SET and reproduce the signed hash (RFC 5652 §5.4).
|
|
208
|
+
signedAttrsBytes = m.fields.signedAttrs.node.bytes;
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
version: version,
|
|
212
|
+
sid: sid,
|
|
213
|
+
digestAlgorithm: m.fields.digestAlgorithm.value.result,
|
|
214
|
+
signedAttrs: signedAttrs,
|
|
215
|
+
signedAttrsBytes: signedAttrsBytes,
|
|
216
|
+
signatureAlgorithm: m.fields.signatureAlgorithm.value.result,
|
|
217
|
+
signature: m.fields.signature.value,
|
|
218
|
+
unsignedAttrs: m.fields.unsignedAttrs.present ? m.fields.unsignedAttrs.value.items.map(function (it) { return it.value.result; }) : null,
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// SignedData ::= SEQUENCE { version CMSVersion, digestAlgorithms SET OF,
|
|
224
|
+
// encapContentInfo, certificates [0] IMPLICIT OPTIONAL, crls [1] IMPLICIT
|
|
225
|
+
// OPTIONAL, signerInfos SET OF } (RFC 5652 §5.1). digestAlgorithms and
|
|
226
|
+
// signerInfos are min:0 — a degenerate certs-only SignedData carries neither.
|
|
227
|
+
var SIGNED_DATA = schema.seq([
|
|
228
|
+
schema.field("version", SIGNED_DATA_VERSION),
|
|
229
|
+
schema.field("digestAlgorithms", schema.setOf(ALGORITHM_IDENTIFIER, { min: 0, code: "cms/bad-digest-algorithms", what: "digestAlgorithms" })),
|
|
230
|
+
schema.field("encapContentInfo", ENCAP_CONTENT_INFO),
|
|
231
|
+
schema.optional("certificates", schema.implicitSetOf(0, schema.any(), { min: 1, code: "cms/bad-certificates", what: "certificates" }), { tag: 0 }),
|
|
232
|
+
schema.optional("crls", schema.implicitSetOf(1, schema.any(), { min: 1, code: "cms/bad-crls", what: "crls" }), { tag: 1 }),
|
|
233
|
+
schema.field("signerInfos", schema.setOf(SIGNER_INFO, { min: 0, code: "cms/bad-signer-infos", what: "signerInfos" })),
|
|
234
|
+
], {
|
|
235
|
+
assert: "sequence", code: "cms/bad-signed-data", what: "SignedData",
|
|
236
|
+
build: function (m) {
|
|
237
|
+
var version = m.fields.version.value;
|
|
238
|
+
var encapContentInfo = m.fields.encapContentInfo.value.result;
|
|
239
|
+
var certificates = m.fields.certificates.present ? m.fields.certificates.value.items.map(rawElement) : [];
|
|
240
|
+
var crls = m.fields.crls.present ? m.fields.crls.value.items.map(rawElement) : [];
|
|
241
|
+
var signerInfos = m.fields.signerInfos.value.items.map(function (it) { return it.value.result; });
|
|
242
|
+
|
|
243
|
+
// RFC 5652 §5.3 — signedAttrs MAY be omitted only when the content type is
|
|
244
|
+
// id-data; any other eContentType requires each SignerInfo to carry signedAttrs
|
|
245
|
+
// (so the content-type + message-digest attributes bind the signature).
|
|
246
|
+
if (encapContentInfo.eContentType !== OID_DATA) {
|
|
247
|
+
for (var s = 0; s < signerInfos.length; s++) {
|
|
248
|
+
if (signerInfos[s].signedAttrs === null) throw NS.E("cms/missing-signed-attrs", "a SignerInfo must carry signedAttrs when the content type is not id-data (RFC 5652 §5.3)");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// RFC 5652 §5.3 — when signedAttrs are present, the content-type attribute's
|
|
253
|
+
// value MUST equal the eContentType (a cross-field consistency both parsed
|
|
254
|
+
// here; a mismatch is an internally-inconsistent SignedData).
|
|
255
|
+
for (var si = 0; si < signerInfos.length; si++) {
|
|
256
|
+
var sa = signerInfos[si].signedAttrs;
|
|
257
|
+
if (!sa) continue;
|
|
258
|
+
for (var ai = 0; ai < sa.length; ai++) {
|
|
259
|
+
if (sa[ai].type !== OID_CONTENT_TYPE) continue;
|
|
260
|
+
var ctv = asn1.read.oid(asn1.decode(sa[ai].values[0]));
|
|
261
|
+
if (ctv !== encapContentInfo.eContentType) throw NS.E("cms/content-type-mismatch", "the content-type signed attribute (" + ctv + ") must equal the eContentType (" + encapContentInfo.eContentType + ") (RFC 5652 §5.3)");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// RFC 5652 §5.1 — the SignedData CMSVersion is determined by its contents.
|
|
266
|
+
var expected = _expectedSignedDataVersion(certificates, crls, signerInfos, encapContentInfo.eContentType);
|
|
267
|
+
if (version !== expected) throw NS.E("cms/bad-version", "SignedData version " + version + " does not match its contents (RFC 5652 §5.1 requires v" + expected + ")");
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
version: version,
|
|
271
|
+
digestAlgorithms: m.fields.digestAlgorithms.value.items.map(function (it) { return it.value.result; }),
|
|
272
|
+
encapContentInfo: encapContentInfo,
|
|
273
|
+
certificates: certificates,
|
|
274
|
+
crls: crls,
|
|
275
|
+
signerInfos: signerInfos,
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ContentInfo ::= SEQUENCE { contentType OID, content [0] EXPLICIT ANY DEFINED BY
|
|
281
|
+
// contentType } (RFC 5652 §3). The content is captured raw (explicit(0, any()))
|
|
282
|
+
// and re-dispatched by contentType inside the build: id-signedData walks
|
|
283
|
+
// SIGNED_DATA, the other PKCS#7 types are recognized-and-deferred, unknown OIDs
|
|
284
|
+
// are rejected.
|
|
285
|
+
var CONTENT_INFO = schema.seq([
|
|
286
|
+
schema.field("contentType", schema.oidLeaf()),
|
|
287
|
+
schema.field("content", schema.explicit(0, schema.any(), { code: "cms/not-a-content-info" })),
|
|
288
|
+
], {
|
|
289
|
+
assert: "sequence", arity: { exact: 2 }, code: "cms/not-a-content-info", what: "ContentInfo",
|
|
290
|
+
build: function (m, ctx) {
|
|
291
|
+
var ct = m.fields.contentType.value;
|
|
292
|
+
if (ct === OID_SIGNED_DATA) return schema.walk(SIGNED_DATA, m.fields.content.value, ctx).result;
|
|
293
|
+
if (DEFERRED.has(ct)) {
|
|
294
|
+
throw NS.E("cms/unsupported-content-type", (ctx.oid.name(ct) || ct) + " is recognized but not parsed by this build");
|
|
295
|
+
}
|
|
296
|
+
throw NS.E("cms/unknown-content-type", "unrecognized ContentInfo content type " + ct);
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* @primitive pki.schema.cms.parse
|
|
302
|
+
* @signature pki.schema.cms.parse(input) -> signedData
|
|
303
|
+
* @since 0.1.10
|
|
304
|
+
* @status experimental
|
|
305
|
+
* @spec RFC 5652
|
|
306
|
+
* @related pki.schema.parse, pki.schema.x509.parse
|
|
307
|
+
*
|
|
308
|
+
* Parse a DER `Buffer` or a PEM (`CMS`) string into a structured CMS SignedData:
|
|
309
|
+
* `{ version, digestAlgorithms, encapContentInfo, certificates, crls,
|
|
310
|
+
* signerInfos }`. `encapContentInfo.eContent` is the raw content (or `null` when
|
|
311
|
+
* detached); each SignerInfo carries its raw `signature` and, when present, the
|
|
312
|
+
* on-wire `signedAttrsBytes` for external verification. A ContentInfo whose type
|
|
313
|
+
* is not `id-signedData` throws `cms/unsupported-content-type` (a recognized
|
|
314
|
+
* PKCS#7 type) or `cms/unknown-content-type`; a malformed structure throws a typed
|
|
315
|
+
* `CmsError` (`cms/*`) and a leaf-level codec fault surfaces as `asn1/*`.
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* var cms = pki.schema.cms.parse(der);
|
|
319
|
+
* cms.signerInfos[0].sid.serialNumberHex; // -> "0a1b..."
|
|
320
|
+
* cms.encapContentInfo.eContent; // -> Buffer | null (detached)
|
|
321
|
+
*/
|
|
322
|
+
var parse = pkix.makeParser({ pemLabel: "CMS", PemError: PemError, ErrorClass: CmsError, prefix: "cms", what: "CMS ContentInfo", topSchema: CONTENT_INFO, ns: NS });
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @primitive pki.schema.cms.pemDecode
|
|
326
|
+
* @signature pki.schema.cms.pemDecode(text, label?) -> Buffer
|
|
327
|
+
* @since 0.1.10
|
|
328
|
+
* @status experimental
|
|
329
|
+
* @spec RFC 7468, RFC 5652
|
|
330
|
+
* @related pki.schema.cms.parse
|
|
331
|
+
*
|
|
332
|
+
* Extract the DER bytes from a PEM CMS block (default label `CMS`). Throws
|
|
333
|
+
* `PemError` on a missing / mismatched envelope or a non-base64 body.
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* var der = pki.schema.cms.pemDecode(pemText);
|
|
337
|
+
*/
|
|
338
|
+
function pemDecode(text, label) { return pkix.pemDecode(text, label || "CMS", PemError); }
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @primitive pki.schema.cms.pemEncode
|
|
342
|
+
* @signature pki.schema.cms.pemEncode(der, label?) -> string
|
|
343
|
+
* @since 0.1.10
|
|
344
|
+
* @status experimental
|
|
345
|
+
* @spec RFC 7468
|
|
346
|
+
* @related pki.schema.cms.pemDecode
|
|
347
|
+
*
|
|
348
|
+
* Wrap DER bytes in a PEM CMS envelope (default label `CMS`).
|
|
349
|
+
*
|
|
350
|
+
* @example
|
|
351
|
+
* var pem = pki.schema.cms.pemEncode(der);
|
|
352
|
+
*/
|
|
353
|
+
function pemEncode(der, label) { return pkix.pemEncode(der, label || "CMS", PemError); }
|
|
354
|
+
|
|
355
|
+
// A CMS ContentInfo root is the only registered structure whose root leads with an
|
|
356
|
+
// OBJECT IDENTIFIER child: a SEQUENCE of exactly 2 whose first child is an OID and
|
|
357
|
+
// second a context [0] constructed wrapper. x509/crl/csr lead with a tbs SEQUENCE
|
|
358
|
+
// and pkcs8 with an INTEGER, so the detectors are mutually exclusive regardless of
|
|
359
|
+
// registry order.
|
|
360
|
+
function matches(root) {
|
|
361
|
+
var TAGS = asn1.TAGS;
|
|
362
|
+
if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE) return false;
|
|
363
|
+
var k = root.children;
|
|
364
|
+
if (!k || k.length !== 2) return false;
|
|
365
|
+
if (!(k[0].tagClass === "universal" && k[0].tagNumber === TAGS.OBJECT_IDENTIFIER)) return false;
|
|
366
|
+
if (!(k[1].tagClass === "context" && k[1].tagNumber === 0 && k[1].children)) return false;
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = {
|
|
371
|
+
parse: parse,
|
|
372
|
+
pemDecode: pemDecode,
|
|
373
|
+
pemEncode: pemEncode,
|
|
374
|
+
matches: matches,
|
|
375
|
+
};
|
package/lib/schema-crl.js
CHANGED
|
@@ -46,7 +46,7 @@ var OID_INVALIDITY_DATE = oid.byName("invalidityDate");
|
|
|
46
46
|
|
|
47
47
|
// The crl error namespace the schema engine walks under. Shared PKIX sub-schemas
|
|
48
48
|
// are instantiated here under crl/* so a structural fault reports a crl code.
|
|
49
|
-
var NS =
|
|
49
|
+
var NS = pkix.makeNS("crl", CrlError, oid);
|
|
50
50
|
|
|
51
51
|
var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
|
|
52
52
|
var NAME = pkix.name(NS);
|
|
@@ -56,12 +56,7 @@ var TIME = schema.time(NS);
|
|
|
56
56
|
// CRL Version ::= INTEGER { v1(0), v2(1) } — a BARE INTEGER (not [0] EXPLICIT).
|
|
57
57
|
// A CRL is at most v2; reject an explicit v1 (DER forbids the default) and any
|
|
58
58
|
// value >= 2. Do NOT reuse the certificate readVersion (it maps 2 -> v3).
|
|
59
|
-
var CRL_VERSION =
|
|
60
|
-
var v = asn1.read.integer(n);
|
|
61
|
-
if (v === 1n) return 2;
|
|
62
|
-
if (v === 0n) throw NS.E("crl/bad-version", "DER forbids explicitly encoding the default version v1");
|
|
63
|
-
throw NS.E("crl/bad-version", "unsupported CRL version " + v.toString() + " (a CRL is at most v2)");
|
|
64
|
-
});
|
|
59
|
+
var CRL_VERSION = pkix.versionReader(NS, { "1": 2 });
|
|
65
60
|
|
|
66
61
|
// The three cheap, high-value CRL extension values are decoded from their raw
|
|
67
62
|
// extnValue octets (RFC 5280 §5.2/§5.3); GeneralNames-based extensions
|
package/lib/schema-csr.js
CHANGED
|
@@ -40,7 +40,7 @@ var PemError = frameworkError.PemError;
|
|
|
40
40
|
|
|
41
41
|
// The csr error namespace the schema engine walks under. Shared PKIX sub-schemas
|
|
42
42
|
// are instantiated here under csr/* so a structural fault reports a csr code.
|
|
43
|
-
var NS =
|
|
43
|
+
var NS = pkix.makeNS("csr", CsrError, oid);
|
|
44
44
|
|
|
45
45
|
var NAME = pkix.name(NS);
|
|
46
46
|
var SPKI = pkix.spki(NS);
|
|
@@ -50,30 +50,12 @@ var SPKI = pkix.spki(NS);
|
|
|
50
50
|
// readers, which reject 0 as the DER-forbidden default. Surface v1 as 1. A
|
|
51
51
|
// cert-shaped [0] EXPLICIT wrapper at this position makes asn1.read.integer throw
|
|
52
52
|
// asn1/* (fail-closed).
|
|
53
|
-
var CSR_VERSION =
|
|
54
|
-
var v = asn1.read.integer(n);
|
|
55
|
-
if (v === 0n) return 1;
|
|
56
|
-
throw NS.E("csr/bad-version", "unsupported CSR version " + v.toString() + " (PKCS#10 requires v1)");
|
|
57
|
-
});
|
|
53
|
+
var CSR_VERSION = pkix.versionReader(NS, { "0": 1 });
|
|
58
54
|
|
|
59
|
-
// Attribute ::= SEQUENCE { type
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
var ATTRIBUTE = schema.seq([
|
|
64
|
-
schema.field("type", schema.oidLeaf()),
|
|
65
|
-
schema.field("values", schema.setOf(schema.any(), { assert: "set", min: 1, code: "csr/bad-attribute-values", what: "attribute values" })),
|
|
66
|
-
], {
|
|
67
|
-
assert: "sequence", arity: { exact: 2 }, code: "csr/bad-attribute", what: "Attribute",
|
|
68
|
-
build: function (m, ctx) {
|
|
69
|
-
var t = m.fields.type.value;
|
|
70
|
-
return {
|
|
71
|
-
type: t,
|
|
72
|
-
name: ctx.oid.name(t) || null,
|
|
73
|
-
values: m.fields.values.value.items.map(function (it) { return it.node.bytes; }),
|
|
74
|
-
};
|
|
75
|
-
},
|
|
76
|
-
});
|
|
55
|
+
// Attribute ::= SEQUENCE { type OID, values SET OF ANY } — the shared pkix factory
|
|
56
|
+
// under the csr NS (values raw DER, SET SIZE(1..MAX), no uniqueness). The same
|
|
57
|
+
// factory serves the PKCS#8 private-key attributes.
|
|
58
|
+
var ATTRIBUTE = pkix.attribute(NS);
|
|
77
59
|
|
|
78
60
|
// CertificationRequestInfo ::= SEQUENCE { version, subject Name,
|
|
79
61
|
// subjectPKInfo SubjectPublicKeyInfo, attributes [0] IMPLICIT SET OF Attribute }.
|
package/lib/schema-engine.js
CHANGED
|
@@ -97,6 +97,13 @@ function integerLeaf() { return { kind: "leaf", read: asn1.read.integer }; }
|
|
|
97
97
|
function boolean() { return { kind: "leaf", read: asn1.read.boolean }; }
|
|
98
98
|
function octetString() { return { kind: "leaf", read: asn1.read.octetString }; }
|
|
99
99
|
function bitString() { return { kind: "leaf", read: function (n) { var b = asn1.read.bitString(n); return { unusedBits: b.unusedBits, bytes: b.bytes }; } }; }
|
|
100
|
+
// A [tag] IMPLICIT BIT STRING leaf (context-class primitive) — the primitive-leaf
|
|
101
|
+
// counterpart to implicitSetOf, for e.g. the PKCS#8 publicKey [1] (RFC 5958 §2).
|
|
102
|
+
function implicitBitString(tag) { return { kind: "leaf", read: function (n) { var b = asn1.read.bitStringImplicit(n, tag); return { unusedBits: b.unusedBits, bytes: b.bytes }; } }; }
|
|
103
|
+
// A [tag] IMPLICIT OCTET STRING leaf (context-class primitive) — the sibling of
|
|
104
|
+
// implicitBitString, for e.g. the CMS SignerIdentifier subjectKeyIdentifier [0]
|
|
105
|
+
// (RFC 5652 §5.3). Yields the raw content bytes.
|
|
106
|
+
function implicitOctetString(tag) { return { kind: "leaf", read: function (n) { return asn1.read.octetStringImplicit(n, tag); } }; }
|
|
100
107
|
function any() { return { kind: "any" }; }
|
|
101
108
|
function decode(fn) { return { kind: "decode", fn: fn }; }
|
|
102
109
|
|
|
@@ -368,7 +375,8 @@ module.exports = {
|
|
|
368
375
|
seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, implicitSetOf: implicitSetOf, choice: choice,
|
|
369
376
|
// leaves
|
|
370
377
|
oidLeaf: oidLeaf, integerLeaf: integerLeaf, boolean: boolean, octetString: octetString,
|
|
371
|
-
bitString: bitString,
|
|
378
|
+
bitString: bitString, implicitBitString: implicitBitString, implicitOctetString: implicitOctetString,
|
|
379
|
+
any: any, decode: decode, time: time,
|
|
372
380
|
// engine
|
|
373
381
|
walk: walk,
|
|
374
382
|
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.schema.pkcs8
|
|
6
|
+
* @nav Schema
|
|
7
|
+
* @title PKCS#8
|
|
8
|
+
* @order 140
|
|
9
|
+
* @slug pkcs8
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
12
|
+
* PKCS#8 private-key handling per RFC 5208 §5 (PrivateKeyInfo) and RFC 5958 §2
|
|
13
|
+
* (OneAsymmetricKey). `parse` turns a DER or PEM (`PRIVATE KEY`) key into a
|
|
14
|
+
* structured object: version, the private-key algorithm identifier, the raw
|
|
15
|
+
* private-key bytes, the optional attributes, and — for a v2 OneAsymmetricKey —
|
|
16
|
+
* the optional public key. It composes the same schema engine and shared PKIX
|
|
17
|
+
* sub-schemas (AlgorithmIdentifier, Attribute) the other parsers use.
|
|
18
|
+
*
|
|
19
|
+
* A PKCS#8 key is a container, not a signed structure: it has no signature, no
|
|
20
|
+
* distinguished name, and no to-be-signed region. The private-key OCTET STRING
|
|
21
|
+
* content is kept raw — the algorithm-specific inner key (an RSAPrivateKey, an
|
|
22
|
+
* ECPrivateKey, a CurvePrivateKey) is decoded by the caller using the surfaced
|
|
23
|
+
* algorithm OID, so an unknown or future key type never fails the parse. An
|
|
24
|
+
* `ENCRYPTED PRIVATE KEY` (EncryptedPrivateKeyInfo, RFC 5958 §3) is recognized
|
|
25
|
+
* and surfaced with its encryption algorithm and raw ciphertext; decrypting it
|
|
26
|
+
* needs a passphrase and is out of scope for structural parsing.
|
|
27
|
+
*
|
|
28
|
+
* @card
|
|
29
|
+
* Parse DER / PEM PKCS#8 private keys (RFC 5208 / 5958) into structured,
|
|
30
|
+
* validated fields — algorithm, raw key bytes, attributes, optional public key,
|
|
31
|
+
* fail-closed. Encrypted keys are recognized, not decrypted.
|
|
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 Pkcs8Error = frameworkError.Pkcs8Error;
|
|
41
|
+
var PemError = frameworkError.PemError;
|
|
42
|
+
|
|
43
|
+
// The pkcs8 error namespace the schema engine walks under.
|
|
44
|
+
var NS = pkix.makeNS("pkcs8", Pkcs8Error, oid);
|
|
45
|
+
|
|
46
|
+
var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
|
|
47
|
+
var ATTRIBUTE = pkix.attribute(NS);
|
|
48
|
+
|
|
49
|
+
// version ::= INTEGER { v1(0), v2(1) } — 0 and 1 are both LEGAL (the divergence
|
|
50
|
+
// from every other reader; cert/CRL reject 0, CSR accepts only 0). Surface as the
|
|
51
|
+
// RFC's vN number.
|
|
52
|
+
var PKCS8_VERSION = pkix.versionReader(NS, { "0": 1, "1": 2 });
|
|
53
|
+
|
|
54
|
+
// PrivateKeyInfo ::= SEQUENCE { version, privateKeyAlgorithm AlgorithmIdentifier,
|
|
55
|
+
// privateKey OCTET STRING, attributes [0] IMPLICIT SET OF Attribute OPTIONAL,
|
|
56
|
+
// publicKey [1] IMPLICIT BIT STRING OPTIONAL (v2 only) }. The two trailing
|
|
57
|
+
// context fields are modeled with `trailing` so they must appear in ascending tag
|
|
58
|
+
// order and an unknown trailing context tag is rejected (not bled into a field).
|
|
59
|
+
var PRIVATE_KEY_INFO = schema.seq([
|
|
60
|
+
schema.field("version", PKCS8_VERSION),
|
|
61
|
+
schema.field("privateKeyAlgorithm", ALGORITHM_IDENTIFIER),
|
|
62
|
+
schema.field("privateKey", schema.octetString()),
|
|
63
|
+
schema.trailing([
|
|
64
|
+
{ tag: 0, name: "attributes", schema: schema.implicitSetOf(0, ATTRIBUTE, { min: 0, code: "pkcs8/bad-attributes", what: "attributes" }) },
|
|
65
|
+
{ tag: 1, name: "publicKey", schema: schema.implicitBitString(1) },
|
|
66
|
+
], { minTag: 0, maxTag: 1, unexpectedCode: "pkcs8/not-a-private-key-info", orderCode: "pkcs8/bad-order" }),
|
|
67
|
+
], {
|
|
68
|
+
assert: "sequence", arity: { min: 3 }, code: "pkcs8/not-a-private-key-info", what: "PrivateKeyInfo",
|
|
69
|
+
build: function (m) {
|
|
70
|
+
var version = m.fields.version.value; // 1 (v1) | 2 (v2)
|
|
71
|
+
var hasPublicKey = m.fields.publicKey.present;
|
|
72
|
+
// RFC 5958 §2 — publicKey present <=> version is v2. Enforce both directions.
|
|
73
|
+
if (hasPublicKey && version !== 2) throw NS.E("pkcs8/bad-version", "a [1] publicKey is permitted only in a v2 OneAsymmetricKey");
|
|
74
|
+
if (!hasPublicKey && version === 2) throw NS.E("pkcs8/bad-version", "a v2 OneAsymmetricKey must carry a [1] publicKey");
|
|
75
|
+
return {
|
|
76
|
+
version: version,
|
|
77
|
+
privateKeyAlgorithm: m.fields.privateKeyAlgorithm.value.result,
|
|
78
|
+
privateKey: m.fields.privateKey.value, // raw OCTET STRING content (the inner key DER)
|
|
79
|
+
attributes: m.fields.attributes.present ? m.fields.attributes.value.items.map(function (it) { return it.value.result; }) : [],
|
|
80
|
+
publicKey: hasPublicKey ? m.fields.publicKey.value : null,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// EncryptedPrivateKeyInfo ::= SEQUENCE { encryptionAlgorithm AlgorithmIdentifier,
|
|
86
|
+
// encryptedData OCTET STRING } (RFC 5958 §3). Recognized and surfaced; the raw
|
|
87
|
+
// ciphertext is kept for a later decryption layer (PBES2/PBKDF2 + a passphrase).
|
|
88
|
+
var ENCRYPTED_PRIVATE_KEY_INFO = schema.seq([
|
|
89
|
+
schema.field("encryptionAlgorithm", ALGORITHM_IDENTIFIER),
|
|
90
|
+
schema.field("encryptedData", schema.octetString()),
|
|
91
|
+
], {
|
|
92
|
+
assert: "sequence", arity: { exact: 2 }, code: "pkcs8/not-an-encrypted-private-key-info", what: "EncryptedPrivateKeyInfo",
|
|
93
|
+
build: function (m) {
|
|
94
|
+
return {
|
|
95
|
+
encryptionAlgorithm: m.fields.encryptionAlgorithm.value.result,
|
|
96
|
+
encryptedData: m.fields.encryptedData.value, // raw ciphertext
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @primitive pki.schema.pkcs8.parse
|
|
103
|
+
* @signature pki.schema.pkcs8.parse(input) -> privateKey
|
|
104
|
+
* @since 0.1.9
|
|
105
|
+
* @status experimental
|
|
106
|
+
* @spec RFC 5208, RFC 5958
|
|
107
|
+
* @related pki.schema.parse, pki.schema.x509.parse
|
|
108
|
+
*
|
|
109
|
+
* Parse a DER `Buffer` or a PEM (`PRIVATE KEY`) string into a structured PKCS#8
|
|
110
|
+
* key: `{ version, privateKeyAlgorithm, privateKey, attributes, publicKey }`. The
|
|
111
|
+
* `privateKey` is the raw OCTET STRING content (the algorithm-specific inner key,
|
|
112
|
+
* decoded by the caller using `privateKeyAlgorithm.oid`); `publicKey` is `null`
|
|
113
|
+
* for a v1 key. A malformed PrivateKeyInfo throws a typed `Pkcs8Error` (`pkcs8/*`)
|
|
114
|
+
* and a leaf-level codec fault surfaces as `asn1/*`.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* var key = pki.schema.pkcs8.parse(der);
|
|
118
|
+
* key.privateKeyAlgorithm.oid; // -> "1.3.101.112" (Ed25519)
|
|
119
|
+
* key.privateKey; // -> Buffer (the inner key encoding)
|
|
120
|
+
*/
|
|
121
|
+
var parse = pkix.makeParser({ pemLabel: "PRIVATE KEY", PemError: PemError, ErrorClass: Pkcs8Error, prefix: "pkcs8", what: "private key", topSchema: PRIVATE_KEY_INFO, ns: NS });
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @primitive pki.schema.pkcs8.parseEncrypted
|
|
125
|
+
* @signature pki.schema.pkcs8.parseEncrypted(input) -> encrypted
|
|
126
|
+
* @since 0.1.9
|
|
127
|
+
* @status experimental
|
|
128
|
+
* @spec RFC 5958, RFC 5208
|
|
129
|
+
* @related pki.schema.pkcs8.parse
|
|
130
|
+
*
|
|
131
|
+
* Parse a DER `Buffer` or a PEM (`ENCRYPTED PRIVATE KEY`) string into an
|
|
132
|
+
* EncryptedPrivateKeyInfo: `{ encryptionAlgorithm, encryptedData }`. The
|
|
133
|
+
* ciphertext is surfaced raw; decrypting it (PBES2/PBKDF2 + a passphrase) is a
|
|
134
|
+
* separate concern from structural validation.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* var enc = pki.schema.pkcs8.parseEncrypted(der);
|
|
138
|
+
* enc.encryptionAlgorithm.oid; // -> "1.2.840.113549.1.5.13" (PBES2)
|
|
139
|
+
*/
|
|
140
|
+
var parseEncrypted = pkix.makeParser({ pemLabel: "ENCRYPTED PRIVATE KEY", PemError: PemError, ErrorClass: Pkcs8Error, prefix: "pkcs8", what: "encrypted private key", topSchema: ENCRYPTED_PRIVATE_KEY_INFO, ns: NS });
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @primitive pki.schema.pkcs8.pemDecode
|
|
144
|
+
* @signature pki.schema.pkcs8.pemDecode(text, label?) -> Buffer
|
|
145
|
+
* @since 0.1.9
|
|
146
|
+
* @status experimental
|
|
147
|
+
* @spec RFC 7468, RFC 5958
|
|
148
|
+
* @related pki.schema.pkcs8.parse
|
|
149
|
+
*
|
|
150
|
+
* Extract the DER bytes from a PEM private-key block (default label `PRIVATE
|
|
151
|
+
* KEY`). Throws `PemError` on a missing / mismatched envelope or a non-base64
|
|
152
|
+
* body.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* var der = pki.schema.pkcs8.pemDecode(pemText);
|
|
156
|
+
*/
|
|
157
|
+
function pemDecode(text, label) { return pkix.pemDecode(text, label || "PRIVATE KEY", PemError); }
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @primitive pki.schema.pkcs8.pemEncode
|
|
161
|
+
* @signature pki.schema.pkcs8.pemEncode(der, label?) -> string
|
|
162
|
+
* @since 0.1.9
|
|
163
|
+
* @status experimental
|
|
164
|
+
* @spec RFC 7468
|
|
165
|
+
* @related pki.schema.pkcs8.pemDecode
|
|
166
|
+
*
|
|
167
|
+
* Wrap DER bytes in a PEM private-key envelope (default label `PRIVATE KEY`).
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* var pem = pki.schema.pkcs8.pemEncode(der);
|
|
171
|
+
*/
|
|
172
|
+
function pemEncode(der, label) { return pkix.pemEncode(der, label || "PRIVATE KEY", PemError); }
|
|
173
|
+
|
|
174
|
+
// A PKCS#8 PrivateKeyInfo root is a SEQUENCE whose first child is a universal
|
|
175
|
+
// INTEGER (version) and third child a universal OCTET STRING (privateKey) — a
|
|
176
|
+
// shape the signed-envelope trio (whose children[0] is a SEQUENCE and children[2]
|
|
177
|
+
// a BIT STRING) can never present, so the detectors are mutually exclusive
|
|
178
|
+
// regardless of registry order. Trailing context fields are [0]/[1] in ascending
|
|
179
|
+
// order.
|
|
180
|
+
function matches(root) {
|
|
181
|
+
var TAGS = asn1.TAGS;
|
|
182
|
+
if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE) return false;
|
|
183
|
+
var k = root.children;
|
|
184
|
+
if (!k || k.length < 3 || k.length > 5) return false;
|
|
185
|
+
if (!(k[0].tagClass === "universal" && k[0].tagNumber === TAGS.INTEGER)) return false;
|
|
186
|
+
if (!(k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE)) return false;
|
|
187
|
+
if (!(k[2].tagClass === "universal" && k[2].tagNumber === TAGS.OCTET_STRING)) return false;
|
|
188
|
+
var last = -1;
|
|
189
|
+
for (var i = 3; i < k.length; i++) {
|
|
190
|
+
if (k[i].tagClass !== "context" || k[i].tagNumber <= last || k[i].tagNumber > 1) return false;
|
|
191
|
+
last = k[i].tagNumber;
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// NOTE: there is intentionally no `matchesEncrypted` detector. An
|
|
197
|
+
// EncryptedPrivateKeyInfo is a SEQUENCE { SEQUENCE, OCTET STRING } — a shape it
|
|
198
|
+
// shares with a PKCS#1 DigestInfo and other AlgorithmIdentifier-plus-octets
|
|
199
|
+
// structures — so it cannot be classified from structure alone. The orchestrator
|
|
200
|
+
// does not auto-route it; an operator who knows a key is encrypted (e.g. from an
|
|
201
|
+
// 'ENCRYPTED PRIVATE KEY' PEM label) calls parseEncrypted directly. A structural
|
|
202
|
+
// auto-route can return once a validated encryption-algorithm discriminator (the
|
|
203
|
+
// PBES layer) exists.
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
parse: parse,
|
|
207
|
+
parseEncrypted: parseEncrypted,
|
|
208
|
+
pemDecode: pemDecode,
|
|
209
|
+
pemEncode: pemEncode,
|
|
210
|
+
matches: matches,
|
|
211
|
+
};
|
package/lib/schema-pkix.js
CHANGED
|
@@ -105,6 +105,30 @@ var DN_SHORT = {
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
// AlgorithmIdentifier ::= SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY OPTIONAL }
|
|
108
|
+
// The error/OID namespace every format module walks its schema under:
|
|
109
|
+
// { prefix, E:(code,message,cause)=>new ErrorClass(...), oid }. Factored so a
|
|
110
|
+
// format declares it in one line instead of repeating the error-constructor
|
|
111
|
+
// closure (a caller that never passes a cause is unaffected — withCause ignores
|
|
112
|
+
// an undefined third arg).
|
|
113
|
+
function makeNS(prefix, ErrorClass, oidModule) {
|
|
114
|
+
return { prefix: prefix, E: function (code, message, cause) { return new ErrorClass(code, message, cause); }, oid: oidModule };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A bounded universal-INTEGER version reader. `accept` maps each legal wire value
|
|
118
|
+
// (as a decimal string) to its surfaced version number; any other value is a
|
|
119
|
+
// <prefix>/bad-version fault. The one genuine per-format divergence — the cert
|
|
120
|
+
// rejects 0 and maps 1->2/2->3, a CRL accepts only 1->2, a CSR only 0->1, a PKCS#8
|
|
121
|
+
// 0->1/1->2 — is expressed purely as the accept map (RFC 5280 §4.1.2.1 / §5.1.2.1,
|
|
122
|
+
// RFC 2986 §4.1, RFC 5958 §2). read.integer is strict, so an ENUMERATED-tagged
|
|
123
|
+
// version is rejected at the leaf (asn1/*).
|
|
124
|
+
function versionReader(ns, accept) {
|
|
125
|
+
return schema.decode(function (n) {
|
|
126
|
+
var key = asn1.read.integer(n).toString();
|
|
127
|
+
if (Object.prototype.hasOwnProperty.call(accept, key)) return accept[key];
|
|
128
|
+
throw ns.E(ns.prefix + "/bad-version", "unsupported version " + key);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
108
132
|
function algorithmIdentifier(ns) {
|
|
109
133
|
return schema.seq([
|
|
110
134
|
schema.field("algorithm", schema.oidLeaf()),
|
|
@@ -242,6 +266,28 @@ function spki(ns) {
|
|
|
242
266
|
});
|
|
243
267
|
}
|
|
244
268
|
|
|
269
|
+
// Attribute ::= SEQUENCE { type OBJECT IDENTIFIER, values SET OF AttributeValue }.
|
|
270
|
+
// AttributeValue is ANY, kept as raw DER (node.bytes) — an unrecognized attribute
|
|
271
|
+
// type never fails the parse. values is SET SIZE (1..MAX): an empty SET is
|
|
272
|
+
// rejected; there is NO SET-OF uniqueness. Shared by the CSR (requested
|
|
273
|
+
// attributes, RFC 2986 §4.1) and PKCS#8 (private-key attributes, RFC 5958 §2).
|
|
274
|
+
function attribute(ns) {
|
|
275
|
+
return schema.seq([
|
|
276
|
+
schema.field("type", schema.oidLeaf()),
|
|
277
|
+
schema.field("values", schema.setOf(schema.any(), { assert: "set", min: 1, code: ns.prefix + "/bad-attribute-values", what: "attribute values" })),
|
|
278
|
+
], {
|
|
279
|
+
assert: "sequence", arity: { exact: 2 }, code: ns.prefix + "/bad-attribute", what: "Attribute",
|
|
280
|
+
build: function (m, ctx) {
|
|
281
|
+
var t = m.fields.type.value;
|
|
282
|
+
return {
|
|
283
|
+
type: t,
|
|
284
|
+
name: ctx.oid.name(t) || null,
|
|
285
|
+
values: m.fields.values.value.items.map(function (it) { return it.node.bytes; }),
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
245
291
|
// Certificate, CertificateList, and CertificationRequest share one outer shape:
|
|
246
292
|
// SEQUENCE { toBeSigned SEQUENCE, signatureAlgorithm, signatureValue }. This
|
|
247
293
|
// returns the first element (the to-be-signed info) when `root` is that
|
|
@@ -304,6 +350,8 @@ module.exports = {
|
|
|
304
350
|
coerceToDer: coerceToDer,
|
|
305
351
|
decodeRoot: decodeRoot,
|
|
306
352
|
runParse: runParse,
|
|
353
|
+
makeNS: makeNS,
|
|
354
|
+
versionReader: versionReader,
|
|
307
355
|
DN_SHORT: DN_SHORT,
|
|
308
356
|
algorithmIdentifier: algorithmIdentifier,
|
|
309
357
|
spki: spki,
|
|
@@ -314,6 +362,7 @@ module.exports = {
|
|
|
314
362
|
attributeTypeAndValue: attributeTypeAndValue,
|
|
315
363
|
relativeDistinguishedName: relativeDistinguishedName,
|
|
316
364
|
name: name,
|
|
365
|
+
attribute: attribute,
|
|
317
366
|
extension: extension,
|
|
318
367
|
extensions: extensions,
|
|
319
368
|
};
|
package/lib/schema-x509.js
CHANGED
|
@@ -80,7 +80,7 @@ function pemEncode(der, label) { return pkix.pemEncode(der, label, PemError); }
|
|
|
80
80
|
// sub-schemas (AlgorithmIdentifier, Name, Extension) live in lib/pkix-schema.js
|
|
81
81
|
// as ns-parameterized factories so crl.js / cms.js reuse them while emitting
|
|
82
82
|
// their own crl/*, cms/* codes; here they are instantiated under this NS.
|
|
83
|
-
var NS =
|
|
83
|
+
var NS = pkix.makeNS("x509", CertificateError, oid);
|
|
84
84
|
|
|
85
85
|
var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
|
|
86
86
|
var NAME = pkix.name(NS);
|
|
@@ -103,15 +103,9 @@ var EXTENSIONS = pkix.extensions(NS);
|
|
|
103
103
|
// Version ::= INTEGER { v1(0), v2(1), v3(2) }, [0] EXPLICIT DEFAULT v1. Read as a
|
|
104
104
|
// BigInt so an out-of-range value can't coerce to a float; reject an explicitly-
|
|
105
105
|
// encoded v1 (DER forbids encoding the DEFAULT).
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (v === 0n) throw ns.E(ns.prefix + "/bad-version", "DER forbids explicitly encoding the default version v1");
|
|
110
|
-
if (v === 1n) return 2;
|
|
111
|
-
if (v === 2n) return 3;
|
|
112
|
-
throw ns.E(ns.prefix + "/bad-version", "unsupported certificate version " + v.toString());
|
|
113
|
-
});
|
|
114
|
-
}
|
|
106
|
+
// version ::= INTEGER { v1(0), v2(1), v3(2) } inside a [0] EXPLICIT DEFAULT v1 —
|
|
107
|
+
// DER forbids encoding the default, so 0 is rejected; 1->v2, 2->v3.
|
|
108
|
+
var CERTIFICATE_VERSION = pkix.versionReader(NS, { "1": 2, "2": 3 });
|
|
115
109
|
|
|
116
110
|
// TBSCertificate ::= SEQUENCE { version [0] EXPLICIT DEFAULT v1, serialNumber
|
|
117
111
|
// INTEGER, signature AlgorithmIdentifier, issuer Name, validity Validity,
|
|
@@ -120,7 +114,7 @@ function readVersion(ns) {
|
|
|
120
114
|
// EXPLICIT OPTIONAL }. The trailing fields are at-most-once, in increasing tag
|
|
121
115
|
// order (the engine enforces it); only extensions are surfaced.
|
|
122
116
|
var CERTIFICATE_TBS = schema.seq([
|
|
123
|
-
schema.optional("version",
|
|
117
|
+
schema.optional("version", CERTIFICATE_VERSION, { tag: 0, explicit: true, emptyCode: "x509/bad-version", default: 1 }),
|
|
124
118
|
schema.field("serialNumber", schema.integerLeaf()),
|
|
125
119
|
schema.field("signature", ALGORITHM_IDENTIFIER),
|
|
126
120
|
schema.field("issuer", NAME),
|
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:e5089c51-5287-4fe7-bbfc-520b07cbb2f2",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-05T18:32:46.745Z",
|
|
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.10",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
25
|
+
"version": "0.1.10",
|
|
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.10",
|
|
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.10",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|