@blamejs/pki 0.1.22 → 0.1.24

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
 
@@ -42,9 +42,10 @@ var ocsp = require("./schema-ocsp");
42
42
  var tsp = require("./schema-tsp");
43
43
  var crmf = require("./schema-crmf");
44
44
  var cmp = require("./schema-cmp");
45
+ var csrattrs = require("./schema-csrattrs");
45
46
  var attrcert = require("./schema-attrcert");
46
47
  // 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
+ // format -- it has no self-describing DER root (an ESS attribute value is a bare
48
49
  // SEQUENCE) so it is deliberately absent from FORMATS / the detect-and-route
49
50
  // `parse`; it is reached only by explicit OID dispatch on a CMS attribute.
50
51
  var smime = require("./schema-smime");
@@ -61,14 +62,14 @@ var PemError = frameworkError.PemError;
61
62
  // fails typed inside that format's own parse.
62
63
  var ENTRY = { pemLabel: null, PemError: PemError, ErrorClass: SchemaError, prefix: "schema", what: "input", ber: true };
63
64
 
64
- // REGISTRY each format is declarative data: a name, a `detect(root)` that
65
+ // REGISTRY -- each format is declarative data: a name, a `detect(root)` that
65
66
  // recognizes the decoded DER root as this format's outer structure, and the
66
67
  // member's own `parse`. Adding a format is a table entry, not new dispatch
67
68
  // logic. Order matters: the most specific detector wins, so a new member is
68
69
  // inserted ahead of any more-permissive one.
