@blamejs/pki 0.1.21 → 0.1.23

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/lib/schema-all.js CHANGED
@@ -11,13 +11,13 @@
11
11
  *
12
12
  * @intro
13
13
  * The schema family: a declarative ASN.1 structure-schema engine and the
14
- * per-format parsers built on it. Every format from X.509 certificates
14
+ * per-format parsers built on it. Every format -- from X.509 certificates
15
15
  * and CRLs through CMS, OCSP, timestamps, and PKCS#12 stores; `all()`
16
- * enumerates the registered set is a member that COMPOSES the
16
+ * enumerates the registered set -- is a member that COMPOSES the
17
17
  * shared engine and the shared PKIX sub-schemas (AlgorithmIdentifier, Name,
18
- * Extension), so a structural rule bounds-checked positional reads,
18
+ * Extension), so a structural rule -- bounds-checked positional reads,
19
19
  * optional / tagged field ordering, SET-OF uniqueness, fail-closed typed
20
- * errors is defined once in the engine and no format can reintroduce the
20
+ * errors -- is defined once in the engine and no format can reintroduce the
21
21
  * class of bug it prevents.
22
22
  *
23
23
  * `parse` is the orchestrator: hand it DER (or PEM) and it detects which
@@ -26,7 +26,7 @@
26
26
  * and `all()` enumerates the registered formats.
27
27
  *
28
28
  * @card
29
- * One declarative schema engine; every PKI format (X.509, CRL, ) is a
29
+ * One declarative schema engine; every PKI format (X.509, CRL, ...) is a
30
30
  * member composed on it. Detect-and-parse DER, or call a format directly.
31
31
  */
32
32
 
@@ -43,6 +43,11 @@ var tsp = require("./schema-tsp");
43
43
  var crmf = require("./schema-crmf");
44
44
  var cmp = require("./schema-cmp");
45
45
  var attrcert = require("./schema-attrcert");
46
+ // smime is a COMPANION decoder for CMS signed-attribute values, NOT a top-level
47
+ // format -- it has no self-describing DER root (an ESS attribute value is a bare
48
+ // SEQUENCE) so it is deliberately absent from FORMATS / the detect-and-route
49
+ // `parse`; it is reached only by explicit OID dispatch on a CMS attribute.
50
+ var smime = require("./schema-smime");
46
51
  var frameworkError = require("./framework-error");
47
52
 
48
53
  var SchemaError = frameworkError.SchemaError;
@@ -56,14 +61,14 @@ var PemError = frameworkError.PemError;
56
61
  // fails typed inside that format's own parse.
57
62
  var ENTRY = { pemLabel: null, PemError: PemError, ErrorClass: SchemaError, prefix: "schema", what: "input", ber: true };
58
63
 
59
- // REGISTRY each format is declarative data: a name, a `detect(root)` that
64
+ // REGISTRY -- each format is declarative data: a name, a `detect(root)` that
60
65
  // recognizes the decoded DER root as this format's outer structure, and the
61
66
  // member's own `parse`. Adding a format is a table entry, not new dispatch
62
67
  // logic. Order matters: the most specific detector wins, so a new member is
63
68
  // inserted ahead of any more-permissive one.
