@blamejs/pki 0.6.13 → 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -1
- package/README.md +1 -1
- package/index.js +1 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +59 -233
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/smime.js
CHANGED
|
@@ -48,20 +48,6 @@ var SmimeError = frameworkError.SmimeError;
|
|
|
48
48
|
|
|
49
49
|
function _err(code, msg, cause) { return new SmimeError(code, msg, cause); }
|
|
50
50
|
|
|
51
|
-
// ---- the option surface each verb accepts -----------------------------------
|
|
52
|
-
//
|
|
53
|
-
// A misspelled option is the one input that reads as an omission, never as a value: nothing is
|
|
54
|
-
// out of range, nothing fails to parse, and the caller who asked for something stricter silently
|
|
55
|
-
// gets the looser default. `protectHeaders` misspelled sends the headers a caller meant to protect
|
|
56
|
-
// as ordinary display copies; `strictMicalg` misspelled accepts the mismatch it was set to reject;
|
|
57
|
-
// `entity` misspelled wraps a caller's complete MIME entity inside another one.
|
|
58
|
-
//
|
|
59
|
-
// The tables are per verb, not per module, because the surfaces genuinely differ (`form` means
|
|
60
|
-
// something on sign and nothing on encrypt) and a merged table would accept each verb's options at
|
|
61
|
-
// every other one, which is the same silence in a wider form. Each is the keys that verb's body and
|
|
62
|
-
// the helpers it hands `opts` to actually read: the value goes through _entityBytes, _cmsSignOpts or
|
|
63
|
-
// _cmsEncryptOpts as readily as it is read here, so a table built from the verb's own lines alone
|
|
64
|
-
// would refuse options that work.
|
|
65
51
|
var SIGN_OPTS = {
|
|
66
52
|
form: 1, entity: 1, contentType: 1, signingTime: 1, protectHeaders: 1, headers: 1, hcp: 1,
|
|
67
53
|
sid: 1, signedAttributes: 1, additionalSignedAttributes: 1,
|
|
@@ -85,58 +71,32 @@ function _knownOpts(opts, known, verb) {
|
|
|
85
71
|
});
|
|
86
72
|
}
|
|
87
73
|
|
|
88
|
-
// RFC 8551 uses application/pkcs7-<kind>; OpenSSL's legacy `smime` command emits the PKCS#7
|
|
89
|
-
// application/x-pkcs7-<kind>. Accept both on the RECEIVE side (we always EMIT the RFC 8551 form).
|
|
90
74
|
function _isPkcs7(type, kind) {
|
|
91
75
|
var t = (type || "").toLowerCase();
|
|
92
76
|
return t === "application/pkcs7-" + kind || t === "application/x-pkcs7-" + kind;
|
|
93
77
|
}
|
|
94
78
|
|
|
95
|
-
// The RFC 8551 sec. 3.4.3.2 micalg name for a CMS digest (schema-cms surfaces "sha256" etc.).
|
|
96
|
-
// RFC 8551 sec. 3.4.3.2 micalg names, extended with the SHAKE names FIPS 204/205 CMS signers digest
|
|
97
|
-
// with (RFC 8702). An unknown digest passes through verbatim and is never regex-mangled (a blind
|
|
98
|
-
// `sha`->`sha-` would corrupt "shake256" into "sha-ke256").
|
|
99
79
|
var MICALG = { sha1: "sha-1", sha224: "sha-224", sha256: "sha-256", sha384: "sha-384", sha512: "sha-512", md5: "md5", shake128: "shake128", shake256: "shake256" };
|
|
100
80
|
|
|
101
|
-
// The exact bytes to sign: the caller's content wrapped as a MIME entity (text/plain by default) in
|
|
102
|
-
// its canonical form, or, when opts.entity is set, the caller's own complete MIME entity, canonical.
|
|
103
81
|
function _entityBytes(content, opts) {
|
|
104
82
|
var raw = guard.bytes.view(content, SmimeError, "smime/bad-input", "content");
|
|
105
83
|
if (opts.entity) return mime.canonicalize(raw, SmimeError, "smime/bad-mime");
|
|
106
|
-
// Declare the honest Content-Transfer-Encoding: "7bit" requires every byte <= 127 (RFC 2045); a body
|
|
107
|
-
// with any 8-bit byte is "8bit". Declaring 7bit for 8-bit content is a false claim a transport could act on.
|
|
108
84
|
var cte = _is7bit(raw) ? "7bit" : "8bit";
|
|
109
|
-
// Build through the guarded serializer so a CRLF / NUL in opts.contentType cannot inject a header field.
|
|
110
85
|
return mime.buildEntity([{ name: "Content-Type", value: opts.contentType || "text/plain; charset=utf-8" }, { name: "Content-Transfer-Encoding", value: cte }], raw, SmimeError, "smime/bad-header");
|
|
111
86
|
}
|
|
112
87
|
function _is7bit(buf) { for (var i = 0; i < buf.length; i++) if (buf[i] > 0x7f) return false; return true; }
|
|
113
88
|
|
|
114
|
-
// ---- RFC 9788 header protection (an opts.protectHeaders-gated MIME transform over the shipped CMS path) ----
|
|
115
|
-
// The protected header fields are inlined on the Cryptographic Payload root (its Content-Type gains an `hp`
|
|
116
|
-
// parameter: "clear" for signed-only, "cipher" for encrypted); the outer display headers are copied as-is
|
|
117
|
-
// (signed) or passed through a Header Confidentiality Policy (encrypted). The CMS crypto is UNCHANGED.
|
|
118
89
|
|
|
119
|
-
// Is this (lowercased) header field name Structural? RFC 9787 sec. 1.1.1 defines a Structural Header Field as
|
|
120
|
-
// MIME-Version OR any field whose name begins with "Content-" (Content-Type / -Transfer-Encoding /
|
|
121
|
-
// -Disposition / -ID / -Description / -Language / -Location / ...). These describe the specific MIME part --
|
|
122
|
-
// the library sets them -- so they are never protected as Non-Structural fields (rejected at the producer) nor
|
|
123
|
-
// surfaced in the authenticated protectedHeaders set on verify. The prefix rule is the RFC definition itself,
|
|
124
|
-
// so it matches every Content-* field (not just an enumerated subset) and is not a prototype-key lookup (a
|
|
125
|
-
// field named "constructor" / "toString" is simply not Structural).
|
|
126
90
|
function _isStructural(lname) {
|
|
127
91
|
return lname === "mime-version" || lname.indexOf("content-") === 0;
|
|
128
92
|
}
|
|
129
93
|
|
|
130
|
-
// Normalize opts.headers (an object { Name: value } or an array [{ name, value }]) to an ordered list. A
|
|
131
|
-
// malformed shape is a config-time smime/bad-input (the value's byte integrity is guarded at emission).
|
|
132
94
|
function _hpHeaderList(headers) {
|
|
133
95
|
if (headers == null) return [];
|
|
134
96
|
var out = [];
|
|
135
97
|
if (Array.isArray(headers)) {
|
|
136
98
|
headers.forEach(function (h) {
|
|
137
99
|
if (!h || typeof h !== "object") throw _err("smime/bad-input", "each opts.headers entry must be { name, value }");
|
|
138
|
-
// Read name + value once each: an accessor / Proxy could return a different value on a second read, so
|
|
139
|
-
// the snapshot (not the live property) is what both the signed inner header and the outer display copy use.
|
|
140
100
|
var name = h.name, v = h.value;
|
|
141
101
|
if (typeof name !== "string") throw _err("smime/bad-input", "each opts.headers entry must be { name, value }");
|
|
142
102
|
out.push({ name: name, value: v == null ? "" : String(v) });
|
|
@@ -146,17 +106,10 @@ function _hpHeaderList(headers) {
|
|
|
146
106
|
} else {
|
|
147
107
|
throw _err("smime/bad-input", "opts.headers must be an object or an array of { name, value }");
|
|
148
108
|
}
|
|
149
|
-
// Only Non-Structural fields are protected (RFC 9787): a Structural header (Content-Type, MIME-Version, ...)
|
|
150
|
-
// describes the MIME structure -- the library sets it -- so a caller passing one would duplicate it on the
|
|
151
|
-
// payload root. And a REPEATED field name is rejected here at the PRODUCER: a repeated protected field is an
|
|
152
|
-
// unsupported shape (verify fails closed on the ambiguity), so the library never emits a message it cannot
|
|
153
|
-
// re-consume. Reject both; never emit a malformed / self-inconsistent entity.
|
|
154
109
|
var seen = Object.create(null);
|
|
155
110
|
out.forEach(function (h) {
|
|
156
111
|
var ln = h.name.toLowerCase();
|
|
157
112
|
if (_isStructural(ln)) throw _err("smime/bad-input", "opts.headers must not include the Structural header " + JSON.stringify(h.name) + " (RFC 9787); only Non-Structural fields (Subject / From / To / ...) are protected");
|
|
158
|
-
// HP-Outer is a reserved field the library emits itself (RFC 9788 sec. 2.2) to document outer-header
|
|
159
|
-
// confidentiality; a caller-supplied one would be mistaken for that machinery, so reject it.
|
|
160
113
|
if (ln === "hp-outer") throw _err("smime/bad-input", "opts.headers must not include the reserved HP-Outer field (RFC 9788 sec. 2.2); the library emits it");
|
|
161
114
|
if (seen[ln]) throw _err("smime/bad-input", "opts.headers must not repeat a protected field name (" + JSON.stringify(h.name) + "); a repeated protected field is not supported");
|
|
162
115
|
seen[ln] = 1;
|
|
@@ -164,12 +117,6 @@ function _hpHeaderList(headers) {
|
|
|
164
117
|
return out;
|
|
165
118
|
}
|
|
166
119
|
|
|
167
|
-
// The Header Confidentiality Policy for an OUTER header of an encrypted-HP message. hcp_baseline (RFC 9788
|
|
168
|
-
// sec. 3.2.1, the default) obscures Subject to "[...]" and REMOVES Comments / Keywords (null);
|
|
169
|
-
// hcp_no_confidentiality leaves every field visible (opt-in only). This is the sec. 3.2.1 pseudocode
|
|
170
|
-
// Verbatim: Bcc is intentionally not stripped. RFC 9788 sec. 11.4 explains that removing Bcc during
|
|
171
|
-
// encryption can break deliverability to a Bcc'd recipient, so the choice is left to the caller (omit Bcc
|
|
172
|
-
// from opts.headers to keep it out of the plaintext outer headers).
|
|
173
120
|
function _applyHcp(name, value, hcp) {
|
|
174
121
|
if (hcp === "hcp_no_confidentiality") return value;
|
|
175
122
|
var ln = name.toLowerCase();
|
|
@@ -178,8 +125,6 @@ function _applyHcp(name, value, hcp) {
|
|
|
178
125
|
return value;
|
|
179
126
|
}
|
|
180
127
|
|
|
181
|
-
// The outer display header list: for "clear" (signed) the fields are copied verbatim; for "cipher"
|
|
182
|
-
// (encrypted) each passes through the HCP, where a null result removes the field (it lives only in the ciphertext).
|
|
183
128
|
function _outerHeaderList(list, mode, hcp) {
|
|
184
129
|
var policy = hcp == null ? "hcp_baseline" : hcp;
|
|
185
130
|
if (policy !== "hcp_baseline" && policy !== "hcp_no_confidentiality") throw _err("smime/bad-input", "unknown opts.hcp policy " + JSON.stringify(hcp) + " (only \"hcp_baseline\" or \"hcp_no_confidentiality\")");
|
|
@@ -195,39 +140,19 @@ function _outerHeaderList(list, mode, hcp) {
|
|
|
195
140
|
return out;
|
|
196
141
|
}
|
|
197
142
|
|
|
198
|
-
// Build the inner Cryptographic Payload for a header-protected message: the caller's body wrapped with its
|
|
199
|
-
// Content-Type (gaining ; hp="<mode>") + Content-Transfer-Encoding + the protected fields inlined, canonical.
|
|
200
|
-
// hpList is the single normalized snapshot of opts.headers (see the caller), and that same list feeds the signed/
|
|
201
|
-
// encrypted inner header set here and the outer display copy, so an accessor / Proxy cannot make them diverge.
|
|
202
|
-
// outerList is the HCP-processed outer set (_outerHeaderList) -- for a cipher payload it also drives the
|
|
203
|
-
// HP-Outer records embedded here.
|
|
204
143
|
function _protectedInnerEntity(content, opts, mode, hpList, outerList) {
|
|
205
|
-
// Header protection inlines the protected fields on the payload ROOT it builds, so a caller-supplied
|
|
206
|
-
// complete entity (opts.entity) is an unsupported combination: reject it, and never silently bury the
|
|
207
|
-
// caller's entity (its own Content-Type + headers) beneath a new default text/plain wrapper.
|
|
208
144
|
if (opts.entity) throw _err("smime/bad-input", "opts.entity is not supported with protectHeaders -- pass raw content + opts.contentType");
|
|
209
145
|
var raw = guard.bytes.view(content, SmimeError, "smime/bad-input", "content");
|
|
210
|
-
// Header protection sets the hp parameter itself; a caller hp in opts.contentType (with OR without a value)
|
|
211
|
-
// would emit two hp attributes (Content-Type: ...; hp; hp="clear"), an unconsumable non-interoperable
|
|
212
|
-
// message -- reject it here (hasParam catches a bare "hp" that paramCount would miss).
|
|
213
146
|
if (opts.contentType != null && mime.hasParam(String(opts.contentType), "hp")) throw _err("smime/bad-input", "opts.contentType must not set the hp parameter -- header protection sets it (hp=" + JSON.stringify(mode) + ")");
|
|
214
147
|
var ct = (opts.contentType || "text/plain; charset=utf-8") + "; hp=\"" + mode + "\"";
|
|
215
148
|
var fields = [{ name: "Content-Type", value: ct }, { name: "Content-Transfer-Encoding", value: _is7bit(raw) ? "7bit" : "8bit" }];
|
|
216
149
|
hpList.forEach(function (h) { fields.push(h); });
|
|
217
|
-
// RFC 9788 sec. 2.2 / sec. 5.2.1 step 5.v: an ENCRYPTED header-protected payload documents inside the
|
|
218
|
-
// ciphertext, as HP-Outer records, every Non-Structural field it deliberately left visible in the outer
|
|
219
|
-
// (unprotected) header section, as "HP-Outer: <name>: <outer-value>". A field the HCP removed has no HP-Outer
|
|
220
|
-
// record; its absence authenticates that it was made confidential by removal. Signed-only (clear) payloads
|
|
221
|
-
// carry no HP-Outer (sec. 2.2: "not relevant for signed-only messages").
|
|
222
150
|
if (mode === "cipher") {
|
|
223
151
|
outerList.forEach(function (h) { fields.push({ name: "HP-Outer", value: h.name + ": " + h.value }); });
|
|
224
152
|
}
|
|
225
153
|
return mime.buildEntity(fields, raw, SmimeError, "smime/bad-header");
|
|
226
154
|
}
|
|
227
155
|
|
|
228
|
-
// The outer header prefix (display copies + MIME-Version) that precedes the Cryptographic Envelope's
|
|
229
|
-
// Content-Type in a header-protected message. Each field is guard-validated (no CR/LF/NUL injection). list is
|
|
230
|
-
// the same HCP-processed outer set the inner entity's HP-Outer records were built from.
|
|
231
156
|
function _outerPrefix(list) {
|
|
232
157
|
var s = "";
|
|
233
158
|
for (var i = 0; i < list.length; i++) {
|
|
@@ -237,21 +162,11 @@ function _outerPrefix(list) {
|
|
|
237
162
|
return s + "MIME-Version: 1.0\r\n";
|
|
238
163
|
}
|
|
239
164
|
|
|
240
|
-
// Does the payload header region declare an hp Content-Type parameter? A cheap raw scan (unfold the header
|
|
241
|
-
// block, then look for a Content-Type line carrying an hp= parameter) so a NON-HP payload is never parsed --
|
|
242
|
-
// no swallow -- and only a payload that CLAIMS hp is validated (a malformed such payload then fails closed,
|
|
243
|
-
// never a silent downgrade to unprotected).
|
|
244
|
-
// Re-decode a header value that mime.parse latin1-decoded (byte-preserving) as UTF-8 (RFC 6532), so a
|
|
245
|
-
// non-ASCII protected value round-trips intact -- a latin1 string maps 1:1 to bytes, recovering the emitted UTF-8.
|
|
246
165
|
function _utf8Header(s) { return Buffer.from(s, "latin1").toString("utf8"); }
|
|
247
166
|
|
|
248
|
-
// ---- header parsing by character scan (no regex) ----------------------------------------------
|
|
249
|
-
// ASCII lowercase of a code point (A-Z -> a-z), for the case-insensitive "content-type" literal match.
|
|
250
167
|
function _asciiLower(c) { return (c >= 65 && c <= 90) ? c + 32 : c; }
|
|
251
168
|
var _CONTENT_TYPE = "content-type";
|
|
252
169
|
|
|
253
|
-
// RFC 5322 header unfolding: a CRLF followed by one-or-more SP/HTAB (a folded continuation) collapses
|
|
254
|
-
// to a single space (matching /\r\n[ \t]+/g -> " ").
|
|
255
170
|
function _unfoldHeaderFolds(s) {
|
|
256
171
|
var out = "", i = 0, n = s.length;
|
|
257
172
|
while (i < n) {
|
|
@@ -268,9 +183,6 @@ function _unfoldHeaderFolds(s) {
|
|
|
268
183
|
return out;
|
|
269
184
|
}
|
|
270
185
|
|
|
271
|
-
// If the line beginning at `start` is a Content-Type field -- ^[ \t]*content-type[ \t]*: -- return the
|
|
272
|
-
// whole line up to (not including) the next CR or LF (the [^\r\n]* tail); else null. Case-insensitive
|
|
273
|
-
// on the literal, matching the /i flag over the ASCII field name.
|
|
274
186
|
function _contentTypeLineAt(s, start) {
|
|
275
187
|
var n = s.length, p = start;
|
|
276
188
|
while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
|
|
@@ -280,14 +192,12 @@ function _contentTypeLineAt(s, start) {
|
|
|
280
192
|
}
|
|
281
193
|
p += _CONTENT_TYPE.length;
|
|
282
194
|
while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
|
|
283
|
-
if (p >= n || s.charCodeAt(p) !== 0x3a) return null;
|
|
195
|
+
if (p >= n || s.charCodeAt(p) !== 0x3a) return null;
|
|
284
196
|
p += 1;
|
|
285
197
|
while (p < n && s.charCodeAt(p) !== 0x0d && s.charCodeAt(p) !== 0x0a) p += 1;
|
|
286
198
|
return s.slice(start, p);
|
|
287
199
|
}
|
|
288
200
|
|
|
289
|
-
// Every Content-Type field in the header block (the /gim match, /m making ^ match each line start):
|
|
290
|
-
// scan the block start and each position after an LF. Returns the matched lines, or null when none.
|
|
291
201
|
function _contentTypeLines(s) {
|
|
292
202
|
var lines = [], lineStart = 0;
|
|
293
203
|
while (lineStart <= s.length) {
|
|
@@ -300,7 +210,6 @@ function _contentTypeLines(s) {
|
|
|
300
210
|
return lines.length ? lines : null;
|
|
301
211
|
}
|
|
302
212
|
|
|
303
|
-
// The ^[ \t]*content-type[ \t]*: prefix removed from a matched Content-Type line (the value remains).
|
|
304
213
|
function _contentTypeValue(s) {
|
|
305
214
|
var n = s.length, p = 0;
|
|
306
215
|
while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
|
|
@@ -315,105 +224,45 @@ function _contentTypeValue(s) {
|
|
|
315
224
|
}
|
|
316
225
|
|
|
317
226
|
function _declaresHp(content) {
|
|
318
|
-
// content is the CANONICAL entity bytes -- the caller (_hpSurface) runs mime.canonicalizeText ONCE so
|
|
319
|
-
// detection here and the subsequent mime.parse see the identical normalized form (bare CR / LF -> CRLF).
|
|
320
|
-
// Detecting hp on the canonical form -- the bytes the signature covers -- is what stops a fold/line-ending
|
|
321
|
-
// transport rewrite from stripping the hp signal from a still-valid signed message.
|
|
322
227
|
var s = content.toString("latin1");
|
|
323
228
|
var sep = s.indexOf("\r\n\r\n");
|
|
324
229
|
var head = _unfoldHeaderFolds(sep !== -1 ? s.slice(0, sep) : s);
|
|
325
|
-
// Detect hp by composing mime.paramCount, the same comment- + quoted-string-aware tokenizer the parser
|
|
326
|
-
// uses -- over each Content-Type field, so the probe never diverges from mime.parse: a leading-whitespace
|
|
327
|
-
// or before-colon field name is matched (the parser trims it), while an hp= inside a MIME comment or a
|
|
328
|
-
// quoted value is not a parameter (so an ordinary Content-Type cannot opt a message into HP processing). A
|
|
329
|
-
// real hp on ANY Content-Type line is detected; a duplicate Content-Type is rejected downstream.
|
|
330
230
|
var cts = _contentTypeLines(head);
|
|
331
231
|
if (!cts) return false;
|
|
332
232
|
for (var i = 0; i < cts.length; i++) {
|
|
333
|
-
// hasParam (not paramCount): a payload that names the hp attribute without a value ("...; hp") still
|
|
334
|
-
// CLAIMS header protection -- detect it so _hpSurface fails closed on the malformed declaration rather
|
|
335
|
-
// than silently downgrading it to unprotected.
|
|
336
233
|
if (mime.hasParam(_contentTypeValue(cts[i]), "hp")) return true;
|
|
337
234
|
}
|
|
338
235
|
return false;
|
|
339
236
|
}
|
|
340
237
|
|
|
341
|
-
// A fresh not-protected surface (returned when no header protection is present or detection fails soft). A NEW
|
|
342
|
-
// object each call -- the caller Object.assign()s it onto the verify/decrypt result, so it must not be shared.
|
|
343
|
-
// `present` + `protectedHeaders` describe only cryptographically-declared (hp=) protection; `legacy` is null
|
|
344
|
-
// unless an opt-in legacy RFC8551HP inference succeeded (then it carries its own headers/mode/etc.; see below).
|
|
345
|
-
// No header protection means no protected From to compare the outer one against, so the
|
|
346
|
-
// mismatch question is unanswered rather than answered "no" -- null, matching every other
|
|
347
|
-
// field here that reports an absent property.
|
|
348
238
|
function _noneSurface() { return { protectedHeaders: null, headerProtection: { present: false, mode: null, fromMismatch: null, confidential: [], legacy: null } }; }
|
|
349
239
|
|
|
350
|
-
// Count the Content-Type fields in a parsed header list. A structure with more than one is AMBIGUOUS: mime.parse
|
|
351
|
-
// surfaces only the first via contentType, so an hp= parameter or a Cryptographic-Layer media type on a later
|
|
352
|
-
// field would be invisible to the classification, and both the standard HP path and the legacy path fail closed on it.
|
|
353
240
|
function _contentTypeCount(headers) { var n = 0; headers.forEach(function (h) { if (h.lname === "content-type") n++; }); return n; }
|
|
354
241
|
|
|
355
|
-
// Header fields that must not sensibly repeat: the RFC 5322 sec. 3.6 max-1 originator/destination/identification
|
|
356
|
-
// fields, plus Return-Path (the envelope sender; RFC 5322 sec. 3.6 groups it under trace, but RFC 5321 sec. 4.4
|
|
357
|
-
// restricts a delivered message to a single one). A duplicate of one of these is a malformed / ambiguous set (a
|
|
358
|
-
// consumer could pick a different sender / subject); a duplicate of any OTHER field (Received and the other
|
|
359
|
-
// trace fields, Resent-*, Comments, Keywords, and optional / X-* fields) is legitimately repeatable in a real
|
|
360
|
-
// message. A Set (not a plain object) so an attacker-chosen field name like "constructor" / "__proto__" cannot
|
|
361
|
-
// match an inherited Object.prototype member and false-trip the singleton reject.
|
|
362
242
|
var RFC5322_SINGLETON = new Set(["date", "from", "sender", "reply-to", "to", "cc", "bcc", "message-id", "in-reply-to", "references", "subject", "return-path"]);
|
|
363
243
|
|
|
364
|
-
// Extract the Non-Structural protected fields from a recovered inner header list. Shared by the standard HP path
|
|
365
|
-
// (over the Cryptographic Payload root) and the legacy path (over the message/rfc822 inner message, sec. 4.10).
|
|
366
|
-
// Returns { protectedHeaders (UTF-8 map, last-wins, the ergonomic single-value view surfaced by the standard
|
|
367
|
-
// path), entries (an ordered array of every { name, value, raw } occurrence, since a real message repeats trace
|
|
368
|
-
// fields, and a last-wins collapse would drop all but one), innerFrom, refouter (HP-Outer records), dup (any
|
|
369
|
-
// duplicate field name), dupSingleton (a duplicate of an RFC 5322 singleton field) }.
|
|
370
244
|
function _extractProtected(headerList) {
|
|
371
|
-
// mime.parse decodes the header block as latin1 (byte-preserving), so a protected value emitted as UTF-8
|
|
372
|
-
// (RFC 6532, the guard permits it) is re-decoded latin1->UTF-8 here to round-trip intact. protectedHeaders
|
|
373
|
-
// surfaces the exact authenticated field body (rawValue: leading/trailing whitespace preserved), and not a
|
|
374
|
-
// trimmed value that would diverge from the signed octets.
|
|
375
245
|
var protectedHeaders = Object.create(null), innerFrom = null, seen = Object.create(null), dup = null, dupSingleton = null;
|
|
376
|
-
var entries = [];
|
|
377
|
-
var refouter = [];
|
|
246
|
+
var entries = [];
|
|
247
|
+
var refouter = [];
|
|
378
248
|
headerList.forEach(function (h) {
|
|
379
249
|
if (_isStructural(h.lname)) return;
|
|
380
250
|
if (h.lname === "hp-outer") {
|
|
381
|
-
// RFC 9788 sec. 4.2.1 step 4.i: split the HP-Outer value on the first colon into (name, outer-value)
|
|
382
|
-
// -> refouter. The value is kept BYTE-PRESERVING (latin1), never UTF-8-decoded, because the sec. 4.3.1
|
|
383
|
-
// confidentiality comparison must be octet-exact (a lossy decode maps distinct invalid octets 0x80/0x81
|
|
384
|
-
// to one replacement char, which would mis-classify an obscured field as exposed and drop it from the
|
|
385
|
-
// confidential set). HP-Outer is never surfaced as a protected header nor counted toward the duplicate
|
|
386
|
-
// check (sec. 2.2: it "can appear multiple times"). A valueless HP-Outer (no inner colon) is ignored.
|
|
387
251
|
var ci = h.rawValue.indexOf(":");
|
|
388
252
|
if (ci >= 0) refouter.push({ name: h.rawValue.slice(0, ci).trim().toLowerCase(), value: h.rawValue.slice(ci + 1).trim() });
|
|
389
253
|
return;
|
|
390
254
|
}
|
|
391
|
-
var val = _utf8Header(h.rawValue);
|
|
392
|
-
entries.push({ name: h.name, value: val, raw: h.rawValue });
|
|
255
|
+
var val = _utf8Header(h.rawValue);
|
|
256
|
+
entries.push({ name: h.name, value: val, raw: h.rawValue });
|
|
393
257
|
if (seen[h.lname]) { dup = h.name; if (RFC5322_SINGLETON.has(h.lname)) dupSingleton = h.name; }
|
|
394
258
|
seen[h.lname] = 1;
|
|
395
|
-
protectedHeaders[h.name] = val;
|
|
396
|
-
// innerFrom uses the byte-preserving value: a lossy UTF-8 decode maps distinct invalid 8-bit sequences (0x80
|
|
397
|
-
// vs 0x81) to the same replacement char, which would let an attacker alter the displayed From without
|
|
398
|
-
// tripping fromMismatch. Compare the raw octets instead.
|
|
259
|
+
protectedHeaders[h.name] = val;
|
|
399
260
|
if (h.lname === "from") innerFrom = h.value;
|
|
400
261
|
});
|
|
401
262
|
return { protectedHeaders: protectedHeaders, entries: entries, innerFrom: innerFrom, refouter: refouter, dup: dup, dupSingleton: dupSingleton };
|
|
402
263
|
}
|
|
403
264
|
|
|
404
|
-
// RFC 9788 sec. 4.3.1: a protected field occurrence is end-to-end confidential (encrypted-only) unless a
|
|
405
|
-
// `refouter` record carries its EXACT name + value, meaning that value was copied verbatim to the visible outer
|
|
406
|
-
// section. The match is per-OCCURRENCE and MULTISET (each outer record accounts for at most one inside
|
|
407
|
-
// occurrence), so a legally-repeated field with one obscured value and one exposed value (Keywords: secret then
|
|
408
|
-
// Keywords: public, outer exposing only "public") still reports the name confidential, because a last-wins collapse
|
|
409
|
-
// would drop the hidden "secret" and let a caller leak it. A field name is confidential if ANY of its
|
|
410
|
-
// occurrences is unexposed. The comparison is octet-exact (latin1 raw), never lossy-decoded. For the standard
|
|
411
|
-
// path `refouter` is the HP-Outer records; for the legacy path it is the actual outer Header Section (part A).
|
|
412
265
|
function _computeConfidential(entries, refouter) {
|
|
413
|
-
// Index the outer occurrences by (normalized name -> value -> available count) so the multiset comparison is
|
|
414
|
-
// O(N + M), not a per-occurrence rescan of the outer array: matching N identical exposed occurrences would
|
|
415
|
-
// otherwise be N(N+1)/2 comparisons: a DoS an attacker could drive toward the 16 MiB section cap. A two-level
|
|
416
|
-
// Map (never a plain object) so an attacker-chosen name/value like "__proto__" cannot confuse the lookup.
|
|
417
266
|
var byName = new Map();
|
|
418
267
|
refouter.forEach(function (r) {
|
|
419
268
|
var m = byName.get(r.name); if (!m) { m = new Map(); byName.set(r.name, m); }
|
|
@@ -422,110 +271,48 @@ function _computeConfidential(entries, refouter) {
|
|
|
422
271
|
var confidential = [], seen = Object.create(null);
|
|
423
272
|
entries.forEach(function (e) {
|
|
424
273
|
var m = byName.get(e.name.toLowerCase()), val = e.raw.trim(), n = (m && m.get(val)) || 0;
|
|
425
|
-
if (n > 0) { m.set(val, n - 1); return; }
|
|
274
|
+
if (n > 0) { m.set(val, n - 1); return; }
|
|
426
275
|
if (!seen[e.name]) { seen[e.name] = 1; confidential.push(e.name); }
|
|
427
276
|
});
|
|
428
277
|
return confidential;
|
|
429
278
|
}
|
|
430
279
|
|
|
431
|
-
// Inspect every outer From (outerEnt.header returns only the first; an attacker may append a second, forged From
|
|
432
|
-
// that a MUA displays). A mismatch is ANYTHING but exactly one outer From equal to the protected one: a removed
|
|
433
|
-
// outer From, a duplicate, or a differing value all flag tampering of the displayed sender.
|
|
434
|
-
// null when there was nothing to compare, true/false only when a comparison ran. A bare
|
|
435
|
-
// false cannot distinguish "the outer From agrees with the protected one" from "no inner
|
|
436
|
-
// From was protected, so no comparison happened" -- and the second is the common case,
|
|
437
|
-
// which is how `!fromMismatch` came to read as a passed check on a message where nothing
|
|
438
|
-
// was checked at all.
|
|
439
280
|
function _computeFromMismatch(innerFrom, outerEnt) {
|
|
440
281
|
if (innerFrom == null) return null;
|
|
441
282
|
var outerFroms = [];
|
|
442
|
-
outerEnt.headers.forEach(function (h) { if (h.lname === "from") outerFroms.push(h.value.trim()); });
|
|
283
|
+
outerEnt.headers.forEach(function (h) { if (h.lname === "from") outerFroms.push(h.value.trim()); });
|
|
443
284
|
return outerFroms.length !== 1 || outerFroms[0] !== innerFrom.trim();
|
|
444
285
|
}
|
|
445
286
|
|
|
446
|
-
// Is this (lowercased) media type a Cryptographic Layer (RFC 9788 sec. 4.10.1)? Conservative: any
|
|
447
|
-
// application/pkcs7-mime (incl. an OpenSSL x-pkcs7-mime, or a compressed-data layer) plus multipart/signed and
|
|
448
|
-
// multipart/encrypted. Mis-classifying part D as a layer only degrades legacy detection to none, the safe direction.
|
|
449
287
|
function _isCryptoLayer(type) {
|
|
450
|
-
// `type` is the already-lowercased media type from mime.parse's _parseStructured; _isPkcs7 guards the
|
|
451
|
-
// null/undefined case internally, so no local `|| ""` fallback is needed (and no dead branch is introduced).
|
|
452
288
|
return _isPkcs7(type, "mime") || type === "multipart/signed" || type === "multipart/encrypted";
|
|
453
289
|
}
|
|
454
290
|
|
|
455
|
-
// The legacy `refouter`: the actual visible outer Header Section (part A) as name+value records, so the sec.
|
|
456
|
-
// 4.3.1 confidentiality comparison runs against the real outer values (a legacy message carries no HP-Outer).
|
|
457
291
|
function _outerRefouter(outerEnt) {
|
|
458
292
|
var r = [];
|
|
459
293
|
outerEnt.headers.forEach(function (h) { if (!_isStructural(h.lname) && h.lname !== "hp-outer") r.push({ name: h.lname, value: h.rawValue.trim() }); });
|
|
460
294
|
return r;
|
|
461
295
|
}
|
|
462
296
|
|
|
463
|
-
// RFC 9788 sec. 4.10 backward compatibility: identify + surface a legacy RFC8551HP message, a Cryptographic
|
|
464
|
-
// Envelope whose payload is a bare message/rfc822 object carrying the real headers, with NO hp= parameter. This
|
|
465
|
-
// is detect-only (sec. 4.10: "An MUA MUST NOT generate an RFC8551HP message ... MAY try to render") and opt-in
|
|
466
|
-
// (`enabled`). Identification (sec. 4.10.1) is the four conjunctive conditions C1-C4; every failure (an
|
|
467
|
-
// unparseable inner message, a non-message/rfc822 payload, a nested Cryptographic Layer, an hp= on part D, or an
|
|
468
|
-
// ambiguous/empty protected set -- fails SOFT to none (never throws), because the message is not PRECISELY
|
|
469
|
-
// identified. On identification (sec. 4.10.2) the inner message's (part D's) Non-Structural fields are surfaced,
|
|
470
|
-
// the mode is inferred from the envelope (clear for signed, cipher for encrypted), and the confidential set is
|
|
471
|
-
// derived from the actual outer section (part A). CRUCIAL: a legacy RFC8551HP message is structurally
|
|
472
|
-
// INDISTINGUISHABLE from an ordinary signed/forwarded message/rfc822 (sec. 4.10.2: the inference is "not based
|
|
473
|
-
// on any strong end-to-end guarantees"), so the inferred set is never placed in `protectedHeaders` and never
|
|
474
|
-
// sets `present:true`; those describe only cryptographically-declared hp= protection a caller may trust. The
|
|
475
|
-
// inference is surfaced only under `headerProtection.legacy` (its own { headers, mode, fromMismatch, confidential }
|
|
476
|
-
// object), so a caller keying trust off `present` / `protectedHeaders` can never mistake an opted-in heuristic --
|
|
477
|
-
// possibly a forwarded attachment -- for this message's authenticated headers; consuming it is an explicit choice.
|
|
478
297
|
function _legacyHpSurface(canon, outerEnt, mode, enabled) {
|
|
479
298
|
if (!enabled) return _noneSurface();
|
|
480
299
|
var partC, partD;
|
|
481
300
|
try {
|
|
482
|
-
// C1 (outermost object is a Cryptographic Layer) holds by construction: _hpSurface only reaches here after a
|
|
483
|
-
// successful, AUTHENTICATED verify/decrypt. C2: the Cryptographic Payload must be a SINGLE message/rfc822 object.
|
|
484
301
|
partC = mime.parse(canon, SmimeError, "smime/bad-header-protection");
|
|
485
302
|
if (partC.contentType.type !== "message/rfc822") return _noneSurface();
|
|
486
303
|
partD = mime.parse(partC.body, SmimeError, "smime/bad-header-protection");
|
|
487
|
-
} catch (_e) { return _noneSurface(); }
|
|
488
|
-
// The C2-C4 classification reads only the first Content-Type of each part (mime.parse surfaces that one). A
|
|
489
|
-
// duplicate Content-Type on part C or part D is ambiguous, since a later field could carry hp= or a
|
|
490
|
-
// Cryptographic Layer media type the checks below would miss. Fail soft there, matching the standard path's
|
|
491
|
-
// duplicate reject.
|
|
304
|
+
} catch (_e) { return _noneSurface(); }
|
|
492
305
|
if (_contentTypeCount(partC.headers) > 1 || _contentTypeCount(partD.headers) > 1) return _noneSurface();
|
|
493
|
-
// C4: an hp= on part D means the message is not legacy (sec. 4.1: a consumer MUST ignore hp outside the payload
|
|
494
|
-
// root). C3: part D must not itself be a Cryptographic Layer (a genuinely nested signed/encrypted message).
|
|
495
306
|
if (mime.hasParam(partD.contentType.value, "hp")) return _noneSurface();
|
|
496
307
|
if (_isCryptoLayer(partD.contentType.type)) return _noneSurface();
|
|
497
308
|
var ex = _extractProtected(partD.headers);
|
|
498
|
-
// Only a duplicate SINGLETON field (RFC 5322 sec. 3.6) is the ambiguous case. Part D is an ordinary received
|
|
499
|
-
// message whose trace fields (Received, ...) and other repeatable fields legitimately recur, so a repeatable
|
|
500
|
-
// duplicate must not reject the inference (else legacy detection fails on essentially all delivered mail).
|
|
501
309
|
if (ex.dupSingleton) return _noneSurface();
|
|
502
|
-
if (!ex.entries.length) return _noneSurface();
|
|
310
|
+
if (!ex.entries.length) return _noneSurface();
|
|
503
311
|
var confidential = mode === "cipher" ? _computeConfidential(ex.entries, _outerRefouter(outerEnt)) : [];
|
|
504
|
-
// present stays false + protectedHeaders stays null: the inferred set is surfaced only under `legacy` so a
|
|
505
|
-
// caller cannot mistake an opt-in heuristic (indistinguishable from a forwarded message/rfc822) for authenticated
|
|
506
|
-
// headers. `headers` is the ORDERED [{ name, value }] list (every occurrence; a real message repeats trace
|
|
507
|
-
// fields); the internal `raw` per occurrence is dropped from the public shape.
|
|
508
312
|
var headers = ex.entries.map(function (e) { return { name: e.name, value: e.value }; });
|
|
509
|
-
// The OUTER fromMismatch stays unanswered here: a legacy wrap is an inference, never
|
|
510
|
-
// declared protection, so the top-level field must not report a comparison the message
|
|
511
|
-
// did not authenticate. The legacy block carries its own, clearly opt-in.
|
|
512
313
|
return { protectedHeaders: null, headerProtection: { present: false, mode: null, fromMismatch: null, confidential: [], legacy: { headers: headers, mode: mode, fromMismatch: _computeFromMismatch(ex.innerFrom, outerEnt), confidential: confidential } } };
|
|
513
314
|
}
|
|
514
315
|
|
|
515
|
-
// Detect + surface RFC 9788 header protection on a recovered inner entity, FAIL-CLOSED. A payload that does
|
|
516
|
-
// not declare hp surfaces protectedHeaders:null, unless opts.legacyHeaderProtection is set and it is a legacy
|
|
517
|
-
// RFC8551HP message/rfc822 wrap (sec. 4.10), which _legacyHpSurface then surfaces (opt-in, fail-soft). A payload
|
|
518
|
-
// that DECLARES hp is validated: a malformed block, an invalid hp value, or an hp mode that CONTRADICTS the
|
|
519
|
-
// cryptographic envelope (expectedMode "clear" for a signed message, "cipher" for a decrypted one) throws
|
|
520
|
-
// smime/bad-header-protection -- never a silent downgrade. Otherwise the inline Non-Structural fields ARE the
|
|
521
|
-
// authenticated set; a From that differs from the untrusted OUTER From is flagged fromMismatch.
|
|
522
|
-
// The email identities a signer certificate actually asserts. RFC 8550 sec. 4.4.3 makes
|
|
523
|
-
// subjectAltName the carrier: an ASCII local-part rides the rfc822Name CHOICE, and a
|
|
524
|
-
// certificate MAY assert several addresses, so this collects every one rather than the
|
|
525
|
-
// first. A non-ASCII local-part rides otherName / SmtpUTF8Mailbox (RFC 8398 sec. 3); those
|
|
526
|
-
// are counted as PRESENT but not returned as comparable strings, so a certificate that
|
|
527
|
-
// carries only internationalized identities reports "there is an identity here that this
|
|
528
|
-
// comparison cannot read" instead of the indistinguishable "no identities".
|
|
529
316
|
var SAN_OID = oid.byName("subjectAltName");
|
|
530
317
|
var SMTP_UTF8_MAILBOX = oid.byName("smtpUtf8Mailbox");
|
|
531
318
|
var EMAIL_ADDRESS_ATTR = oid.byName("emailAddress");
|
|
@@ -535,11 +322,6 @@ var _extCtx = { E: function (c, m, cause) { return new SmimeError(c, m, cause);
|
|
|
535
322
|
function _signerEmails(certDer) {
|
|
536
323
|
var out = { addresses: [], unreadable: 0 };
|
|
537
324
|
var parsed = null;
|
|
538
|
-
// A failure here is COUNTED, never absorbed: it raises `unreadable`, which drives the
|
|
539
|
-
// sender verdict to null (undecidable) rather than to a clean "no identities". Returning
|
|
540
|
-
// early on the error would make an unreadable certificate indistinguishable from one
|
|
541
|
-
// that genuinely asserts no address, and the caller would read "nothing to match" as a
|
|
542
|
-
// finished comparison instead of one that never happened.
|
|
543
325
|
try { parsed = schemaX509.parse(certDer); }
|
|
544
326
|
catch (parseFailed) { out.unreadable++; out.reason = parseFailed.code || "unparseable"; }
|
|
545
327
|
if (parsed === null) return out;
|
|
@@ -550,39 +332,13 @@ function _signerEmails(certDer) {
|
|
|
550
332
|
catch (sanFailed) { out.unreadable++; out.reason = sanFailed.code || "bad-san"; }
|
|
551
333
|
if (dec === null) return;
|
|
552
334
|
(dec.names || []).forEach(function (n) {
|
|
553
|
-
if (n.tagNumber === 1) { out.addresses.push(String(n.value)); return; }
|
|
554
|
-
// An otherName only bears on the EMAIL question when it is an SmtpUTF8Mailbox
|
|
555
|
-
// (RFC 8398 sec. 3). A certificate may legitimately carry unrelated otherNames -- a
|
|
556
|
-
// Microsoft UPN is routine in enterprise issuance -- and counting those as an
|
|
557
|
-
// unreadable identity would drive a certificate whose rfc822Name matches perfectly
|
|
558
|
-
// to an undecidable verdict, making expectedSender unusable for that whole class.
|
|
335
|
+
if (n.tagNumber === 1) { out.addresses.push(String(n.value)); return; }
|
|
559
336
|
if (n.tagNumber !== 0) return;
|
|
560
|
-
// The decoder builds otherName as value: { typeId, valueBytes } (schema-pkix's
|
|
561
|
-
// GeneralName rule), so the type-id lives one level in. Reading it off the node
|
|
562
|
-
// itself silently matched nothing, which made every SmtpUTF8Mailbox invisible and
|
|
563
|
-
// let a legacy subject value speak for a certificate whose SAN was authoritative.
|
|
564
|
-
// An otherName whose type-id cannot be read at all counts as unreadable: unknown
|
|
565
|
-
// shape is a reason to withhold a verdict, not to ignore the entry.
|
|
566
337
|
var typeId = n.value && typeof n.value === "object" ? n.value.typeId : undefined;
|
|
567
338
|
if (typeId === undefined) { out.unreadable++; return; }
|
|
568
339
|
if (String(typeId) === SMTP_UTF8_MAILBOX) out.unreadable++;
|
|
569
340
|
});
|
|
570
341
|
});
|
|
571
|
-
// RFC 8550 sec. 3: the address SHOULD live in subjectAltName, but a receiving agent
|
|
572
|
-
// "MUST recognize email addresses in the distinguished name field in the PKCS #9
|
|
573
|
-
// emailAddress attribute". Reading only the extension would leave every legacy
|
|
574
|
-
// certificate of that shape permanently undecidable, so the subject DN is consulted too.
|
|
575
|
-
// It is a fallback, not a peer: a SAN address is the conformant carrier.
|
|
576
|
-
// A fallback, not a peer: consulted ONLY when subjectAltName yielded no address. RFC 8550
|
|
577
|
-
// sec. 3 says the address SHOULD be in the extension and SHOULD NOT be in the subject, so
|
|
578
|
-
// where both exist the extension is authoritative. Merging them would let a stale or
|
|
579
|
-
// conflicting subject value satisfy expectedSender while the SAN names a different
|
|
580
|
-
// mailbox -- a positive binding drawn from the weaker of two disagreeing identities.
|
|
581
|
-
// "No SAN email identity" means none of EITHER kind. An SmtpUTF8Mailbox raises
|
|
582
|
-
// `unreadable` without adding a comparable address, so testing `addresses` alone would
|
|
583
|
-
// fall through to the subject for exactly the certificates whose SAN does carry an
|
|
584
|
-
// authoritative identity -- letting a stale subject value override an internationalized
|
|
585
|
-
// SAN this toolkit simply cannot read.
|
|
586
342
|
var sanCarriedIdentity = out.addresses.length > 0 || out.unreadable > 0;
|
|
587
343
|
var rdns = !sanCarriedIdentity && parsed.subject ? parsed.subject.rdns : null;
|
|
588
344
|
(rdns || []).forEach(function (rdn) {
|
|
@@ -594,25 +350,8 @@ function _signerEmails(certDer) {
|
|
|
594
350
|
return out;
|
|
595
351
|
}
|
|
596
352
|
|
|
597
|
-
// What the signer's certificate says about who sent this, and whether anyone asked.
|
|
598
|
-
//
|
|
599
|
-
// `match` is THREE-valued and never collapses: true only when an identity in the signer's
|
|
600
|
-
// certificate matches under RFC 5280 sec. 7.5; false when every identity was comparable and
|
|
601
|
-
// none matched; null when the question was not asked, or was asked and could not be decided
|
|
602
|
-
// (no readable identity, or an address needing an IDNA transform this toolkit will not do).
|
|
603
|
-
// A boolean here would make "nobody checked" indistinguishable from "checked and agreed",
|
|
604
|
-
// which is the defect this field exists to remove -- so a caller enforcing sender binding
|
|
605
|
-
// tests `match === true`, and null fails that test exactly as false does.
|
|
606
|
-
//
|
|
607
|
-
// `expectedSender` is the authoritative input. The outer From is attacker-controlled on an
|
|
608
|
-
// unprotected message, so comparing against it is reported as advisory (`source: "from"`)
|
|
609
|
-
// and never presented as a verified binding.
|
|
610
353
|
function _senderSurface(signers, outerEnt, expectedSender) {
|
|
611
354
|
var expected = null, source = null;
|
|
612
|
-
// Entry-tier: a mistyped option is a caller bug and throws, never a coercion. String()
|
|
613
|
-
// would accept any object with a toString, so `{ toString: () => "alice@example.com" }`
|
|
614
|
-
// would drive a sender binding the caller never actually stated -- the authoritative
|
|
615
|
-
// input to this comparison must be the string it claims to be.
|
|
616
355
|
if (expectedSender != null) {
|
|
617
356
|
if (typeof expectedSender !== "string") {
|
|
618
357
|
throw _err("smime/bad-input", "expectedSender must be a string email address, got " + typeof expectedSender);
|
|
@@ -622,53 +361,24 @@ function _senderSurface(signers, outerEnt, expectedSender) {
|
|
|
622
361
|
else {
|
|
623
362
|
var froms = [];
|
|
624
363
|
outerEnt.headers.forEach(function (h) { if (h.lname === "from") froms.push(h.value.trim()); });
|
|
625
|
-
// Exactly one From, or there is no unambiguous address to compare against.
|
|
626
364
|
if (froms.length === 1) { expected = _addrSpec(froms[0]); source = expected === null ? null : "from"; }
|
|
627
365
|
}
|
|
628
366
|
var identities = [], undecidable = false, sawIdentity = false;
|
|
629
367
|
(signers || []).forEach(function (s) {
|
|
630
|
-
// Only a signer whose signature actually verified may contribute an identity.
|
|
631
|
-
// cms.verify reports every matched signer, including ones that failed, as ok:false --
|
|
632
|
-
// harvesting their subjectAltName would let a tampered message report valid:false
|
|
633
|
-
// alongside sender.match:true, and this verb's own guidance is to enforce
|
|
634
|
-
// sender.match === true. A failed signer makes the answer undecidable, never a clean
|
|
635
|
-
// no-match.
|
|
636
|
-
// Only a signer whose signature actually verified may contribute an identity.
|
|
637
|
-
// cms.verify reports every matched signer, including ones that failed, as ok:false --
|
|
638
|
-
// harvesting their subjectAltName would let a tampered message report valid:false
|
|
639
|
-
// alongside sender.match:true, and this verb's own guidance is to enforce
|
|
640
|
-
// sender.match === true. A failed signer makes the answer undecidable, never a clean
|
|
641
|
-
// no-match.
|
|
642
|
-
// A verified signature is not enough: anyone can self-sign a certificate carrying the
|
|
643
|
-
// victim's address, and cms.verify reports that signer ok:true / trusted:false. Reading
|
|
644
|
-
// its subjectAltName would let an attacker-minted certificate produce a positive
|
|
645
|
-
// binding, which is the same defect one layer up from the ok:false case. Name your
|
|
646
|
-
// roots in opts.trustAnchors; with none supplied nothing is trusted, so the binding
|
|
647
|
-
// question stays unanswered rather than answered from an unvetted certificate.
|
|
648
368
|
if (!s || s.ok !== true || s.trusted !== true) { undecidable = true; return; }
|
|
649
369
|
var e = s.cert ? _signerEmails(s.cert) : null;
|
|
650
|
-
if (e === null) { undecidable = true; return; }
|
|
370
|
+
if (e === null) { undecidable = true; return; }
|
|
651
371
|
if (e.unreadable > 0) { undecidable = true; sawIdentity = true; }
|
|
652
372
|
e.addresses.forEach(function (a) { sawIdentity = true; if (identities.indexOf(a) === -1) identities.push(a); });
|
|
653
373
|
});
|
|
654
374
|
if (expected === null) return { checked: false, expected: null, source: null, identities: identities, match: null };
|
|
655
375
|
if (!sawIdentity) return { checked: true, expected: expected, source: source, identities: identities, match: null };
|
|
656
|
-
// No early return on the first match. A match is only the answer once nothing else has
|
|
657
|
-
// made the result undecidable: a failed co-signer, an unreadable certificate or an
|
|
658
|
-
// address this toolkit cannot canonicalize all mean the verdict is unsettled, and
|
|
659
|
-
// reporting true from inside the loop would let that be read as a clean binding.
|
|
660
|
-
// Undecidable always wins over a positive.
|
|
661
376
|
var sawMatch = false, sawNotComparable = false;
|
|
662
377
|
for (var i = 0; i < identities.length; i++) {
|
|
663
378
|
var v = guard.name.emailEqual(identities[i], expected);
|
|
664
379
|
if (v === "match") sawMatch = true;
|
|
665
380
|
else if (v === "not-comparable") sawNotComparable = true;
|
|
666
381
|
}
|
|
667
|
-
// The question is existential: does a verified signer's certificate assert this address?
|
|
668
|
-
// An exact match answers it, and a second identity that could not be compared does not
|
|
669
|
-
// un-answer it -- a certificate legitimately carrying several addresses would otherwise
|
|
670
|
-
// fail the binding because of one it was never asked about. Undecidability only decides
|
|
671
|
-
// the verdict when nothing matched.
|
|
672
382
|
var verdict;
|
|
673
383
|
if (sawMatch) verdict = true;
|
|
674
384
|
else if (undecidable || sawNotComparable) verdict = null;
|
|
@@ -676,75 +386,34 @@ function _senderSurface(signers, outerEnt, expectedSender) {
|
|
|
676
386
|
return { checked: true, expected: expected, source: source, identities: identities, match: verdict };
|
|
677
387
|
}
|
|
678
388
|
|
|
679
|
-
// The addr-spec inside an RFC 5322 From. A display-name form ("Bob <bob@x>") yields the
|
|
680
|
-
// angle-addr; a bare address yields itself. Anything else (a group, a second address, an
|
|
681
|
-
// unclosed angle bracket) returns null so the caller reports "not compared" rather than
|
|
682
|
-
// comparing against a fragment.
|
|
683
389
|
function _addrSpec(v) {
|
|
684
390
|
var lt = v.indexOf("<"), gt = v.lastIndexOf(">");
|
|
685
391
|
if (lt === -1 && gt === -1) return v.indexOf(",") === -1 ? v.trim() : null;
|
|
686
392
|
if (lt === -1 || gt < lt) return null;
|
|
687
|
-
// Nothing but whitespace may follow the angle address. `Alice <alice@x>, mallory@y` is two
|
|
688
|
-
// mailboxes, and returning the first would report a single compared sender over a field
|
|
689
|
-
// that named more than one -- the ambiguity this function exists to refuse.
|
|
690
393
|
if (v.slice(gt + 1).trim().length) return null;
|
|
691
394
|
var inner = v.slice(lt + 1, gt).trim();
|
|
692
395
|
return inner.length && inner.indexOf(",") === -1 && inner.indexOf("<") === -1 ? inner : null;
|
|
693
396
|
}
|
|
694
397
|
|
|
695
398
|
function _hpSurface(content, outerEnt, expectedMode, authenticated, legacyEnabled) {
|
|
696
|
-
// Header protection is an AUTHENTICATED property: surface the inner headers as protected only when the
|
|
697
|
-
// cryptographic verdict succeeded (a valid signature, or an authenticated-encryption decrypt). An invalid
|
|
698
|
-
// signature or an unauthenticated (AES-CBC, no integrity) decrypt yields attacker-influenced bytes, and those
|
|
699
|
-
// are never marked or exposed as protected (a caller must not treat protectedHeaders as a trust signal
|
|
700
|
-
// unless integrity held).
|
|
701
399
|
if (!authenticated) return _noneSurface();
|
|
702
|
-
// Detect + parse the CANONICAL entity: the exact bytes the signature covers. A transport may rewrite a
|
|
703
|
-
// CRLF fold OR the header/body separator to bare CR/LF; canonicalize repairs them (so verification still
|
|
704
|
-
// succeeds), and both the hp detection and the parse must run on the repaired bytes, or they diverge from
|
|
705
|
-
// the signed content -- stripping the signal, or false-rejecting a valid message as an unparseable block.
|
|
706
|
-
// The returned `content` stays the raw recovered bytes; only this HP inspection uses the canonical copy.
|
|
707
400
|
var canon = mime.canonicalizeText(content);
|
|
708
|
-
// A payload that does not declare hp is either non-protected (protectedHeaders:null) or a legacy RFC8551HP
|
|
709
|
-
// message/rfc822 wrap the caller opted into detecting (sec. 4.10). Both are mutually exclusive with the
|
|
710
|
-
// standard path below by construction: it runs only when a real hp= is present on the payload root.
|
|
711
401
|
if (!_declaresHp(canon)) return _legacyHpSurface(canon, outerEnt, expectedMode, legacyEnabled);
|
|
712
|
-
var inner = mime.parse(canon, SmimeError, "smime/bad-header-protection");
|
|
713
|
-
// A duplicate Content-Type makes the hp declaration ambiguous (a parser reads params from the first field,
|
|
714
|
-
// but an hp= may sit on a later one), so the wrap is malformed and fails closed, never a silent downgrade.
|
|
402
|
+
var inner = mime.parse(canon, SmimeError, "smime/bad-header-protection");
|
|
715
403
|
var ctCount = _contentTypeCount(inner.headers);
|
|
716
404
|
if (ctCount > 1) throw _err("smime/bad-header-protection", "a header-protected payload must carry exactly one Content-Type field (found " + ctCount + ")");
|
|
717
|
-
// A duplicate hp attribute is ambiguous (mime.parse keeps the last value, but a recipient honoring the
|
|
718
|
-
// first would see a different mode); fail closed, like the duplicate Content-Type. Counted by attribute
|
|
719
|
-
// name (bare OR valued) so a bare+valued pair ("hp; hp=x") is caught too, and quote-/comment-aware (an hp=
|
|
720
|
-
// inside a quoted value or comment is not the hp parameter, so a message we emit is never self-rejected).
|
|
721
405
|
if (mime.paramNameCount(inner.contentType.value, "hp") > 1) throw _err("smime/bad-header-protection", "a header-protected payload declares the hp parameter more than once");
|
|
722
|
-
// _declaresHp composes the same comment/quoted-string-aware tokenizer (mime.paramCount) mime.parse uses, so
|
|
723
|
-
// reaching here means the single Content-Type carries a real hp parameter (a duplicate is rejected above) --
|
|
724
|
-
// hp is defined. A malformed value fails CLOSED at the mode checks below, never a silent downgrade.
|
|
725
|
-
// The hp keyword values clear / cipher are an enumerated Content-Type parameter. Per RFC 2045 sec. 5.1 a
|
|
726
|
-
// value of this kind is compared case-insensitively for its intended use, so a peer that emits hp="Clear"
|
|
727
|
-
// or hp=CIPHER is still recognized. (We always EMIT lowercase; we ACCEPT any case. The value is inside the
|
|
728
|
-
// signed/encrypted payload, so normalizing it enables no attack; the mode-contradiction check still runs.)
|
|
729
|
-
// _declaresHp detected the hp attribute (hasParam); a bare "hp" with no value is a malformed HP declaration
|
|
730
|
-
// and fails closed (mime.parse skips a valueless parameter, so params.hp is undefined here only for that
|
|
731
|
-
// malformed case).
|
|
732
406
|
var raw = inner.contentType.params.hp;
|
|
733
407
|
if (raw === undefined) throw _err("smime/bad-header-protection", "a header-protected payload declares a bare hp parameter with no value (expected hp=\"clear\" or hp=\"cipher\")");
|
|
734
408
|
var hp = raw.toLowerCase();
|
|
735
409
|
if (hp !== "clear" && hp !== "cipher") throw _err("smime/bad-header-protection", "the header-protected payload declares an invalid hp value " + JSON.stringify(raw) + " (only clear / cipher)");
|
|
736
410
|
if (hp !== expectedMode) throw _err("smime/bad-header-protection", "the payload hp=" + JSON.stringify(hp) + " contradicts the cryptographic envelope (a " + (expectedMode === "cipher" ? "decrypted" : "signed") + " message requires hp=" + JSON.stringify(expectedMode) + ")");
|
|
737
411
|
var ex = _extractProtected(inner.headers);
|
|
738
|
-
// A duplicate protected field is ambiguous (the last-wins overwrite hides an earlier value a different
|
|
739
|
-
// parser might select), so the payload fails closed.
|
|
740
412
|
if (ex.dup) throw _err("smime/bad-header-protection", "a header-protected payload has a duplicate protected header field " + JSON.stringify(ex.dup));
|
|
741
|
-
// RFC 9788 sec. 4.3.1: for an ENCRYPTED payload (hp="cipher") the confidential set is the fields not copied
|
|
742
|
-
// verbatim to the outer section via an HP-Outer record. Signed-only (clear) payloads carry no HP-Outer.
|
|
743
413
|
var confidential = hp === "cipher" ? _computeConfidential(ex.entries, ex.refouter) : [];
|
|
744
414
|
return { protectedHeaders: ex.protectedHeaders, headerProtection: { present: true, mode: hp, fromMismatch: _computeFromMismatch(ex.innerFrom, outerEnt), confidential: confidential, legacy: null } };
|
|
745
415
|
}
|
|
746
416
|
|
|
747
|
-
// Map smime opts to cms.sign opts (the S/MIME layer is algorithm-agnostic and forwards any signer).
|
|
748
417
|
function _cmsSignOpts(opts, detached) {
|
|
749
418
|
var o = { detached: detached };
|
|
750
419
|
if (opts.signingTime !== undefined) o.signingTime = opts.signingTime;
|
|
@@ -754,20 +423,12 @@ function _cmsSignOpts(opts, detached) {
|
|
|
754
423
|
return o;
|
|
755
424
|
}
|
|
756
425
|
|
|
757
|
-
// Coverage residual: the defensive `|| default` fallbacks in the helpers below (a null media type, a
|
|
758
|
-
// non-standard SignerInfo digest name -> a derived micalg, an absent micalg -> "sha-256"/null, a
|
|
759
|
-
// non-Buffer cms.sign result) and the two catch arms (a CMS body that passed cms.verify yet fails a
|
|
760
|
-
// second schema-cms.parse) are belt-and-braces around the primary path the round-trips exercise; the
|
|
761
|
-
// producer never emits any of them.
|
|
762
426
|
function _micName(name) { return name ? (MICALG[name] || name) : null; }
|
|
763
|
-
// Normalize a micalg header value into the same sorted-distinct comma-joined form _micalgOf emits.
|
|
764
427
|
function _micalgSet(value) {
|
|
765
428
|
var seen = [];
|
|
766
429
|
String(value).split(",").forEach(function (m) { var t = m.trim().toLowerCase(); if (t && seen.indexOf(t) < 0) seen.push(t); });
|
|
767
430
|
return seen.sort().join(",");
|
|
768
431
|
}
|
|
769
|
-
// The micalg for the SignedData -- RFC 8551 sec. 3.4.3.2: EVERY signer's message-digest algorithm,
|
|
770
|
-
// distinct, sorted, comma-separated (a multi-signer message with mixed digests lists them all).
|
|
771
432
|
function _micalgOf(p7Der) {
|
|
772
433
|
var parsed;
|
|
773
434
|
try { parsed = schemaCms.parse(guard.bytes.view(p7Der, SmimeError, "smime/bad-mime", "the CMS body")); }
|
|
@@ -780,11 +441,8 @@ function _micalgOf(p7Der) {
|
|
|
780
441
|
return out.length ? out.sort().join(",") : null;
|
|
781
442
|
}
|
|
782
443
|
|
|
783
|
-
// A fresh multipart boundary that cannot collide with the content (random, dashed, unique per call).
|
|
784
444
|
function _boundary() { return "----=_pki_smime_" + nodeCrypto.randomBytes(18).toString("hex"); }
|
|
785
445
|
|
|
786
|
-
// Base64 wrapped to 64-column lines joined by CRLF, no trailing CRLF (matching (.{64})->"$1\r\n"
|
|
787
|
-
// then a trailing-CRLF strip; base64 output carries no CRLF of its own).
|
|
788
446
|
function _wrap64Crlf(s) {
|
|
789
447
|
var out = "";
|
|
790
448
|
for (var i = 0; i < s.length; i += 64) {
|
|
@@ -841,11 +499,8 @@ function _base64Body(der) {
|
|
|
841
499
|
async function sign(content, signers, opts) {
|
|
842
500
|
opts = opts || {};
|
|
843
501
|
_knownOpts(opts, SIGN_OPTS, "sign");
|
|
844
|
-
// RFC 9788 header protection (opts.protectHeaders): the inner Cryptographic Payload gains hp="clear" +
|
|
845
|
-
// the inlined protected fields, and the outer frame carries the display copies. Off by default => the
|
|
846
|
-
// shipped path, byte-for-byte.
|
|
847
502
|
var hp = opts.protectHeaders === true;
|
|
848
|
-
var hpList = hp ? _hpHeaderList(opts.headers) : null;
|
|
503
|
+
var hpList = hp ? _hpHeaderList(opts.headers) : null;
|
|
849
504
|
var outerList = hp ? _outerHeaderList(hpList, "clear", opts.hcp) : null;
|
|
850
505
|
var entity = hp ? _protectedInnerEntity(content, opts, "clear", hpList, outerList) : _entityBytes(content, opts);
|
|
851
506
|
var outer = hp ? _outerPrefix(outerList) : "";
|
|
@@ -868,10 +523,6 @@ async function sign(content, signers, opts) {
|
|
|
868
523
|
return _capped(Buffer.concat([head2, dl, entity, mime.CRLF, dl, sigPart, mime.CRLF, close]));
|
|
869
524
|
}
|
|
870
525
|
|
|
871
|
-
// A produced message MUST stay within the same size ceiling verify enforces on receive, so sign never
|
|
872
|
-
// emits an S/MIME message our own verify would reject as too large (producer/consumer symmetry).
|
|
873
|
-
// Coverage residual: the throw only fires on a >16 MiB assembled message -- a real cap, exercised by no
|
|
874
|
-
// unit vector (driving it would sign 16 MiB).
|
|
875
526
|
function _capped(msg) {
|
|
876
527
|
guard.limits.byteCap(msg, C.LIMITS.MIME_MAX_BYTES, _err, "smime/too-large", "the assembled S/MIME message");
|
|
877
528
|
return msg;
|
|
@@ -962,25 +613,11 @@ async function verify(message, opts) {
|
|
|
962
613
|
_knownOpts(opts, VERIFY_OPTS, "verify");
|
|
963
614
|
var ent = mime.parse(message, SmimeError, "smime/bad-mime");
|
|
964
615
|
var ct = ent.contentType;
|
|
965
|
-
// Forwarded, not re-decided here. This verb documents itself as pki.cms.verify's verdict plus the
|
|
966
|
-
// MIME surface, so the trust seam that verb offers has to reach it: building the options from
|
|
967
|
-
// scratch and passing only `certs` would leave a caller naming trust anchors with no way to have
|
|
968
|
-
// them applied, and a `trusted` that read false for want of ever being asked.
|
|
969
616
|
var vOpts = {};
|
|
970
617
|
if (opts.certs) vOpts.certs = opts.certs;
|
|
971
618
|
if (opts.trustAnchors != null) {
|
|
972
619
|
vOpts.trustAnchors = opts.trustAnchors;
|
|
973
|
-
// Trusted FOR THIS PURPOSE. A chain alone does not make a signer right for email: a
|
|
974
|
-
// certificate restricted to serverAuth chains to its root perfectly well and is still the
|
|
975
|
-
// wrong key to have signed a message. RFC 8551 sec. 4.4.4 names emailProtection as the purpose
|
|
976
|
-
// an S/MIME signer's certificate must carry, so this verb asks for it rather than accepting the
|
|
977
|
-
// purpose-neutral answer. A caller who means something else says so with `requiredEku`.
|
|
978
620
|
vOpts.requiredEku = opts.requiredEku != null ? opts.requiredEku : ["emailProtection"];
|
|
979
|
-
// Both ends of the chain. The EKU above constrains the LEAF; this selects the anchor's own
|
|
980
|
-
// trust metadata, which pki.path consults only when a purpose is named. A root distributed
|
|
981
|
-
// with NSS trust bits can be marked untrusted for email while remaining a good TLS root, so
|
|
982
|
-
// asking only the leaf would let a root explicitly distrusted for email still answer
|
|
983
|
-
// "trusted" for an email message.
|
|
984
621
|
vOpts.checkPurpose = opts.checkPurpose != null ? opts.checkPurpose : "emailProtection";
|
|
985
622
|
}
|
|
986
623
|
if (opts.time !== undefined) vOpts.time = opts.time;
|
|
@@ -1000,10 +637,6 @@ async function verify(message, opts) {
|
|
|
1000
637
|
var sigEnt = mime.parse(parts[1], SmimeError, "smime/bad-mime");
|
|
1001
638
|
if (!_isPkcs7(sigEnt.contentType.type, "signature")) throw _err("smime/bad-multipart", "the second part must be application/pkcs7-signature");
|
|
1002
639
|
var p7s = _decodeCms(sigEnt);
|
|
1003
|
-
// RFC 8551 sec. 3.4 / RFC 1847: the multipart/signed signature is DETACHED -- the SignedData carries
|
|
1004
|
-
// NO encapsulated content, the first MIME part IS the signed unit. Reject an ATTACHED SignedData:
|
|
1005
|
-
// otherwise cms.verify would verify over the embedded eContent and IGNORE the first part, so an
|
|
1006
|
-
// attacker could pair any validly-signed attached blob with an arbitrary first part (a forgery).
|
|
1007
640
|
var sd;
|
|
1008
641
|
try { sd = schemaCms.parse(p7s); }
|
|
1009
642
|
catch (e) { throw _err("smime/bad-mime", "the pkcs7-signature part is not a CMS SignedData", e); }
|
|
@@ -1013,8 +646,6 @@ async function verify(message, opts) {
|
|
|
1013
646
|
var canon = mime.canonicalize(parts[0], SmimeError, "smime/bad-mime");
|
|
1014
647
|
var res2 = await cms.verify(p7s, Object.assign({}, vOpts, { content: canon }));
|
|
1015
648
|
var micalg = ct.params.micalg || null;
|
|
1016
|
-
// Compare micalg as an ORDER-INDEPENDENT set of digest names (RFC 8551 sec. 3.4.3.2 lists them
|
|
1017
|
-
// comma-separated, in no required order, possibly with whitespace).
|
|
1018
649
|
if (opts.strictMicalg && micalg && _micalgSet(micalg) !== (_micalgOf(p7s) || "")) {
|
|
1019
650
|
throw _err("smime/micalg-mismatch", "the multipart/signed micalg " + JSON.stringify(micalg) + " disagrees with the SignerInfo digests");
|
|
1020
651
|
}
|
|
@@ -1023,17 +654,10 @@ async function verify(message, opts) {
|
|
|
1023
654
|
throw _err("smime/unsupported-type", "not a signed S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
|
|
1024
655
|
}
|
|
1025
656
|
|
|
1026
|
-
// The application/pkcs7-mime header for an opaque S/MIME message (RFC 8551 sec. 3.2 / sec. 3.2.2). `name`
|
|
1027
|
-
// is the smime.p7m (enveloped) / smime.p7z (compressed) file name for the media type + Content-Disposition.
|
|
1028
657
|
function _pkcs7MimeHead(smimeType, name) {
|
|
1029
658
|
return Buffer.from("Content-Type: application/pkcs7-mime; smime-type=" + smimeType + "; name=" + name + "\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=" + name + "\r\n\r\n", "latin1");
|
|
1030
659
|
}
|
|
1031
660
|
|
|
1032
|
-
// The smime-type for a CMS enveloped body, DERIVED from its OUTER content type (RFC 8551 sec. 3.2.2) --
|
|
1033
|
-
// the single source of truth, never an opts guess: AuthEnvelopedData -> "authEnveloped-data" (AEAD,
|
|
1034
|
-
// integrity), EnvelopedData -> "enveloped-data" (no integrity). Anything else is not an S/MIME enveloped
|
|
1035
|
-
// message. Returns { smimeType, name } (name is the parsed contentTypeName). Throws smime/unsupported-type
|
|
1036
|
-
// for a non-enveloped body (EncryptedData / SignedData carries no smime-type mapping here).
|
|
1037
661
|
function _envelopedTypeOf(der, badParseCode) {
|
|
1038
662
|
var name;
|
|
1039
663
|
try { name = schemaCms.parse(guard.bytes.view(der, SmimeError, "smime/bad-mime", "the CMS body")).contentTypeName; }
|
|
@@ -1043,10 +667,6 @@ function _envelopedTypeOf(der, badParseCode) {
|
|
|
1043
667
|
throw _err(badParseCode, "the pkcs7-mime body is a " + name + ", not an EnvelopedData / AuthEnvelopedData");
|
|
1044
668
|
}
|
|
1045
669
|
|
|
1046
|
-
// Map smime opts to cms.encrypt opts. The S/MIME layer is recipient-agnostic: it forwards the content
|
|
1047
|
-
// -encryption choice + every recipient-shaping opt unchanged, and never emits PEM (the CMS body is base64
|
|
1048
|
-
// inside a MIME frame). opts.contentType is the MIME media type of the INNER entity (handled by
|
|
1049
|
-
// _entityBytes), NOT the CMS encapsulated content type -- it is deliberately not forwarded here.
|
|
1050
670
|
function _cmsEncryptOpts(opts) {
|
|
1051
671
|
var o = {};
|
|
1052
672
|
if (opts.contentEncryptionAlgorithm !== undefined) o.contentEncryptionAlgorithm = opts.contentEncryptionAlgorithm;
|
|
@@ -1099,18 +719,11 @@ function _cmsEncryptOpts(opts) {
|
|
|
1099
719
|
async function encrypt(content, recipients, opts) {
|
|
1100
720
|
opts = opts || {};
|
|
1101
721
|
_knownOpts(opts, ENCRYPT_OPTS, "encrypt");
|
|
1102
|
-
// RFC 9788 header protection (opts.protectHeaders): the inner payload carries hp="cipher" + the REAL
|
|
1103
|
-
// headers (inside the ciphertext); the outer frame carries only the Header-Confidentiality-Policy-processed
|
|
1104
|
-
// display copies (hcp_baseline obscures Subject to [...], removes Comments/Keywords). Off => shipped path.
|
|
1105
722
|
var hp = opts.protectHeaders === true;
|
|
1106
|
-
var hpList = hp ? _hpHeaderList(opts.headers) : null;
|
|
723
|
+
var hpList = hp ? _hpHeaderList(opts.headers) : null;
|
|
1107
724
|
var outerList = hp ? _outerHeaderList(hpList, "cipher", opts.hcp) : null;
|
|
1108
725
|
var entity = hp ? _protectedInnerEntity(content, opts, "cipher", hpList, outerList) : _entityBytes(content, opts);
|
|
1109
|
-
// Build the outer prefix (opts.headers + opts.hcp were validated when outerList was computed) BEFORE the
|
|
1110
|
-
// expensive cms.encrypt, so a bad policy / header fails closed without doing the crypto.
|
|
1111
726
|
var outer = hp ? Buffer.from(_outerPrefix(outerList), "utf8") : Buffer.alloc(0);
|
|
1112
|
-
// Normalize a single descriptor to an array so cms.encrypt always takes the ENVELOPED path (an array of
|
|
1113
|
-
// RecipientInfos), never bare EncryptedData -- which is not an S/MIME construct (no smime-type maps to it).
|
|
1114
727
|
var recips = Array.isArray(recipients) ? recipients : [recipients];
|
|
1115
728
|
var der = _toBuf(await cms.encrypt(entity, recips, _cmsEncryptOpts(opts)));
|
|
1116
729
|
var head = _pkcs7MimeHead(_envelopedTypeOf(der, "smime/bad-mime"), "smime.p7m");
|
|
@@ -1165,8 +778,6 @@ async function decrypt(message, keyMaterial, opts) {
|
|
|
1165
778
|
var ent = mime.parse(message, SmimeError, "smime/bad-mime");
|
|
1166
779
|
var ct = ent.contentType;
|
|
1167
780
|
if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not an encrypted S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
|
|
1168
|
-
// The smime-type header is advisory (the CMS body is authoritative), so compare it case-insensitively --
|
|
1169
|
-
// liberal on accept, strict on emit -- rather than rejecting a legitimate message over its parameter casing.
|
|
1170
781
|
var st = ct.params["smime-type"];
|
|
1171
782
|
var stLower = st ? st.toLowerCase() : st;
|
|
1172
783
|
if (st && stLower !== "enveloped-data" && stLower !== "authenveloped-data") throw _err("smime/unsupported-type", "unsupported smime-type " + JSON.stringify(st) + " (only enveloped-data / authEnveloped-data)");
|
|
@@ -1246,7 +857,6 @@ async function decompress(message, opts) {
|
|
|
1246
857
|
var ent = mime.parse(message, SmimeError, "smime/bad-mime");
|
|
1247
858
|
var ct = ent.contentType;
|
|
1248
859
|
if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not a compressed S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
|
|
1249
|
-
// The smime-type header is advisory (the CMS body is authoritative) -- compare case-insensitively.
|
|
1250
860
|
var st = ct.params["smime-type"];
|
|
1251
861
|
if (st && st.toLowerCase() !== "compressed-data") throw _err("smime/unsupported-type", "unsupported smime-type " + JSON.stringify(st) + " (only compressed-data)");
|
|
1252
862
|
var der = _decodeCms(ent);
|
|
@@ -1256,10 +866,7 @@ async function decompress(message, opts) {
|
|
|
1256
866
|
return { content: res.content, contentType: res.contentType, contentTypeName: res.contentTypeName, compressionAlgorithm: res.compressionAlgorithm };
|
|
1257
867
|
}
|
|
1258
868
|
|
|
1259
|
-
// Decode a CMS body per its Content-Transfer-Encoding (base64 is the S/MIME norm; 7bit/8bit/binary pass through).
|
|
1260
869
|
function _decodeCms(ent) {
|
|
1261
|
-
// guard.encoding.base64 is strict (no whitespace) and rejects via a (code,msg) FACTORY -- strip the
|
|
1262
|
-
// MIME line-wrapping whitespace first, and pass _err (not the SmimeError class).
|
|
1263
870
|
if (ent.cte === "base64") return guard.encoding.base64(pkix.stripBase64Whitespace(ent.body.toString("latin1")), C.LIMITS.MIME_MAX_BYTES, _err, "smime/bad-mime", "the CMS body");
|
|
1264
871
|
return ent.body;
|
|
1265
872
|
}
|