69
70
  var FORMATS = [
70
71
  {
71
- // CMS ContentInfo (RFC 5652 §3) the only registered root that leads with an
72
+ // CMS ContentInfo (RFC 5652 sec. 3) -- the only registered root that leads with an
72
73
  // OBJECT IDENTIFIER child (a SEQUENCE of exactly 2: contentType OID + a
73
74
  // context [0] EXPLICIT content wrapper). Disjoint from the INTEGER-first pkcs8
74
75
  // and the tbs-SEQUENCE-first signed-envelope trio, so it detects unambiguously.
@@ -80,13 +81,13 @@ var FORMATS = [
80
81
  parse: function (input) { return cms.parse(input); },
81
82
  },
82
83
  {
83
- // TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken OPTIONAL }
84
+ // TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken OPTIONAL } --
84
85
  // a SEQUENCE of 1-2 whose first child is a PKIStatusInfo SEQUENCE whose own first
85
86
  // child is an INTEGER status. This detector is a strict REFINEMENT of the
86
87
  // ocsp-request one (both are SEQUENCE-of-1-2 with a SEQUENCE first child), so it
87
88
  // MUST be checked first: a tokenless TimeStampResp (a bare PKIStatusInfo) would
88
89
  // otherwise be shadowed by ocsp-request, whereas an OCSPRequest's tbsRequest never
89
- // leads with a universal INTEGER and so never matches here (RFC 3161 §2.4.2). A
90
+ // leads with a universal INTEGER and so never matches here (RFC 3161 sec. 2.4.2). A
90
91
  // bare timestamp token is a CMS ContentInfo and routes to cms; a bare TSTInfo is
91
92
  // an internal payload (reached via pki.schema.tsp.parseTstInfo).
92
93
  name: "tsp",
@@ -95,7 +96,7 @@ var FORMATS = [
95
96
  parse: function (input) { return tsp.parse(input); },
96
97
  },
97
98
  {
98
- // CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg (RFC 4211 §3) a
99
+ // CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg (RFC 4211 sec. 3) -- a
99
100
  // SEQUENCE whose first CertReqMsg's CertRequest leads with a universal INTEGER
100
101
  // (certReqId) then a universal SEQUENCE (certTemplate). Leads with a SEQUENCE
101
102
  // like the signed-envelope trio and the ocsp-request, but the INTEGER-then-
@@ -109,25 +110,39 @@ var FORMATS = [
109
110
  },
110
111
  {
111
112
  // PKIMessage ::= SEQUENCE { header PKIHeader, body PKIBody [0..26],
112
- // protection [0]?, extraCerts [1]? } (RFC 9810 §5.1) a SEQUENCE of 2-4
113
+ // protection [0]?, extraCerts [1]? } (RFC 9810 sec. 5.1) -- a SEQUENCE of 2-4
113
114
  // whose first child is a >=3-child SEQUENCE leading with a bare INTEGER
114
115
  // (pvno) and whose second child is context-constructed [0..26]. ORDER IS
115
116
  // LOAD-BEARING here: a 2-child PKIMessage whose body is ir [0] also
116
117
  // satisfies the shallow ocsp-request probe below (k[0] SEQUENCE +
117
118
  // k[1] context-[0]), while no OCSPRequest satisfies this detector (its
118
- // tbsRequest never leads with a bare INTEGER) so cmp MUST sit ahead of
119
+ // tbsRequest never leads with a bare INTEGER) -- so cmp MUST sit ahead of
119
120
  // ocsp-request for the pair to dispatch deterministically.
120
121
  name: "cmp",
121
122
  module: cmp,
122
123
  detect: cmp.matches,
123
124
  parse: function (input) { return cmp.parse(input); },
124
125
  },
126
+ {
127
+ // CsrAttrs ::= SEQUENCE SIZE (0..MAX) OF AttrOrOID (RFC 8951 sec. 3.5) -- a
128
+ // SEQUENCE whose every child is a bare universal OID or an Attribute (a
129
+ // universal SEQUENCE of exactly 2: OID + SET). A 1-2-element all-Attribute
130
+ // CsrAttrs satisfies the permissive ocsp-request probe below (SEQUENCE +
131
+ // constructed first child), and the empty 30 00 is a CsrAttrs no other root
132
+ // accepts -- so csrattrs is the strict refinement and MUST sit ahead of
133
+ // ocsp-request, the same resolution tsp / cmp use. Disjoint from cmp (whose
134
+ // body child is context-[0..26], never a universal OID/Attribute).
135
+ name: "csrattrs",
136
+ module: csrattrs,
137
+ detect: csrattrs.matches,
138
+ parse: function (input) { return csrattrs.parse(input); },
139
+ },
125
140
  {
126
141
  // OCSPRequest ::= SEQUENCE { tbsRequest SEQUENCE, optionalSignature [0] EXPLICIT
127
- // OPTIONAL } a SEQUENCE of 1-2 whose first child is the tbsRequest SEQUENCE.
142
+ // OPTIONAL } -- a SEQUENCE of 1-2 whose first child is the tbsRequest SEQUENCE.
128
143
  // Leads with a SEQUENCE like the signed-envelope trio, but is excluded by arity
129
144
  // (the trio is EXACTLY 3 children; an OCSPRequest is 1-2). Checked AFTER tsp,
130
- // whose detector is a strict refinement (RFC 6960 §4.1.1), and AFTER cmp,
145
+ // whose detector is a strict refinement (RFC 6960 sec. 4.1.1), and AFTER cmp,
131
146
  // whose 2-child ir-body shape this probe would otherwise shadow.
132
147
  name: "ocsp-request",
133
148
  module: ocsp,
@@ -136,8 +151,8 @@ var FORMATS = [
136
151
  },
137
152
  {
138
153
  // OCSPResponse ::= SEQUENCE { responseStatus ENUMERATED, responseBytes [0]
139
- // EXPLICIT OPTIONAL } the only registered root that leads with an ENUMERATED
140
- // child, so it is disjoint from every other format (RFC 6960 §4.2.1).
154
+ // EXPLICIT OPTIONAL } -- the only registered root that leads with an ENUMERATED
155
+ // child, so it is disjoint from every other format (RFC 6960 sec. 4.2.1).
141
156
  name: "ocsp-response",
142
157
  module: ocsp,
143
158
  detect: ocsp.matchesResponse,
@@ -145,12 +160,12 @@ var FORMATS = [
145
160
  },
146
161
  {
147
162
  // PKCS#12 PFX ::= SEQUENCE { version INTEGER, authSafe ContentInfo, macData
148
- // OPTIONAL } INTEGER-first like pkcs8, so the discriminators are deeper:
149
- // children[1] is a ContentInfo (SEQUENCE of exactly 2: OID + [0] constructed
163
+ // OPTIONAL } -- INTEGER-first like pkcs8, so the discriminators are deeper:
164
+ // children[1] is a ContentInfo (SEQUENCE of exactly 2: OID + [0] constructed --
150
165
  // a shape a PrivateKeyInfo's AlgorithmIdentifier never presents) and
151
166
  // children[2] is a MacData SEQUENCE or absent (pkcs8 requires an OCTET STRING
152
167
  // there). Shape-disjoint from pkcs8, and registered ahead of it as the more
153
- // specific probe (RFC 7292 §4). A BER-encoded PFX detects through the
168
+ // specific probe (RFC 7292 sec. 4). A BER-encoded PFX detects through the
154
169
  // orchestrator's BER decode fallback and routes here.
155
170
  name: "pkcs12",
156
171
  module: pkcs12,
@@ -158,11 +173,11 @@ var FORMATS = [
158
173
  parse: function (input) { return pkcs12.parse(input); },
159
174
  },
160
175
  {
161
- // PKCS#8 PrivateKeyInfo / OneAsymmetricKey SEQUENCE whose first child is an
176
+ // PKCS#8 PrivateKeyInfo / OneAsymmetricKey -- SEQUENCE whose first child is an
162
177
  // INTEGER (version) and third an OCTET STRING (privateKey); disjoint from the
163
178
  // signed-envelope trio. (EncryptedPrivateKeyInfo is deliberately NOT
164
- // auto-routed: its SEQUENCE{SEQUENCE, OCTET STRING} shape is ambiguous a
165
- // PKCS#1 DigestInfo is identical so structural detection cannot classify it
179
+ // auto-routed: its SEQUENCE{SEQUENCE, OCTET STRING} shape is ambiguous -- a
180
+ // PKCS#1 DigestInfo is identical -- so structural detection cannot classify it
166
181
  // without a validated encryption-algorithm discriminator, which arrives with
167
182
  // the PBES layer. It is reached explicitly via pki.schema.pkcs8.parseEncrypted.)
168
183
  name: "pkcs8",
@@ -172,7 +187,7 @@ var FORMATS = [
172
187
  },
173
188
  {
174
189
  // CertificationRequest ::= SEQUENCE { certificationRequestInfo,
175
- // signatureAlgorithm, signature } the same outer 3-element shape,
190
+ // signatureAlgorithm, signature } -- the same outer 3-element shape,
176
191
  // distinguished by a CertificationRequestInfo of EXACTLY four children
177
192
  // ending in the IMPLICIT [0] attributes element. Checked first because that
178
193
  // detector is the most specific and mutually exclusive with the others.
@@ -183,20 +198,28 @@ var FORMATS = [
183
198
  },
184
199
  {
185
200
  // AttributeCertificate ::= SEQUENCE { acinfo, signatureAlgorithm, signatureValue }
186
- // the signed-envelope trio, distinguished from a certificate / CRL by an acinfo
187
- // that LEADS WITH a bare INTEGER version and carries a 2-GeneralizedTime validity
188
- // at children[5]. Registered ahead of crl / x509 (most-specific-first): a legacy v1
189
- // AC would otherwise satisfy x509.matches's Validity probe (RFC 5755 §4.1).
201
+ // -- the signed-envelope trio, recognized by an acinfo that LEADS WITH a bare
202
+ // INTEGER version and carries a 2-GeneralizedTime attrCertValidityPeriod at
203
+ // children[5] (RFC 5755, section 4.1). Order-independent versus crl / x509 / csr:
204
+ // a v2 acinfo puts the AlgorithmIdentifier at the position where their probes
205
+ // require a bare Time / a 2-Time Validity / a 4-child CRI, and none of their tbs
206
+ // shapes carry the 2-GeneralizedTime marker at children[5].
190
207
  name: "attrcert",
191
208
  module: attrcert,
192
209
  detect: attrcert.matches,
193
210
  parse: function (input) { return attrcert.parse(input); },
194
211
  },
195
212
  {
196
- // AttributeCertificateV1 (X.509-1997, RFC 5652 §10.2) the obsolete predecessor
197
- // whose acInfo LEADS WITH the subject CHOICE (context [0]/[1]). Recognized and
198
- // deferred with a precise attrcert/legacy-v1-not-supported rather than routed to a
199
- // wrong format. children[1] == SEQUENCE keeps it disjoint from a v3 certificate.
213
+ // AttributeCertificateV1 (X.509-1997, RFC 5652, section 10.2) -- the obsolete
214
+ // predecessor whose acInfo LEADS WITH the subject CHOICE (context [0]/[1]).
215
+ // Recognized and deferred with a precise attrcert/legacy-v1-not-supported rather
216
+ // than routed to a wrong format. ORDER IS LOAD-BEARING against x509: a [0]-subject
217
+ // v1 AC ALSO satisfies x509.matches (the [0] child reads as the certificate's
218
+ // EXPLICIT version marker, putting the 2-GeneralizedTime attrCertValidityPeriod at
219
+ // the Validity probe offset), so this entry MUST sit ahead of x509 for first-match
220
+ // dispatch to defer it here. children[1] == SEQUENCE keeps a v3 certificate (whose
221
+ // [0] version is followed by an INTEGER serialNumber) from matching in the other
222
+ // direction.
200
223
  name: "attrcert-v1",
201
224
  module: attrcert,
202
225
  detect: attrcert.matchesV1,
@@ -204,7 +227,7 @@ var FORMATS = [
204
227
  },
205
228
  {
206
229
  // CertificateList ::= SEQUENCE { tbsCertList, signatureAlgorithm,
207
- // signatureValue } the same outer shape as a certificate, distinguished
230
+ // signatureValue } -- the same outer shape as a certificate, distinguished
208
231
  // by its tbsCertList (a bare Time at the certificate's Validity position).
209
232
  name: "crl",
210
233
  module: crl,
@@ -214,7 +237,7 @@ var FORMATS = [
214
237
  {
215
238
  name: "x509",
216
239
  module: x509,
217
- // Certificate identified by a Validity (SEQUENCE of two Times) inside the
240
+ // Certificate -- identified by a Validity (SEQUENCE of two Times) inside the
218
241
  // tbs, so a CSR / other 3-element signed envelope is NOT misclassified as a
219
242
  // certificate (it falls through to schema/unknown-format).
220
243
  detect: x509.matches,
@@ -233,7 +256,7 @@ var FORMATS = [
233
256
  * The names of every registered format, in detection order.
234
257
  *
235
258
  * @example
236
- * pki.schema.all(); // ["cms", "tsp", "crmf", "cmp", "ocsp-request", "ocsp-response", "pkcs12", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
259
+ * pki.schema.all(); // -> ["cms", "tsp", "crmf", "cmp", "ocsp-request", "ocsp-response", "pkcs12", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
237
260
  */
238
261
  function all() { return FORMATS.map(function (f) { return f.name; }); }
239
262
 
@@ -252,7 +275,7 @@ function all() { return FORMATS.map(function (f) { return f.name; }); }
252
275
  * errors of the matched format propagate unchanged.
253
276
  *
254
277
  * @example
255
- * var parsed = pki.schema.parse(der); // cert the pki.schema.x509 shape
278
+ * var parsed = pki.schema.parse(der); // cert -> the pki.schema.x509 shape
256
279
  */
257
280
  function parse(input) {
258
281
  // Coerce + decode via the shared parse-entry, then route the COERCED DER (a
@@ -272,16 +295,17 @@ function parse(input) {
272
295
  module.exports = {
273
296
  engine: engine,
274
297
  x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode },
275
- crl: { parse: crl.parse, pemDecode: crl.pemDecode },
298
+ crl: { parse: crl.parse, pemDecode: crl.pemDecode, pemEncode: crl.pemEncode },
276
299
  csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
277
300
  pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
278
301
  pkcs12: { parse: pkcs12.parse, pemDecode: pkcs12.pemDecode, pemEncode: pkcs12.pemEncode },
279
302
  cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
280
- ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode },
281
- tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode },
282
- crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode },
303
+ ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode, pemEncode: ocsp.pemEncode },
304
+ tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode, pemEncode: tsp.pemEncode },
305
+ crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode, pemEncode: crmf.pemEncode },
283
306
  cmp: { parse: cmp.parse, pemDecode: cmp.pemDecode, pemEncode: cmp.pemEncode },
284
- attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode },
307
+ csrattrs: { parse: csrattrs.parse },
308
+ attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode, pemEncode: attrcert.pemEncode },
285
309
  smime: { parseSigningCertificate: smime.parseSigningCertificate, parseSigningCertificateV2: smime.parseSigningCertificateV2, parseSmimeCapabilities: smime.parseSmimeCapabilities, decodeAttribute: smime.decodeAttribute },
286
310
  all: all,
287
311
  parse: parse,
@@ -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
  };