64
69
  var FORMATS = [
65
70
  {
66
- // CMS ContentInfo (RFC 5652 §3) the only registered root that leads with an
71
+ // CMS ContentInfo (RFC 5652 sec. 3) -- the only registered root that leads with an
67
72
  // OBJECT IDENTIFIER child (a SEQUENCE of exactly 2: contentType OID + a
68
73
  // context [0] EXPLICIT content wrapper). Disjoint from the INTEGER-first pkcs8
69
74
  // and the tbs-SEQUENCE-first signed-envelope trio, so it detects unambiguously.
@@ -75,13 +80,13 @@ var FORMATS = [
75
80
  parse: function (input) { return cms.parse(input); },
76
81
  },
77
82
  {
78
- // TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken OPTIONAL }
83
+ // TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken OPTIONAL } --
79
84
  // a SEQUENCE of 1-2 whose first child is a PKIStatusInfo SEQUENCE whose own first
80
85
  // child is an INTEGER status. This detector is a strict REFINEMENT of the
81
86
  // ocsp-request one (both are SEQUENCE-of-1-2 with a SEQUENCE first child), so it
82
87
  // MUST be checked first: a tokenless TimeStampResp (a bare PKIStatusInfo) would
83
88
  // otherwise be shadowed by ocsp-request, whereas an OCSPRequest's tbsRequest never
84
- // leads with a universal INTEGER and so never matches here (RFC 3161 §2.4.2). A
89
+ // leads with a universal INTEGER and so never matches here (RFC 3161 sec. 2.4.2). A
85
90
  // bare timestamp token is a CMS ContentInfo and routes to cms; a bare TSTInfo is
86
91
  // an internal payload (reached via pki.schema.tsp.parseTstInfo).
87
92
  name: "tsp",
@@ -90,7 +95,7 @@ var FORMATS = [
90
95
  parse: function (input) { return tsp.parse(input); },
91
96
  },
92
97
  {
93
- // CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg (RFC 4211 §3) a
98
+ // CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg (RFC 4211 sec. 3) -- a
94
99
  // SEQUENCE whose first CertReqMsg's CertRequest leads with a universal INTEGER
95
100
  // (certReqId) then a universal SEQUENCE (certTemplate). Leads with a SEQUENCE
96
101
  // like the signed-envelope trio and the ocsp-request, but the INTEGER-then-
@@ -104,13 +109,13 @@ var FORMATS = [
104
109
  },
105
110
  {
106
111
  // PKIMessage ::= SEQUENCE { header PKIHeader, body PKIBody [0..26],
107
- // protection [0]?, extraCerts [1]? } (RFC 9810 §5.1) a SEQUENCE of 2-4
112
+ // protection [0]?, extraCerts [1]? } (RFC 9810 sec. 5.1) -- a SEQUENCE of 2-4
108
113
  // whose first child is a >=3-child SEQUENCE leading with a bare INTEGER
109
114
  // (pvno) and whose second child is context-constructed [0..26]. ORDER IS
110
115
  // LOAD-BEARING here: a 2-child PKIMessage whose body is ir [0] also
111
116
  // satisfies the shallow ocsp-request probe below (k[0] SEQUENCE +
112
117
  // k[1] context-[0]), while no OCSPRequest satisfies this detector (its
113
- // tbsRequest never leads with a bare INTEGER) so cmp MUST sit ahead of
118
+ // tbsRequest never leads with a bare INTEGER) -- so cmp MUST sit ahead of
114
119
  // ocsp-request for the pair to dispatch deterministically.
115
120
  name: "cmp",
116
121
  module: cmp,
@@ -119,10 +124,10 @@ var FORMATS = [
119
124
  },
120
125
  {
121
126
  // OCSPRequest ::= SEQUENCE { tbsRequest SEQUENCE, optionalSignature [0] EXPLICIT
122
- // OPTIONAL } a SEQUENCE of 1-2 whose first child is the tbsRequest SEQUENCE.
127
+ // OPTIONAL } -- a SEQUENCE of 1-2 whose first child is the tbsRequest SEQUENCE.
123
128
  // Leads with a SEQUENCE like the signed-envelope trio, but is excluded by arity
124
129
  // (the trio is EXACTLY 3 children; an OCSPRequest is 1-2). Checked AFTER tsp,
125
- // whose detector is a strict refinement (RFC 6960 §4.1.1), and AFTER cmp,
130
+ // whose detector is a strict refinement (RFC 6960 sec. 4.1.1), and AFTER cmp,
126
131
  // whose 2-child ir-body shape this probe would otherwise shadow.
127
132
  name: "ocsp-request",
128
133
  module: ocsp,
@@ -131,8 +136,8 @@ var FORMATS = [
131
136
  },
132
137
  {
133
138
  // OCSPResponse ::= SEQUENCE { responseStatus ENUMERATED, responseBytes [0]
134
- // EXPLICIT OPTIONAL } the only registered root that leads with an ENUMERATED
135
- // child, so it is disjoint from every other format (RFC 6960 §4.2.1).
139
+ // EXPLICIT OPTIONAL } -- the only registered root that leads with an ENUMERATED
140
+ // child, so it is disjoint from every other format (RFC 6960 sec. 4.2.1).
136
141
  name: "ocsp-response",
137
142
  module: ocsp,
138
143
  detect: ocsp.matchesResponse,
@@ -140,12 +145,12 @@ var FORMATS = [
140
145
  },
141
146
  {
142
147
  // PKCS#12 PFX ::= SEQUENCE { version INTEGER, authSafe ContentInfo, macData
143
- // OPTIONAL } INTEGER-first like pkcs8, so the discriminators are deeper:
144
- // children[1] is a ContentInfo (SEQUENCE of exactly 2: OID + [0] constructed
148
+ // OPTIONAL } -- INTEGER-first like pkcs8, so the discriminators are deeper:
149
+ // children[1] is a ContentInfo (SEQUENCE of exactly 2: OID + [0] constructed --
145
150
  // a shape a PrivateKeyInfo's AlgorithmIdentifier never presents) and
146
151
  // children[2] is a MacData SEQUENCE or absent (pkcs8 requires an OCTET STRING
147
152
  // there). Shape-disjoint from pkcs8, and registered ahead of it as the more
148
- // specific probe (RFC 7292 §4). A BER-encoded PFX detects through the
153
+ // specific probe (RFC 7292 sec. 4). A BER-encoded PFX detects through the
149
154
  // orchestrator's BER decode fallback and routes here.
150
155
  name: "pkcs12",
151
156
  module: pkcs12,
@@ -153,11 +158,11 @@ var FORMATS = [
153
158
  parse: function (input) { return pkcs12.parse(input); },
154
159
  },
155
160
  {
156
- // PKCS#8 PrivateKeyInfo / OneAsymmetricKey SEQUENCE whose first child is an
161
+ // PKCS#8 PrivateKeyInfo / OneAsymmetricKey -- SEQUENCE whose first child is an
157
162
  // INTEGER (version) and third an OCTET STRING (privateKey); disjoint from the
158
163
  // signed-envelope trio. (EncryptedPrivateKeyInfo is deliberately NOT
159
- // auto-routed: its SEQUENCE{SEQUENCE, OCTET STRING} shape is ambiguous a
160
- // PKCS#1 DigestInfo is identical so structural detection cannot classify it
164
+ // auto-routed: its SEQUENCE{SEQUENCE, OCTET STRING} shape is ambiguous -- a
165
+ // PKCS#1 DigestInfo is identical -- so structural detection cannot classify it
161
166
  // without a validated encryption-algorithm discriminator, which arrives with
162
167
  // the PBES layer. It is reached explicitly via pki.schema.pkcs8.parseEncrypted.)
163
168
  name: "pkcs8",
@@ -167,7 +172,7 @@ var FORMATS = [
167
172
  },
168
173
  {
169
174
  // CertificationRequest ::= SEQUENCE { certificationRequestInfo,
170
- // signatureAlgorithm, signature } the same outer 3-element shape,
175
+ // signatureAlgorithm, signature } -- the same outer 3-element shape,
171
176
  // distinguished by a CertificationRequestInfo of EXACTLY four children
172
177
  // ending in the IMPLICIT [0] attributes element. Checked first because that
173
178
  // detector is the most specific and mutually exclusive with the others.
@@ -178,20 +183,28 @@ var FORMATS = [
178
183
  },
179
184
  {
180
185
  // AttributeCertificate ::= SEQUENCE { acinfo, signatureAlgorithm, signatureValue }
181
- // the signed-envelope trio, distinguished from a certificate / CRL by an acinfo
182
- // that LEADS WITH a bare INTEGER version and carries a 2-GeneralizedTime validity
183
- // at children[5]. Registered ahead of crl / x509 (most-specific-first): a legacy v1
184
- // AC would otherwise satisfy x509.matches's Validity probe (RFC 5755 §4.1).
186
+ // -- the signed-envelope trio, recognized by an acinfo that LEADS WITH a bare
187
+ // INTEGER version and carries a 2-GeneralizedTime attrCertValidityPeriod at
188
+ // children[5] (RFC 5755, section 4.1). Order-independent versus crl / x509 / csr:
189
+ // a v2 acinfo puts the AlgorithmIdentifier at the position where their probes
190
+ // require a bare Time / a 2-Time Validity / a 4-child CRI, and none of their tbs
191
+ // shapes carry the 2-GeneralizedTime marker at children[5].
185
192
  name: "attrcert",
186
193
  module: attrcert,
187
194
  detect: attrcert.matches,
188
195
  parse: function (input) { return attrcert.parse(input); },
189
196
  },
190
197
  {
191
- // AttributeCertificateV1 (X.509-1997, RFC 5652 §10.2) the obsolete predecessor
192
- // whose acInfo LEADS WITH the subject CHOICE (context [0]/[1]). Recognized and
193
- // deferred with a precise attrcert/legacy-v1-not-supported rather than routed to a
194
- // wrong format. children[1] == SEQUENCE keeps it disjoint from a v3 certificate.
198
+ // AttributeCertificateV1 (X.509-1997, RFC 5652, section 10.2) -- the obsolete
199
+ // predecessor whose acInfo LEADS WITH the subject CHOICE (context [0]/[1]).
200
+ // Recognized and deferred with a precise attrcert/legacy-v1-not-supported rather
201
+ // than routed to a wrong format. ORDER IS LOAD-BEARING against x509: a [0]-subject
202
+ // v1 AC ALSO satisfies x509.matches (the [0] child reads as the certificate's
203
+ // EXPLICIT version marker, putting the 2-GeneralizedTime attrCertValidityPeriod at
204
+ // the Validity probe offset), so this entry MUST sit ahead of x509 for first-match
205
+ // dispatch to defer it here. children[1] == SEQUENCE keeps a v3 certificate (whose
206
+ // [0] version is followed by an INTEGER serialNumber) from matching in the other
207
+ // direction.
195
208
  name: "attrcert-v1",
196
209
  module: attrcert,
197
210
  detect: attrcert.matchesV1,
@@ -199,7 +212,7 @@ var FORMATS = [
199
212
  },
200
213
  {
201
214
  // CertificateList ::= SEQUENCE { tbsCertList, signatureAlgorithm,
202
- // signatureValue } the same outer shape as a certificate, distinguished
215
+ // signatureValue } -- the same outer shape as a certificate, distinguished
203
216
  // by its tbsCertList (a bare Time at the certificate's Validity position).
204
217
  name: "crl",
205
218
  module: crl,
@@ -209,7 +222,7 @@ var FORMATS = [
209
222
  {
210
223
  name: "x509",
211
224
  module: x509,
212
- // Certificate identified by a Validity (SEQUENCE of two Times) inside the
225
+ // Certificate -- identified by a Validity (SEQUENCE of two Times) inside the
213
226
  // tbs, so a CSR / other 3-element signed envelope is NOT misclassified as a
214
227
  // certificate (it falls through to schema/unknown-format).
215
228
  detect: x509.matches,
@@ -228,7 +241,7 @@ var FORMATS = [
228
241
  * The names of every registered format, in detection order.
229
242
  *
230
243
  * @example
231
- * pki.schema.all(); // ["cms", "tsp", "crmf", "cmp", "ocsp-request", "ocsp-response", "pkcs12", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
244
+ * pki.schema.all(); // -> ["cms", "tsp", "crmf", "cmp", "ocsp-request", "ocsp-response", "pkcs12", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
232
245
  */
233
246
  function all() { return FORMATS.map(function (f) { return f.name; }); }
234
247
 
@@ -247,7 +260,7 @@ function all() { return FORMATS.map(function (f) { return f.name; }); }
247
260
  * errors of the matched format propagate unchanged.
248
261
  *
249
262
  * @example
250
- * var parsed = pki.schema.parse(der); // cert the pki.schema.x509 shape
263
+ * var parsed = pki.schema.parse(der); // cert -> the pki.schema.x509 shape
251
264
  */
252
265
  function parse(input) {
253
266
  // Coerce + decode via the shared parse-entry, then route the COERCED DER (a
@@ -267,16 +280,17 @@ function parse(input) {
267
280
  module.exports = {
268
281
  engine: engine,
269
282
  x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode },
270
- crl: { parse: crl.parse, pemDecode: crl.pemDecode },
283
+ crl: { parse: crl.parse, pemDecode: crl.pemDecode, pemEncode: crl.pemEncode },
271
284
  csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
272
285
  pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
273
286
  pkcs12: { parse: pkcs12.parse, pemDecode: pkcs12.pemDecode, pemEncode: pkcs12.pemEncode },
274
287
  cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
275
- ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode },
276
- tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode },
277
- crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode },
288
+ ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode, pemEncode: ocsp.pemEncode },
289
+ tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode, pemEncode: tsp.pemEncode },
290
+ crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode, pemEncode: crmf.pemEncode },
278
291
  cmp: { parse: cmp.parse, pemDecode: cmp.pemDecode, pemEncode: cmp.pemEncode },
279
- attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode },
292
+ attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode, pemEncode: attrcert.pemEncode },
293
+ smime: { parseSigningCertificate: smime.parseSigningCertificate, parseSigningCertificateV2: smime.parseSigningCertificateV2, parseSmimeCapabilities: smime.parseSmimeCapabilities, decodeAttribute: smime.decodeAttribute },
280
294
  all: all,
281
295
  parse: parse,
282
296
  };
@@ -11,23 +11,25 @@
11
11
  * @intro
12
12
  * X.509 Attribute Certificate handling per RFC 5755. An attribute certificate
13
13
  * binds a holder to a set of privilege attributes (role, group, clearance)
14
- * without carrying a public key the authorization counterpart to an identity
14
+ * without carrying a public key -- the authorization counterpart to an identity
15
15
  * certificate. `parse` decodes a v2 `AttributeCertificate` into its holder,
16
16
  * issuer, validity window (real `Date`s), attributes, and extensions, reusing
17
17
  * the shared signed-envelope so the raw `tbsBytes` (the exact signed region) and
18
18
  * the `signatureValue` are surfaced for a downstream verifier.
19
19
  *
20
20
  * The holder and issuer identities are `GeneralName`s, validated on the way in
21
- * (each alternative's form and content per RFC 5280 §4.2.1.6) and surfaced with
22
- * their raw bytes; the profile MUSTs above structural scope (a single
23
- * non-empty `directoryName` issuer, the holder-to-PKC binding, targeting and
24
- * revocation) are verification-layer concerns. The obsolete X.509-1997
21
+ * (each alternative's form and content per RFC 5280 sec. 4.2.1.6) and surfaced with
22
+ * their raw bytes. The RFC 5755 sec. 4.2.3 issuer profile is enforced at parse: the
23
+ * issuer is `v2Form` (never `v1Form`) carrying exactly one non-empty
24
+ * `directoryName` in `issuerName`, with `baseCertificateID` / `objectDigestInfo`
25
+ * absent. The MUSTs above parse altitude (the holder-to-PKC binding, targeting
26
+ * and revocation) remain verification-layer concerns. The obsolete X.509-1997
25
27
  * `AttributeCertificateV1` is recognized and deferred, not parsed. DER-only,
26
28
  * fail-closed.
27
29
  *
28
30
  * @card
29
31
  * Parse DER / PEM RFC 5755 attribute certificates into holder, issuer,
30
- * validity, attributes, and extensions validated GeneralNames, raw verifier
32
+ * validity, attributes, and extensions -- validated GeneralNames, raw verifier
31
33
  * inputs, legacy v1 recognize-and-defer, fail-closed.
32
34
  */
33
35
 
@@ -47,36 +49,31 @@ var TAGS = asn1.TAGS;
47
49
  var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
48
50
  var EXTENSIONS = pkix.extensions(NS);
49
51
 
50
- // AttCertVersion ::= INTEGER { v2(1) } a bare, mandatory INTEGER (not the x509
52
+ // AttCertVersion ::= INTEGER { v2(1) } -- a bare, mandatory INTEGER (not the x509
51
53
  // [0] EXPLICIT DEFAULT shape). The only legal value is v2, surfaced as 2.
52
54
  var VERSION = pkix.versionReader(NS, { "1": 2 });
53
55
 
54
56
  // ---- shared leaves ---------------------------------------------------
55
57
 
56
- // AttCertValidityPeriod times are GeneralizedTime (RFC 5755 §4.2.6) a UTCTime is
58
+ // AttCertValidityPeriod times are GeneralizedTime (RFC 5755 sec. 4.2.6) -- a UTCTime is
57
59
  // rejected. Non-fractional YYYYMMDDHHMMSSZ is enforced by the codec.
58
- var GENERALIZED_TIME = schema.decode(function (n, ctx) {
59
- if (n.tagClass !== "universal" || n.tagNumber !== TAGS.GENERALIZED_TIME) {
60
- throw ctx.E("attrcert/bad-time", "AttCertValidityPeriod times must be GeneralizedTime (RFC 5755 §4.2.6)");
61
- }
62
- return asn1.read.time(n);
63
- });
60
+ var GENERALIZED_TIME = pkix.generalizedTime(NS, { code: "attrcert/bad-time", message: "AttCertValidityPeriod times must be GeneralizedTime (RFC 5755 sec. 4.2.6)" });
64
61
 
65
62
  // digestedObjectType ::= ENUMERATED { publicKey(0), publicKeyCert(1),
66
- // otherObjectTypes(2) } (RFC 5755 §4.1) an undefined value is rejected; a
63
+ // otherObjectTypes(2) } (RFC 5755 sec. 4.1) -- an undefined value is rejected; a
67
64
  // non-ENUMERATED tag fails at the codec (asn1/*).
68
65
  var ODT_NAMES = { "0": "publicKey", "1": "publicKeyCert", "2": "otherObjectTypes" };
69
66
  var DIGESTED_OBJECT_TYPE = schema.decode(function (n, ctx) {
70
67
  var v = asn1.read.enumerated(n);
71
68
  var k = v.toString();
72
69
  if (!Object.prototype.hasOwnProperty.call(ODT_NAMES, k)) {
73
- throw ctx.E("attrcert/bad-digested-object-type", "digestedObjectType " + k + " is not a defined value (RFC 5755 §4.1)");
70
+ throw ctx.E("attrcert/bad-digested-object-type", "digestedObjectType " + k + " is not a defined value (RFC 5755 sec. 4.1)");
74
71
  }
75
- // RFC 5755 §7.3 a conformant AC MUST NOT use otherObjectTypes(2); the digested
72
+ // RFC 5755 sec. 7.3 -- a conformant AC MUST NOT use otherObjectTypes(2); the digested
76
73
  // object is limited to a public key or a public-key certificate, so reject it
77
74
  // fail-closed rather than parse a digest whose object type is unidentifiable.
78
75
  if (k === "2") {
79
- throw ctx.E("attrcert/bad-digested-object-type", "otherObjectTypes(2) MUST NOT be used (RFC 5755 §7.3)");
76
+ throw ctx.E("attrcert/bad-digested-object-type", "otherObjectTypes(2) MUST NOT be used (RFC 5755 sec. 7.3)");
80
77
  }
81
78
  return { code: Number(v), name: ODT_NAMES[k] };
82
79
  });
@@ -84,7 +81,7 @@ var DIGESTED_OBJECT_TYPE = schema.decode(function (n, ctx) {
84
81
  // ---- IssuerSerial / ObjectDigestInfo ---------------------------------
85
82
 
86
83
  // IssuerSerial ::= SEQUENCE { issuer GeneralNames, serial CertificateSerialNumber,
87
- // issuerUID UniqueIdentifier OPTIONAL } (RFC 5755 §4.1). Reached as an IMPLICIT
84
+ // issuerUID UniqueIdentifier OPTIONAL } (RFC 5755 sec. 4.1). Reached as an IMPLICIT
88
85
  // [n] node (Holder [0] / V2Form [0]) whose tag replaces the SEQUENCE tag, so the
89
86
  // shape assertion is "constructed" (the tag is pinned by the enclosing trailing);
90
87
  // a context [n] PRIMITIVE node has no children and fails closed here.
@@ -106,7 +103,7 @@ var ISSUER_SERIAL = schema.seq([
106
103
 
107
104
  // ObjectDigestInfo ::= SEQUENCE { digestedObjectType ENUMERATED, otherObjectTypeID
108
105
  // OBJECT IDENTIFIER OPTIONAL, digestAlgorithm AlgorithmIdentifier, objectDigest BIT
109
- // STRING } (RFC 5755 §4.1). otherObjectTypeID is present only for otherObjectTypes(2).
106
+ // STRING } (RFC 5755 sec. 4.1). otherObjectTypeID is present only for otherObjectTypes(2).
110
107
  var OBJECT_DIGEST_INFO = schema.seq([
111
108
  schema.field("digestedObjectType", DIGESTED_OBJECT_TYPE),
112
109
  schema.optional("otherObjectTypeID", schema.oidLeaf(), { whenUniversal: [TAGS.OBJECT_IDENTIFIER] }),
@@ -117,10 +114,10 @@ var OBJECT_DIGEST_INFO = schema.seq([
117
114
  build: function (m, ctx) {
118
115
  var t = m.fields.digestedObjectType.value;
119
116
  // otherObjectTypeID identifies a non-certificate object type, meaningful only with
120
- // otherObjectTypes(2) which RFC 5755 §7.3 forbids (rejected at the leaf above)
117
+ // otherObjectTypes(2) -- which RFC 5755 sec. 7.3 forbids (rejected at the leaf above) --
121
118
  // so a present otherObjectTypeID is never valid here and is rejected fail-closed.
122
119
  if (m.fields.otherObjectTypeID.present) {
123
- throw ctx.E("attrcert/bad-object-digest-info", "otherObjectTypeID is only valid with otherObjectTypes(2), which RFC 5755 §7.3 forbids");
120
+ throw ctx.E("attrcert/bad-object-digest-info", "otherObjectTypeID is only valid with otherObjectTypes(2), which RFC 5755 sec. 7.3 forbids");
124
121
  }
125
122
  return {
126
123
  digestedObjectType: t,
@@ -135,7 +132,7 @@ var OBJECT_DIGEST_INFO = schema.seq([
135
132
 
136
133
  // Holder ::= SEQUENCE { baseCertificateID [0] IMPLICIT IssuerSerial OPTIONAL,
137
134
  // entityName [1] IMPLICIT GeneralNames OPTIONAL, objectDigestInfo [2] IMPLICIT
138
- // ObjectDigestInfo OPTIONAL } (RFC 5755 §4.1). All three are IMPLICIT + OPTIONAL;
135
+ // ObjectDigestInfo OPTIONAL } (RFC 5755 sec. 4.1). All three are IMPLICIT + OPTIONAL;
139
136
  // the profile RECOMMENDS exactly one, but the parser surfaces all three (null when
140
137
  // absent) and does not enforce the cardinality.
141
138
  var HOLDER = schema.seq([
@@ -157,8 +154,11 @@ var HOLDER = schema.seq([
157
154
 
158
155
  // V2Form ::= SEQUENCE { issuerName GeneralNames OPTIONAL, baseCertificateID [0]
159
156
  // IMPLICIT IssuerSerial OPTIONAL, objectDigestInfo [1] IMPLICIT ObjectDigestInfo
160
- // OPTIONAL } (RFC 5755 §4.1). Reached via the AttCertIssuer CHOICE on a context [0]
161
- // node, so the shape is "constructed".
157
+ // OPTIONAL } (RFC 5755 sec. 4.1). Reached via the AttCertIssuer CHOICE on a context [0]
158
+ // node, so the shape is "constructed". The build enforces the sec. 4.2.3 issuer
159
+ // profile MUSTs: every field is syntactically OPTIONAL, but a conformant AC's
160
+ // issuerName is present with one and only one GeneralName -- a non-empty
161
+ // directoryName -- and baseCertificateID / objectDigestInfo are absent.
162
162
  var V2_FORM = schema.seq([
163
163
  schema.optional("issuerName", pkix.generalNames(NS, { code: "attrcert/bad-issuer-name" }), { whenUniversal: [TAGS.SEQUENCE] }),
164
164
  schema.trailing([
@@ -167,27 +167,47 @@ var V2_FORM = schema.seq([
167
167
  ], { minTag: 0, maxTag: 1, unexpectedCode: "attrcert/bad-v2form", orderCode: "attrcert/bad-v2form" }),
168
168
  ], {
169
169
  assert: "constructed", code: "attrcert/bad-v2form", what: "V2Form",
170
- build: function (m) {
171
- return {
172
- issuerName: m.fields.issuerName.present ? m.fields.issuerName.value.result : null,
173
- baseCertificateID: m.fields.baseCertificateID.present ? m.fields.baseCertificateID.value.result : null,
174
- objectDigestInfo: m.fields.objectDigestInfo.present ? m.fields.objectDigestInfo.value.result : null,
175
- };
170
+ build: function (m, ctx) {
171
+ if (!m.fields.issuerName.present) {
172
+ throw ctx.E("attrcert/bad-issuer-name", "v2Form must contain issuerName (RFC 5755 sec. 4.2.3)");
173
+ }
174
+ var issuerName = m.fields.issuerName.value.result;
175
+ if (issuerName.names.length !== 1) {
176
+ throw ctx.E("attrcert/bad-issuer-name", "issuerName must contain one and only one GeneralName (RFC 5755 sec. 4.2.3)");
177
+ }
178
+ if (issuerName.names[0].tagNumber !== 4) {
179
+ throw ctx.E("attrcert/bad-issuer-name", "the issuerName GeneralName must be a directoryName [4] (RFC 5755 sec. 4.2.3)");
180
+ }
181
+ // directoryName [4] EXPLICIT wraps exactly one Name (the shared GeneralName
182
+ // grammar validated that); a zero-RDN Name is an empty DN identifying no
183
+ // issuer, which sec. 4.2.3 forbids.
184
+ var dirName = asn1.decode(issuerName.names[0].bytes).children[0];
185
+ if (!dirName.children || dirName.children.length < 1) {
186
+ throw ctx.E("attrcert/bad-issuer-name", "the issuerName directoryName must be a non-empty distinguished name (RFC 5755 sec. 4.2.3)");
187
+ }
188
+ if (m.fields.baseCertificateID.present || m.fields.objectDigestInfo.present) {
189
+ throw ctx.E("attrcert/bad-v2form", "baseCertificateID and objectDigestInfo must not be present in v2Form (RFC 5755 sec. 4.2.3)");
190
+ }
191
+ return { issuerName: issuerName, baseCertificateID: null, objectDigestInfo: null };
176
192
  },
177
193
  });
178
194
 
179
195
  // AttCertIssuer ::= CHOICE { v1Form GeneralNames, v2Form [0] IMPLICIT V2Form } (RFC
180
- // 5755 §4.1). A universal SEQUENCE is the (obsolete but structurally legal) v1Form;
181
- // a context [0] is v2Form. A conforming AC uses v2Form; the parser surfaces which.
196
+ // 5755 sec. 4.1). A universal SEQUENCE is the (obsolete but structurally recognizable)
197
+ // v1Form, which a conformant AC MUST NOT use (sec. 4.2.3) -- the arm is recognized and
198
+ // rejected with a precise verdict rather than mis-dispatched; a context [0] is v2Form.
182
199
  var ATT_CERT_ISSUER = schema.choice([
183
- { when: { tagClass: "universal", tagNumber: TAGS.SEQUENCE }, schema: pkix.generalNames(NS, { code: "attrcert/bad-issuer" }) },
200
+ { when: { tagClass: "universal", tagNumber: TAGS.SEQUENCE },
201
+ schema: schema.decode(function (n, ctx) {
202
+ throw ctx.E("attrcert/bad-issuer", "AttCertIssuer must use v2Form -- v1Form MUST NOT be used (RFC 5755 sec. 4.2.3)");
203
+ }) },
184
204
  { when: { tagClass: "context", tagNumber: 0 }, schema: V2_FORM },
185
205
  ], { code: "attrcert/bad-issuer" });
186
206
 
187
207
  // ---- validity + attributes -------------------------------------------
188
208
 
189
209
  // AttCertValidityPeriod ::= SEQUENCE { notBeforeTime GeneralizedTime, notAfterTime
190
- // GeneralizedTime } (RFC 5755 §4.2.6).
210
+ // GeneralizedTime } (RFC 5755 sec. 4.2.6).
191
211
  var VALIDITY = schema.seq([
192
212
  schema.field("notBeforeTime", GENERALIZED_TIME),
193
213
  schema.field("notAfterTime", GENERALIZED_TIME),
@@ -196,7 +216,7 @@ var VALIDITY = schema.seq([
196
216
  build: function (m) { return { notBeforeTime: m.fields.notBeforeTime.value, notAfterTime: m.fields.notAfterTime.value }; },
197
217
  });
198
218
 
199
- // attributes ::= SEQUENCE OF Attribute (RFC 5755 §4.2.7) MUST be non-empty and
219
+ // attributes ::= SEQUENCE OF Attribute (RFC 5755 sec. 4.2.7) -- MUST be non-empty and
200
220
  // each AttributeType OID unique. Order-preserving (SEQUENCE OF, not SET OF).
201
221
  var ATTRIBUTES = schema.seqOf(pkix.attribute(NS), {
202
222
  assert: "sequence", min: 1, code: "attrcert/bad-attributes", what: "attributes",
@@ -208,7 +228,7 @@ var ATTRIBUTES = schema.seqOf(pkix.attribute(NS), {
208
228
 
209
229
  // AttributeCertificateInfo ::= SEQUENCE { version, holder, issuer, signature
210
230
  // AlgorithmIdentifier, serialNumber, attrCertValidityPeriod, attributes,
211
- // issuerUniqueID OPTIONAL, extensions OPTIONAL } (RFC 5755 §4.1). The tbs body the
231
+ // issuerUniqueID OPTIONAL, extensions OPTIONAL } (RFC 5755 sec. 4.1). The tbs body the
212
232
  // signed envelope wraps; the cross-field invariants live in the envelope build.
213
233
  var ACINFO = schema.seq([
214
234
  schema.field("version", VERSION),
@@ -223,39 +243,36 @@ var ACINFO = schema.seq([
223
243
  ], { assert: "sequence", code: "attrcert/bad-acinfo", what: "AttributeCertificateInfo" });
224
244
 
225
245
  // AttributeCertificate ::= SEQUENCE { acinfo, signatureAlgorithm, signatureValue
226
- // BIT STRING } the shared SIGNED envelope (RFC 5755 §4.1). The AC-specific
227
- // invariants (§4.2.4 sig-alg agreement, §4.2.5 positive-and-<=20-octet serialNumber)
246
+ // BIT STRING } -- the shared SIGNED envelope (RFC 5755 sec. 4.1). The AC-specific
247
+ // invariants (sec. 4.2.4 sig-alg agreement, sec. 4.2.5 positive-and-<=20-octet serialNumber)
228
248
  // run in the build; the envelope owns the SEQUENCE-of-3 shape + signature extraction.
229
249
  var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
230
250
  code: "attrcert/not-an-attribute-certificate", what: "AttributeCertificate",
231
251
  build: function (e, ctx) {
232
252
  var acinfo = e.tbsMatch;
233
253
 
234
- // RFC 5755 §4.2.4 the outer signatureAlgorithm MUST equal acinfo.signature.
254
+ // RFC 5755 sec. 4.2.4 -- the outer signatureAlgorithm MUST equal acinfo.signature.
235
255
  if (!e.outerSignatureAlgorithmBytes.equals(acinfo.fields.signature.node.bytes)) {
236
- throw ctx.E("attrcert/bad-signature-algorithm", "signatureAlgorithm must match AttributeCertificateInfo.signature (RFC 5755 §4.2.4)");
256
+ throw ctx.E("attrcert/bad-signature-algorithm", "signatureAlgorithm must match AttributeCertificateInfo.signature (RFC 5755 sec. 4.2.4)");
237
257
  }
238
258
 
239
- // RFC 5755 §4.2.5 serialNumber MUST be a positive INTEGER of at most 20 content
259
+ // RFC 5755 sec. 4.2.5 -- serialNumber MUST be a positive INTEGER of at most 20 content
240
260
  // octets. Positive excludes both a negative (DER sign bit set) and zero (a minimal
241
- // INTEGER 0 is a single 0x00 octet the codec already rejected any non-minimal
261
+ // INTEGER 0 is a single 0x00 octet -- the codec already rejected any non-minimal
242
262
  // zero). Surface it lossless (BigInt) + as hex.
243
263
  var serialNode = acinfo.fields.serialNumber.node;
244
264
  var sc = serialNode.content;
245
265
  if (sc.length === 0 || (sc[0] & 0x80) !== 0 || (sc.length === 1 && sc[0] === 0)) {
246
- throw ctx.E("attrcert/bad-serial-number", "serialNumber must be a positive INTEGER (RFC 5755 §4.2.5)");
266
+ throw ctx.E("attrcert/bad-serial-number", "serialNumber must be a positive INTEGER (RFC 5755 sec. 4.2.5)");
247
267
  }
248
268
  if (sc.length > 20) {
249
- throw ctx.E("attrcert/bad-serial-number", "serialNumber must not exceed 20 content octets (RFC 5755 §4.2.5)");
269
+ throw ctx.E("attrcert/bad-serial-number", "serialNumber must not exceed 20 content octets (RFC 5755 sec. 4.2.5)");
250
270
  }
251
271
 
252
- // AttCertIssuer is a CHOICE the chosen arm is read off the issuer node's tag
253
- // (a universal SEQUENCE is v1Form; a context [0] is v2Form).
254
- var issuerNode = acinfo.fields.issuer.node;
255
- var issuerVal = acinfo.fields.issuer.value;
256
- var issuer = issuerNode.tagClass === "context"
257
- ? { form: "v2Form", v2Form: issuerVal.result, v1Form: null }
258
- : { form: "v1Form", v1Form: issuerVal.result, v2Form: null };
272
+ // AttCertIssuer is a CHOICE whose v1Form arm rejects at the leaf (sec. 4.2.3), so
273
+ // the surviving arm is always v2Form. The { form, v2Form, v1Form } shape is
274
+ // kept stable for consumers keying off `form`.
275
+ var issuer = { form: "v2Form", v2Form: acinfo.fields.issuer.value.result, v1Form: null };
259
276
 
260
277
  var extField = acinfo.fields.extensions;
261
278
  return {
@@ -286,7 +303,8 @@ var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
286
303
  * @spec RFC 5755, X.509
287
304
  * @related pki.schema.parse, pki.schema.x509.parse
288
305
  *
289
- * Parse a DER `Buffer` or a PEM string/Buffer into a structured v2 attribute
306
+ * Parse a DER `Buffer` or a PEM string/Buffer (label `ATTRIBUTE CERTIFICATE`,
307
+ * the OpenSSL armor) into a structured v2 attribute
290
308
  * certificate: `{ version, holder, issuer, signatureAlgorithm, serialNumber,
291
309
  * serialNumberHex, validity, attributes, issuerUniqueID, extensions, tbsBytes,
292
310
  * signatureValue }`. The holder / issuer identities come back as validated
@@ -302,23 +320,23 @@ var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
302
320
  * ac.attributes[0].name; // "role"
303
321
  * ac.validity.notAfterTime;// Date
304
322
  */
305
- var PARSE_OPTS = { pemLabel: null, PemError: PemError, ErrorClass: AttrCertError, prefix: "attrcert", what: "attribute certificate" };
323
+ var PARSE_OPTS = { pemLabel: "ATTRIBUTE CERTIFICATE", PemError: PemError, ErrorClass: AttrCertError, prefix: "attrcert", what: "attribute certificate" };
306
324
 
307
325
  function _legacyV1Error() {
308
- return new AttrCertError("attrcert/legacy-v1-not-supported", "AttributeCertificateV1 (X.509-1997) is obsolete and not parsed by this build (RFC 5755 §1)");
326
+ return new AttrCertError("attrcert/legacy-v1-not-supported", "AttributeCertificateV1 (X.509-1997) is obsolete and not parsed by this build (RFC 5755 sec. 1)");
309
327
  }
310
328
 
311
329
  function parse(input) {
312
330
  var root = pkix.decodeRoot(pkix.coerceToDer(input, PARSE_OPTS), PARSE_OPTS);
313
331
  // A well-formed legacy AttributeCertificateV1 gets the advertised, stable
314
- // attrcert/legacy-v1-not-supported on the DIRECT path too not a low-level asn1/*
315
- // tag error from attempting the v2 walk so a direct caller of this entry sees the
332
+ // attrcert/legacy-v1-not-supported on the DIRECT path too -- not a low-level asn1/*
333
+ // tag error from attempting the v2 walk -- so a direct caller of this entry sees the
316
334
  // same error family for the recognized-but-deferred form as the orchestrator does.
317
335
  if (matchesV1(root)) throw _legacyV1Error();
318
336
  return schema.walk(ATTRIBUTE_CERTIFICATE, root, NS).result;
319
337
  }
320
338
 
321
- // The obsolete X.509-1997 AttributeCertificateV1 (RFC 5652 §10.2) is recognized and
339
+ // The obsolete X.509-1997 AttributeCertificateV1 (RFC 5652 sec. 10.2) is recognized and
322
340
  // deferred: decode the envelope (so a malformed input still fails as bad DER), then
323
341
  // throw the precise diagnostic rather than routing it to a wrong format.
324
342
  function parseV1(input) {
@@ -334,19 +352,37 @@ function parseV1(input) {
334
352
  * @spec RFC 7468, RFC 5755
335
353
  * @related pki.schema.attrcert.parse
336
354
  *
337
- * Extract the DER bytes from a PEM block (OpenSSL emits
338
- * `-----BEGIN ATTRIBUTE CERTIFICATE-----`; the first block is taken unless `label`
339
- * is given). Throws `PemError` on a missing envelope or a non-base64 body.
355
+ * Extract the DER bytes from a PEM block (default label
356
+ * `ATTRIBUTE CERTIFICATE`, the OpenSSL armor -- the canonical-label default
357
+ * every sibling format applies). Pass a `label` to enforce a different block
358
+ * type, or an explicit `null` to take the first block of any type. Throws
359
+ * `PemError` on a missing / mismatched envelope or a non-base64 body.
340
360
  *
341
361
  * @example
342
362
  * var der = pki.schema.attrcert.pemDecode(pemText);
343
363
  */
344
- function pemDecode(text, label) { return pkix.pemDecode(text, label || null, PemError); }
364
+ function pemDecode(text, label) { return pkix.pemDecode(text, label === null ? null : (label || "ATTRIBUTE CERTIFICATE"), PemError); }
365
+
366
+ /**
367
+ * @primitive pki.schema.attrcert.pemEncode
368
+ * @signature pki.schema.attrcert.pemEncode(der, label?) -> string
369
+ * @since 0.1.23
370
+ * @status experimental
371
+ * @spec RFC 7468, RFC 5755
372
+ * @related pki.schema.attrcert.pemDecode
373
+ *
374
+ * Wrap DER bytes in a PEM envelope (default label `ATTRIBUTE CERTIFICATE`, the
375
+ * same default `pemDecode` reads). Throws `PemError` on a malformed label.
376
+ *
377
+ * @example
378
+ * var pem = pki.schema.attrcert.pemEncode(der);
379
+ */
380
+ function pemEncode(der, label) { return pkix.pemEncode(der, label || "ATTRIBUTE CERTIFICATE", PemError); }
345
381
 
346
382
  // matches(root): a v2 AttributeCertificate is the signed-envelope trio whose acinfo
347
383
  // LEADS WITH a bare universal INTEGER version (== v2 = 1, NOT the x509 [0] EXPLICIT
348
384
  // wrapper) and whose attrCertValidityPeriod at children[5] is a SEQUENCE of exactly
349
- // two GeneralizedTime (RFC 5755 §4.2.6 forbids UTCTime) a marker no cert / CRL /
385
+ // two GeneralizedTime (RFC 5755 sec. 4.2.6 forbids UTCTime) -- a marker no cert / CRL /
350
386
  // CSR presents at that position. Disjoint from matchesV1 by the children[0] tag class.
351
387
  function matches(root) {
352
388
  var acinfo = pkix.signedEnvelopeTbs(root);
@@ -360,7 +396,7 @@ function matches(root) {
360
396
  return v.children.every(function (t) { return schema.isUniversal(t, TAGS.GENERALIZED_TIME); });
361
397
  }
362
398
 
363
- // matchesV1(root): the obsolete AttributeCertificateV1 its acInfo (version DEFAULT
399
+ // matchesV1(root): the obsolete AttributeCertificateV1 -- its acInfo (version DEFAULT
364
400
  // v1 OMITTED) LEADS WITH the subject CHOICE (a context [0]/[1]), and its children[1]
365
401
  // is the issuer GeneralNames (a universal SEQUENCE). The children[1] == SEQUENCE test
366
402
  // keeps it disjoint from a v3 certificate (whose [0] EXPLICIT version is followed by
@@ -378,6 +414,7 @@ module.exports = {
378
414
  parse: parse,
379
415
  parseV1: parseV1,
380
416
  pemDecode: pemDecode,
417
+ pemEncode: pemEncode,
381
418
  matches: matches,
382
419
  matchesV1: matchesV1,
383
420
  };