@blamejs/core 0.9.49 → 0.10.1
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 +951 -908
- package/index.js +25 -0
- package/lib/_test/crypto-fixtures.js +67 -0
- package/lib/agent-event-bus.js +52 -6
- package/lib/agent-idempotency.js +169 -16
- package/lib/agent-orchestrator.js +263 -9
- package/lib/agent-posture-chain.js +163 -5
- package/lib/agent-saga.js +146 -16
- package/lib/agent-snapshot.js +349 -19
- package/lib/agent-stream.js +34 -2
- package/lib/agent-tenant.js +179 -23
- package/lib/agent-trace.js +84 -21
- package/lib/auth/aal.js +8 -1
- package/lib/auth/ciba.js +6 -1
- package/lib/auth/dpop.js +7 -2
- package/lib/auth/fal.js +17 -8
- package/lib/auth/jwt-external.js +128 -4
- package/lib/auth/oauth.js +232 -10
- package/lib/auth/oid4vci.js +67 -7
- package/lib/auth/openid-federation.js +71 -25
- package/lib/auth/passkey.js +140 -6
- package/lib/auth/sd-jwt-vc.js +67 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/compliance.js +176 -8
- package/lib/crypto-field.js +114 -14
- package/lib/crypto.js +216 -20
- package/lib/db.js +1 -0
- package/lib/guard-jmap.js +321 -0
- package/lib/guard-managesieve-command.js +566 -0
- package/lib/guard-pop3-command.js +317 -0
- package/lib/guard-smtp-command.js +58 -3
- package/lib/mail-agent.js +20 -7
- package/lib/mail-arc-sign.js +12 -8
- package/lib/mail-auth.js +323 -34
- package/lib/mail-crypto-pgp.js +934 -0
- package/lib/mail-crypto-smime.js +340 -0
- package/lib/mail-crypto.js +108 -0
- package/lib/mail-dav.js +1224 -0
- package/lib/mail-deploy.js +492 -0
- package/lib/mail-dkim.js +431 -26
- package/lib/mail-journal.js +435 -0
- package/lib/mail-scan.js +502 -0
- package/lib/mail-server-imap.js +64 -26
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +40 -30
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +13 -0
- package/lib/mail-server-submission.js +70 -24
- package/lib/mail-server-tls.js +445 -0
- package/lib/mail-sieve.js +557 -0
- package/lib/mail-spam-score.js +284 -0
- package/lib/mail.js +99 -0
- package/lib/metrics.js +80 -3
- package/lib/middleware/dpop.js +58 -3
- package/lib/middleware/idempotency-key.js +255 -42
- package/lib/middleware/protected-resource-metadata.js +114 -2
- package/lib/network-dns-resolver.js +33 -0
- package/lib/network-tls.js +46 -0
- package/lib/outbox.js +62 -12
- package/lib/pqc-agent.js +13 -5
- package/lib/retry.js +23 -9
- package/lib/router.js +23 -1
- package/lib/safe-ical.js +634 -0
- package/lib/safe-icap.js +502 -0
- package/lib/safe-mime.js +15 -0
- package/lib/safe-sieve.js +684 -0
- package/lib/safe-smtp.js +57 -0
- package/lib/safe-url.js +37 -0
- package/lib/safe-vcard.js +473 -0
- package/lib/self-update-standalone-verifier.js +32 -3
- package/lib/self-update.js +153 -33
- package/lib/vendor/MANIFEST.json +161 -156
- package/lib/vendor-data.js +127 -9
- package/lib/vex.js +324 -59
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.mail.crypto.smime
|
|
4
|
+
* @nav Communication
|
|
5
|
+
* @title Mail S/MIME
|
|
6
|
+
* @order 121
|
|
7
|
+
* @slug mail-crypto-smime
|
|
8
|
+
*
|
|
9
|
+
* @card
|
|
10
|
+
* S/MIME 4.0 signature verification per RFC 8551 + RFC 5652 CMS
|
|
11
|
+
* SignedData. v1 surface is cert preflight; sign/verify deferred.
|
|
12
|
+
*
|
|
13
|
+
* @intro
|
|
14
|
+
* S/MIME 4.0 (RFC 8551, replacing RFC 5751) `multipart/signed;
|
|
15
|
+
* protocol="application/pkcs7-signature"` signature verification
|
|
16
|
+
* for inbound mail. CMS SignedData (RFC 5652) carries the actual
|
|
17
|
+
* signature; the signed payload travels in the first MIME part of
|
|
18
|
+
* the multipart/signed wrapper with the SignedData attached to the
|
|
19
|
+
* second part as base64-encoded DER.
|
|
20
|
+
*
|
|
21
|
+
* Posture (when the surface lights up):
|
|
22
|
+
* - Refuses SHA-1 as the signature hash (CVE-2017-9006-class —
|
|
23
|
+
* PKCS#7 collision attacks against legacy S/MIME) and as the
|
|
24
|
+
* certificate signature algorithm.
|
|
25
|
+
* - Refuses RSA keys < 2048 bits (RFC 8301 §3.1 — same posture
|
|
26
|
+
* as the rest of the mail surface).
|
|
27
|
+
* - Refuses MD5 anywhere (the historical S/MIME-v2 default; long
|
|
28
|
+
* broken).
|
|
29
|
+
* - Validates the signer certificate's chain against an operator-
|
|
30
|
+
* supplied trust anchor set; never falls back to a system root
|
|
31
|
+
* store implicitly (the system store binds operator trust to
|
|
32
|
+
* whatever the host happens to ship with).
|
|
33
|
+
* - Refuses certificate algorithms outside the modern set
|
|
34
|
+
* (RSA-PKCS1-v1_5 with SHA-256 / SHA-384 / SHA-512, ECDSA over
|
|
35
|
+
* P-256 / P-384 with SHA-256 / SHA-384, Ed25519). RFC 8551 §2.5
|
|
36
|
+
* mandates SHA-256 as the MUST-support floor.
|
|
37
|
+
*
|
|
38
|
+
* Threat model:
|
|
39
|
+
* - EFAIL (CVE-2017-17688 / CVE-2017-17689) — the S/MIME variant
|
|
40
|
+
* attacks decrypt+render pipelines. Same gate as PGP: when
|
|
41
|
+
* encrypt/decrypt lights up, decrypted HTML routes through
|
|
42
|
+
* `b.guardHtml` strict profile, remote-content fetches in
|
|
43
|
+
* encrypted parts are refused, and the MIME-part tree at
|
|
44
|
+
* decrypt time is compared byte-for-byte against the tree at
|
|
45
|
+
* render time.
|
|
46
|
+
* - PKCS#7 / CMS parser confusion — only the SignedData
|
|
47
|
+
* (ContentType 1.2.840.113549.1.7.2) ContentInfo shape is
|
|
48
|
+
* accepted; degenerate, certs-only-bag, AuthEnvelopedData, and
|
|
49
|
+
* encrypted-content variants are refused at parse time.
|
|
50
|
+
*
|
|
51
|
+
* v1 status — DEFERRED with documented conditions:
|
|
52
|
+
*
|
|
53
|
+
* Both sign() and verify() throw `MailCryptoError("mail-crypto/
|
|
54
|
+
* smime/deferred", ...)` in v1. The CMS SignedData ASN.1
|
|
55
|
+
* structure (RFC 5652 §5.1) is a five-field SEQUENCE with nested
|
|
56
|
+
* SET-OF / OPTIONAL / IMPLICIT-tagged fields, a DER content
|
|
57
|
+
* octet-string with constructed indefinite-length variants seen
|
|
58
|
+
* in the wild, and signed-attributes / unsigned-attributes
|
|
59
|
+
* ordering rules (§5.4 — DER set-of attributes MUST be sorted by
|
|
60
|
+
* encoded value for the signature to verify). node:crypto does
|
|
61
|
+
* not expose a CMS codec, and a hand-rolled ASN.1 BER/DER parser
|
|
62
|
+
* of the depth required to round-trip every fielded S/MIME
|
|
63
|
+
* signer's output is comparable in surface to the OpenPGP
|
|
64
|
+
* packet decoder shipped in `b.mail.crypto.pgp` — but with
|
|
65
|
+
* dramatically more shape variation across implementations.
|
|
66
|
+
*
|
|
67
|
+
* Defer condition: no operator demand has surfaced for in-process
|
|
68
|
+
* S/MIME verification; operators receiving S/MIME-signed mail
|
|
69
|
+
* today either (a) trust the gateway's authentication-results
|
|
70
|
+
* header (composed via `b.mail.authResults`) and treat S/MIME as
|
|
71
|
+
* a downstream concern, or (b) run S/MIME verification in their
|
|
72
|
+
* own consumer code with a vetted CMS library. Reopen this
|
|
73
|
+
* surface when ALL of the following hold:
|
|
74
|
+
* 1. At least one operator surfaces concrete demand for
|
|
75
|
+
* in-process S/MIME verify (use case + sample message
|
|
76
|
+
* shape).
|
|
77
|
+
* 2. A vendorable ASN.1 BER/DER decoder lands in `lib/vendor/`
|
|
78
|
+
* under the framework's vendoring discipline (MANIFEST.json
|
|
79
|
+
* + sha256 pin + no transitive deps), OR an operator
|
|
80
|
+
* provides a tested decoder we can fold in directly.
|
|
81
|
+
* 3. RFC 8551 §2.5 + RFC 5652 §11 conformance test vectors are
|
|
82
|
+
* available to drive the implementation. (NIST PKITS-style
|
|
83
|
+
* test vectors exist for X.509 chain validation; equivalent
|
|
84
|
+
* coverage for CMS SignedData is sparser.)
|
|
85
|
+
*
|
|
86
|
+
* Cheap escape hatch: operators wire `node-forge` / `pkijs` /
|
|
87
|
+
* openssl(1) (via child_process) in their own consumer code,
|
|
88
|
+
* extract the signed payload + signature components, and compose
|
|
89
|
+
* with `b.mail.authResults` to record the verification outcome
|
|
90
|
+
* for downstream policy. The S/MIME wire-format constants
|
|
91
|
+
* (Content-Type protocol parameter, micalg mapping, base64 DER
|
|
92
|
+
* framing) are stable and operator-side code interoperates with
|
|
93
|
+
* any inbound S/MIME-signed message regardless of this module's
|
|
94
|
+
* state.
|
|
95
|
+
*
|
|
96
|
+
* v2 reopen tag: the next minor (v0.9.60+) once the conditions
|
|
97
|
+
* above are met. The deferred surface lights up sign+verify
|
|
98
|
+
* together so operators never see a half-implementation.
|
|
99
|
+
*
|
|
100
|
+
* RFC citations:
|
|
101
|
+
* - RFC 8551 (S/MIME 4.0 Message Specification, April 2019;
|
|
102
|
+
* obsoletes RFC 5751)
|
|
103
|
+
* - RFC 5652 (Cryptographic Message Syntax — CMS)
|
|
104
|
+
* - RFC 8550 (S/MIME 4.0 Certificate Handling)
|
|
105
|
+
* - RFC 5280 (X.509 PKI)
|
|
106
|
+
* - RFC 8301 (RSA bit floor — reused as cross-mail-surface RSA posture)
|
|
107
|
+
*
|
|
108
|
+
* CVE citations:
|
|
109
|
+
* - CVE-2017-17688 / CVE-2017-17689 (EFAIL — S/MIME variant; informs
|
|
110
|
+
* the encrypt+decrypt deferral when that surface lights up)
|
|
111
|
+
* - CVE-2017-9006 (PKCS#7 / S/MIME signature-validation bypass
|
|
112
|
+
* class — informs the SHA-1 refusal posture)
|
|
113
|
+
* - CVE-2018-5407 (PortSmash — informs the side-channel hardening
|
|
114
|
+
* posture when private operations land in v2)
|
|
115
|
+
*/
|
|
116
|
+
var lazyRequire = require("./lazy-require");
|
|
117
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
118
|
+
var nodeCrypto = require("node:crypto");
|
|
119
|
+
var validateOpts = require("./validate-opts");
|
|
120
|
+
var { defineClass } = require("./framework-error");
|
|
121
|
+
|
|
122
|
+
var MailCryptoError = defineClass("MailCryptoError", { alwaysPermanent: true });
|
|
123
|
+
|
|
124
|
+
// Constant posture values exported so operators reading this module
|
|
125
|
+
// from configuration code can pin to them by reference rather than
|
|
126
|
+
// hand-copying strings. These reflect RFC 8551 §2.5 + RFC 8301 floors.
|
|
127
|
+
var RSA_MIN_BITS = 2048; // allow:raw-byte-literal — RFC 8301 §3.1
|
|
128
|
+
var ALLOWED_HASHES = ["sha256", "sha384", "sha512"];
|
|
129
|
+
var REFUSED_HASHES = ["md5", "sha1"]; // allow:raw-byte-literal — CVE-2017-9006-class
|
|
130
|
+
|
|
131
|
+
// PROFILES + COMPLIANCE_POSTURES — the framework's standard cross-
|
|
132
|
+
// primitive contract. v1 only emits the metadata; the deferred sign/
|
|
133
|
+
// verify methods read them when they light up.
|
|
134
|
+
var PROFILES = ["strict", "balanced", "permissive"];
|
|
135
|
+
var COMPLIANCE_POSTURES = {
|
|
136
|
+
hipaa: "strict",
|
|
137
|
+
"pci-dss": "strict",
|
|
138
|
+
gdpr: "strict",
|
|
139
|
+
soc2: "strict",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
var DEFERRAL_MESSAGE =
|
|
143
|
+
"b.mail.crypto.smime is deferred in v1. See the @intro comment block " +
|
|
144
|
+
"in lib/mail-crypto-smime.js for the deferral conditions and the " +
|
|
145
|
+
"documented escape hatch (operator-side CMS via a vetted third-party " +
|
|
146
|
+
"library or openssl(1)). Lights up in v0.9.60+ once a vendorable " +
|
|
147
|
+
"ASN.1 BER/DER decoder is folded in under lib/vendor/.";
|
|
148
|
+
|
|
149
|
+
// ---- Public surface (deferred) ----
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @primitive b.mail.crypto.smime.sign
|
|
153
|
+
* @signature b.mail.crypto.smime.sign(opts)
|
|
154
|
+
* @since 0.9.58
|
|
155
|
+
* @status experimental
|
|
156
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
157
|
+
*
|
|
158
|
+
* Deferred entry point. v1 surface is recognition + posture-only;
|
|
159
|
+
* actual CMS emission lights up in v0.9.60+ once a vendorable
|
|
160
|
+
* ASN.1 BER/DER codec is folded in. Throws
|
|
161
|
+
* `mail-crypto/smime/deferred` with a documented escape-hatch path
|
|
162
|
+
* (operator-side CMS via openssl(1) or a vetted library).
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* try {
|
|
166
|
+
* b.mail.crypto.smime.sign({ message: m, certPem: c, privateKeyPem: k });
|
|
167
|
+
* } catch (e) {
|
|
168
|
+
* // e.code === "mail-crypto/smime/deferred"
|
|
169
|
+
* }
|
|
170
|
+
*/
|
|
171
|
+
function sign(opts) {
|
|
172
|
+
opts = opts || {};
|
|
173
|
+
validateOpts(opts, ["message", "certPem", "privateKeyPem", "passphrase", "audit"],
|
|
174
|
+
"mail.crypto.smime.sign");
|
|
175
|
+
_audit(opts.audit, "mail.crypto.smime.sign", "denied", { reason: "deferred" });
|
|
176
|
+
throw new MailCryptoError("mail-crypto/smime/deferred", DEFERRAL_MESSAGE);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @primitive b.mail.crypto.smime.verify
|
|
181
|
+
* @signature b.mail.crypto.smime.verify(opts)
|
|
182
|
+
* @since 0.9.58
|
|
183
|
+
* @status experimental
|
|
184
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
185
|
+
*
|
|
186
|
+
* Deferred entry point — same posture as sign. v1 throws
|
|
187
|
+
* `mail-crypto/smime/deferred`; v0.9.60+ verifies a CMS SignedData
|
|
188
|
+
* blob against `opts.trustedCertsPem`.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* try {
|
|
192
|
+
* b.mail.crypto.smime.verify({ message: m, armored: a, trustedCertsPem: t });
|
|
193
|
+
* } catch (e) {
|
|
194
|
+
* // e.code === "mail-crypto/smime/deferred"
|
|
195
|
+
* }
|
|
196
|
+
*/
|
|
197
|
+
function verify(opts) {
|
|
198
|
+
opts = opts || {};
|
|
199
|
+
validateOpts(opts, ["message", "armored", "trustedCertsPem", "audit"],
|
|
200
|
+
"mail.crypto.smime.verify");
|
|
201
|
+
_audit(opts.audit, "mail.crypto.smime.verify_fail", "denied", { reason: "deferred" });
|
|
202
|
+
throw new MailCryptoError("mail-crypto/smime/deferred", DEFERRAL_MESSAGE);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---- Cert-shape preflight (operator-supplied trust roots) ----
|
|
206
|
+
//
|
|
207
|
+
// This *is* implemented in v1 — even before sign/verify light up,
|
|
208
|
+
// operators wiring an `b.mail.crypto.smime.checkCert({ certPem })`
|
|
209
|
+
// call against a candidate signing cert at boot get the SHA-1 / weak-
|
|
210
|
+
// RSA refusal posture surfaced as a config-time error rather than
|
|
211
|
+
// discovering it post-deploy. Reuses node:crypto's X509Certificate
|
|
212
|
+
// (cf. lib/mtls-ca.js).
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @primitive b.mail.crypto.smime.checkCert
|
|
216
|
+
* @signature b.mail.crypto.smime.checkCert(opts)
|
|
217
|
+
* @since 0.9.58
|
|
218
|
+
* @status stable
|
|
219
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
220
|
+
*
|
|
221
|
+
* Operator-side cert preflight that lights up at boot: refuses
|
|
222
|
+
* SHA-1 / MD5 signatures, RSA keys < 2048 bits, MD2 / MD5 / SHA-1
|
|
223
|
+
* as the certificate-signature algorithm. Returns the parsed cert
|
|
224
|
+
* shape (subject CN, issuer CN, validFrom / validTo, key algorithm
|
|
225
|
+
* + size, signature algorithm). Throws `mail-crypto/smime/bad-cert`
|
|
226
|
+
* on any of the above; throws `mail-crypto/smime/expired-cert` if
|
|
227
|
+
* the cert is outside its validity window.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* var info = b.mail.crypto.smime.checkCert({ certPem: pem });
|
|
231
|
+
* // → { subjectCN, issuerCN, validFrom, validTo, keyAlg, keyBits, sigAlg }
|
|
232
|
+
*/
|
|
233
|
+
function checkCert(opts) {
|
|
234
|
+
opts = validateOpts.requireObject(opts, "mail.crypto.smime.checkCert",
|
|
235
|
+
MailCryptoError, "mail-crypto/smime/bad-opts");
|
|
236
|
+
validateOpts(opts, ["certPem"], "mail.crypto.smime.checkCert");
|
|
237
|
+
validateOpts.requireNonEmptyString(opts.certPem, "certPem",
|
|
238
|
+
MailCryptoError, "mail-crypto/smime/bad-cert");
|
|
239
|
+
|
|
240
|
+
var cert;
|
|
241
|
+
try {
|
|
242
|
+
cert = new nodeCrypto.X509Certificate(opts.certPem);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
throw new MailCryptoError("mail-crypto/smime/bad-cert",
|
|
245
|
+
"certPem could not be parsed as X.509: " + ((e && e.message) || String(e)));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Cert signature algorithm refusal — node:crypto X509Certificate
|
|
249
|
+
// exposes `signatureAlgorithm` (OpenSSL long name like
|
|
250
|
+
// "sha256WithRSAEncryption", "ecdsa-with-SHA384", "ED25519") and
|
|
251
|
+
// `signatureAlgorithmOid` (the canonical OID). We screen on the
|
|
252
|
+
// lowercase long name so SHA-1 / MD5 substrings catch every
|
|
253
|
+
// fielded variant. The OID is reported in the returned shape so
|
|
254
|
+
// operators with stricter posture can pin on it.
|
|
255
|
+
var sigAlgName = cert.signatureAlgorithm || cert.sigAlgName || "";
|
|
256
|
+
var sigAlg = String(sigAlgName).toLowerCase();
|
|
257
|
+
for (var i = 0; i < REFUSED_HASHES.length; i += 1) {
|
|
258
|
+
if (sigAlg.indexOf(REFUSED_HASHES[i]) !== -1) {
|
|
259
|
+
throw new MailCryptoError("mail-crypto/smime/refused-hash",
|
|
260
|
+
"cert signature algorithm '" + sigAlgName +
|
|
261
|
+
"' refused — SHA-1 / MD5 in cert signatures is forbidden " +
|
|
262
|
+
"(CVE-2017-9006-class). Acceptable hashes: " + ALLOWED_HASHES.join(", "));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// RSA bit floor — when the public key is RSA, refuse < RSA_MIN_BITS.
|
|
267
|
+
// The X509Certificate exposes the public key via .publicKey
|
|
268
|
+
// (node 17+) which is a KeyObject we can inspect.
|
|
269
|
+
var pub = cert.publicKey;
|
|
270
|
+
if (pub && pub.asymmetricKeyType === "rsa") {
|
|
271
|
+
var jwk = pub.export({ format: "jwk" });
|
|
272
|
+
var nBytes = Buffer.from(jwk.n, "base64url");
|
|
273
|
+
var bits = nBytes.length * 8; // allow:raw-byte-literal — bits-per-byte conversion // allow:raw-time-literal — RFC 5280 in comment, not seconds
|
|
274
|
+
if (bits < RSA_MIN_BITS) {
|
|
275
|
+
throw new MailCryptoError("mail-crypto/smime/rsa-too-small",
|
|
276
|
+
"cert public key is " + bits + " RSA bits; minimum is " + RSA_MIN_BITS +
|
|
277
|
+
" (RFC 8301 §3.1)");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Validity window — refuse certs outside their notBefore / notAfter
|
|
282
|
+
// window. Codex P1: checkCert's docstring promises this throws
|
|
283
|
+
// `mail-crypto/smime/expired-cert` but the impl was missing, letting
|
|
284
|
+
// expired or not-yet-valid signing certs pass boot-time preflight
|
|
285
|
+
// and fail interop later when peers verify signatures against the
|
|
286
|
+
// RFC 5280 §4.1.2.5 validity field.
|
|
287
|
+
var nowMs = Date.now();
|
|
288
|
+
var notBeforeMs = Date.parse(cert.validFrom);
|
|
289
|
+
var notAfterMs = Date.parse(cert.validTo);
|
|
290
|
+
if (isFinite(notBeforeMs) && nowMs < notBeforeMs) {
|
|
291
|
+
throw new MailCryptoError("mail-crypto/smime/expired-cert",
|
|
292
|
+
"cert is not yet valid (notBefore=" + cert.validFrom + ", now=" +
|
|
293
|
+
new Date(nowMs).toISOString() + ")");
|
|
294
|
+
}
|
|
295
|
+
if (isFinite(notAfterMs) && nowMs > notAfterMs) {
|
|
296
|
+
throw new MailCryptoError("mail-crypto/smime/expired-cert",
|
|
297
|
+
"cert is expired (notAfter=" + cert.validTo + ", now=" +
|
|
298
|
+
new Date(nowMs).toISOString() + ")");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
subject: cert.subject,
|
|
303
|
+
issuer: cert.issuer,
|
|
304
|
+
validFrom: cert.validFrom,
|
|
305
|
+
validTo: cert.validTo,
|
|
306
|
+
sigAlgName: sigAlgName,
|
|
307
|
+
sigAlgOid: cert.signatureAlgorithmOid || null,
|
|
308
|
+
keyType: pub && pub.asymmetricKeyType,
|
|
309
|
+
fingerprint256: cert.fingerprint256,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---- Audit (drop-silent) ----
|
|
314
|
+
|
|
315
|
+
function _audit(auditHandle, action, outcome, metadata) {
|
|
316
|
+
try {
|
|
317
|
+
var a = auditHandle || audit();
|
|
318
|
+
if (a && typeof a.safeEmit === "function") {
|
|
319
|
+
a.safeEmit({
|
|
320
|
+
action: action,
|
|
321
|
+
outcome: outcome,
|
|
322
|
+
actor: {},
|
|
323
|
+
metadata: metadata,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
} catch (_e) { /* drop-silent — audit failures must not crash callers */ }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
module.exports = {
|
|
330
|
+
sign: sign,
|
|
331
|
+
verify: verify,
|
|
332
|
+
checkCert: checkCert,
|
|
333
|
+
MailCryptoError: MailCryptoError,
|
|
334
|
+
PROFILES: PROFILES,
|
|
335
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
336
|
+
ALLOWED_HASHES: ALLOWED_HASHES,
|
|
337
|
+
REFUSED_HASHES: REFUSED_HASHES,
|
|
338
|
+
RSA_MIN_BITS: RSA_MIN_BITS,
|
|
339
|
+
DEFERRAL_MESSAGE: DEFERRAL_MESSAGE,
|
|
340
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.mail.crypto
|
|
4
|
+
* @featured false
|
|
5
|
+
* @nav Communication
|
|
6
|
+
* @title Mail crypto (PGP + S/MIME)
|
|
7
|
+
* @order 119
|
|
8
|
+
* @slug mail-crypto
|
|
9
|
+
*
|
|
10
|
+
* @card
|
|
11
|
+
* End-to-end mail signing (OpenPGP per RFC 9580) + S/MIME 4.0
|
|
12
|
+
* posture (RFC 8551 / RFC 5652 CMS). Sub-namespaces: pgp, smime.
|
|
13
|
+
*
|
|
14
|
+
* @intro
|
|
15
|
+
* End-to-end mail signing + verification, organized into two sub-
|
|
16
|
+
* namespaces by wire format:
|
|
17
|
+
*
|
|
18
|
+
* - `b.mail.crypto.pgp` — OpenPGP per RFC 9580 (November 2024),
|
|
19
|
+
* wrapped in `multipart/signed; protocol="application/pgp-
|
|
20
|
+
* signature"` per RFC 3156. v1 surface: sign() + verify() with
|
|
21
|
+
* v4 detached signatures over Ed25519 (pub-alg 22) and RSA
|
|
22
|
+
* (pub-alg 1, EMSA-PKCS1-v1_5 + SHA-256, 2048-bit floor per
|
|
23
|
+
* RFC 8301).
|
|
24
|
+
* - `b.mail.crypto.smime` — S/MIME 4.0 per RFC 8551 with CMS
|
|
25
|
+
* SignedData per RFC 5652. v1 surface: checkCert() — the
|
|
26
|
+
* operator-side preflight that refuses SHA-1 / MD5 / < 2048-bit
|
|
27
|
+
* RSA certs at boot. sign() + verify() are DEFERRED in v1; see
|
|
28
|
+
* the @intro block in lib/mail-crypto-smime.js for the deferral
|
|
29
|
+
* conditions and the operator escape hatch.
|
|
30
|
+
*
|
|
31
|
+
* Both sub-namespaces share `MailCryptoError` (FrameworkError
|
|
32
|
+
* subclass via defineClass with alwaysPermanent: true) so operator
|
|
33
|
+
* error handling can `catch (e) { if (e instanceof
|
|
34
|
+
* b.mail.crypto.MailCryptoError) ... }` once and cover both
|
|
35
|
+
* protocols.
|
|
36
|
+
*
|
|
37
|
+
* Composition with the rest of the mail surface:
|
|
38
|
+
* - DKIM-Signature (b.mail.dkim) signs at the SMTP-message
|
|
39
|
+
* transport boundary; PGP / S/MIME sign at the user-visible
|
|
40
|
+
* payload boundary. The two are complementary — a message can
|
|
41
|
+
* carry BOTH a DKIM-Signature header (proving the sending
|
|
42
|
+
* domain) AND a PGP / S/MIME signature (proving the human
|
|
43
|
+
* sender's key). Operators wiring both wire DKIM via
|
|
44
|
+
* `opts.dkimSigner` on the smtp transport and call
|
|
45
|
+
* `b.mail.crypto.pgp.sign()` over the multipart body before
|
|
46
|
+
* handing it to the transport.
|
|
47
|
+
* - When the EFAIL-class encrypt/decrypt surface lights up (see
|
|
48
|
+
* per-sub-namespace deferral conditions), rendered HTML routes
|
|
49
|
+
* through `b.guardHtml` strict profile and the MIME-part tree
|
|
50
|
+
* is captured at decrypt time + diffed against the tree at
|
|
51
|
+
* render time.
|
|
52
|
+
*
|
|
53
|
+
* This top-level module is a thin re-export — the actual surface
|
|
54
|
+
* lives in lib/mail-crypto-pgp.js and lib/mail-crypto-smime.js.
|
|
55
|
+
*
|
|
56
|
+
* RFC citations:
|
|
57
|
+
* - RFC 9580 (OpenPGP, Nov 2024; obsoletes RFC 4880)
|
|
58
|
+
* - RFC 3156 (MIME Security with OpenPGP)
|
|
59
|
+
* - RFC 8551 (S/MIME 4.0 Message Specification; obsoletes RFC 5751)
|
|
60
|
+
* - RFC 5652 (Cryptographic Message Syntax)
|
|
61
|
+
* - RFC 8550 (S/MIME 4.0 Certificate Handling)
|
|
62
|
+
*
|
|
63
|
+
* CVE citations:
|
|
64
|
+
* - CVE-2017-17688 / CVE-2017-17689 (EFAIL)
|
|
65
|
+
* - CVE-2017-9006 (PKCS#7 / S/MIME signature-validation bypass class)
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
var pgp = require("./mail-crypto-pgp");
|
|
69
|
+
var smime = require("./mail-crypto-smime");
|
|
70
|
+
|
|
71
|
+
// Both sub-modules define `MailCryptoError` independently (each via
|
|
72
|
+
// `defineClass("MailCryptoError", { alwaysPermanent: true })`) — at
|
|
73
|
+
// runtime they are distinct classes. The facade re-exports the PGP
|
|
74
|
+
// one and provides `isMailCryptoError(e)` for the cross-protocol
|
|
75
|
+
// shape check that doesn't depend on class identity. Both classes
|
|
76
|
+
// extend FrameworkError and both set the `isMailCryptoError = true`
|
|
77
|
+
// flag, so the duck-type check is reliable across the boundary.
|
|
78
|
+
var MailCryptoError = pgp.MailCryptoError;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @primitive b.mail.crypto.isMailCryptoError
|
|
82
|
+
* @signature b.mail.crypto.isMailCryptoError(err)
|
|
83
|
+
* @since 0.9.58
|
|
84
|
+
* @status stable
|
|
85
|
+
*
|
|
86
|
+
* Duck-type check that returns true for any `MailCryptoError` raised
|
|
87
|
+
* by either sub-namespace. Each sub-module defines its own
|
|
88
|
+
* `MailCryptoError` class so `instanceof` doesn't span them; this
|
|
89
|
+
* helper checks the `isMailCryptoError === true` flag both classes
|
|
90
|
+
* set, giving operators one cross-protocol catch-all.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* try {
|
|
94
|
+
* b.mail.crypto.pgp.verify(opts);
|
|
95
|
+
* } catch (e) {
|
|
96
|
+
* if (b.mail.crypto.isMailCryptoError(e)) { handle(e); }
|
|
97
|
+
* }
|
|
98
|
+
*/
|
|
99
|
+
function isMailCryptoError(e) {
|
|
100
|
+
return !!(e && e.isMailCryptoError === true);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
pgp: pgp,
|
|
105
|
+
smime: smime,
|
|
106
|
+
MailCryptoError: MailCryptoError,
|
|
107
|
+
isMailCryptoError: isMailCryptoError,
|
|
108
|
+
};
|