@blamejs/core 0.9.46 → 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.
Files changed (78) hide show
  1. package/CHANGELOG.md +951 -893
  2. package/index.js +30 -0
  3. package/lib/_test/crypto-fixtures.js +67 -0
  4. package/lib/agent-event-bus.js +52 -6
  5. package/lib/agent-idempotency.js +169 -16
  6. package/lib/agent-orchestrator.js +263 -9
  7. package/lib/agent-posture-chain.js +163 -5
  8. package/lib/agent-saga.js +146 -16
  9. package/lib/agent-snapshot.js +349 -19
  10. package/lib/agent-stream.js +34 -2
  11. package/lib/agent-tenant.js +179 -23
  12. package/lib/agent-trace.js +84 -21
  13. package/lib/auth/aal.js +8 -1
  14. package/lib/auth/ciba.js +6 -1
  15. package/lib/auth/dpop.js +7 -2
  16. package/lib/auth/fal.js +17 -8
  17. package/lib/auth/jwt-external.js +128 -4
  18. package/lib/auth/oauth.js +232 -10
  19. package/lib/auth/oid4vci.js +67 -7
  20. package/lib/auth/openid-federation.js +71 -25
  21. package/lib/auth/passkey.js +140 -6
  22. package/lib/auth/sd-jwt-vc.js +67 -5
  23. package/lib/circuit-breaker.js +10 -2
  24. package/lib/compliance.js +176 -8
  25. package/lib/crypto-field.js +114 -14
  26. package/lib/crypto.js +216 -20
  27. package/lib/db.js +1 -0
  28. package/lib/guard-imap-command.js +335 -0
  29. package/lib/guard-jmap.js +321 -0
  30. package/lib/guard-managesieve-command.js +566 -0
  31. package/lib/guard-pop3-command.js +317 -0
  32. package/lib/guard-smtp-command.js +58 -3
  33. package/lib/mail-agent.js +20 -7
  34. package/lib/mail-arc-sign.js +12 -8
  35. package/lib/mail-auth.js +323 -34
  36. package/lib/mail-crypto-pgp.js +934 -0
  37. package/lib/mail-crypto-smime.js +340 -0
  38. package/lib/mail-crypto.js +108 -0
  39. package/lib/mail-dav.js +1224 -0
  40. package/lib/mail-deploy.js +492 -0
  41. package/lib/mail-dkim.js +431 -26
  42. package/lib/mail-journal.js +435 -0
  43. package/lib/mail-scan.js +502 -0
  44. package/lib/mail-server-imap.js +1102 -0
  45. package/lib/mail-server-jmap.js +488 -0
  46. package/lib/mail-server-managesieve.js +853 -0
  47. package/lib/mail-server-mx.js +164 -34
  48. package/lib/mail-server-pop3.js +836 -0
  49. package/lib/mail-server-rate-limit.js +269 -0
  50. package/lib/mail-server-submission.js +1032 -0
  51. package/lib/mail-server-tls.js +445 -0
  52. package/lib/mail-sieve.js +557 -0
  53. package/lib/mail-spam-score.js +284 -0
  54. package/lib/mail.js +99 -0
  55. package/lib/metrics.js +130 -10
  56. package/lib/middleware/dpop.js +58 -3
  57. package/lib/middleware/idempotency-key.js +255 -42
  58. package/lib/middleware/protected-resource-metadata.js +114 -2
  59. package/lib/network-dns-resolver.js +33 -0
  60. package/lib/network-tls.js +46 -0
  61. package/lib/outbox.js +62 -12
  62. package/lib/pqc-agent.js +13 -5
  63. package/lib/retry.js +23 -9
  64. package/lib/router.js +23 -1
  65. package/lib/safe-ical.js +634 -0
  66. package/lib/safe-icap.js +502 -0
  67. package/lib/safe-mime.js +15 -0
  68. package/lib/safe-sieve.js +684 -0
  69. package/lib/safe-smtp.js +57 -0
  70. package/lib/safe-url.js +37 -0
  71. package/lib/safe-vcard.js +473 -0
  72. package/lib/self-update-standalone-verifier.js +32 -3
  73. package/lib/self-update.js +168 -17
  74. package/lib/vendor/MANIFEST.json +161 -156
  75. package/lib/vendor-data.js +127 -9
  76. package/lib/vex.js +324 -59
  77. package/package.json +1 -1
  78. package/sbom.cdx.json +6 -6
@@ -0,0 +1,934 @@
1
+ "use strict";
2
+ // codebase-patterns:allow-file raw-byte-literal — RFC 9580 OpenPGP packet
3
+ // framing carries protocol-mandated byte-shape constants throughout (32-byte
4
+ // Ed25519 keys, 64-byte signature halves, 192 / 8384 / 224 length-octet
5
+ // thresholds, /8 bit-to-byte conversions). These are protocol literals, not
6
+ // memory caps; the C.BYTES.kib/mib helpers don't apply.
7
+ /**
8
+ * @module b.mail.crypto.pgp
9
+ * @nav Communication
10
+ * @title Mail PGP
11
+ * @order 120
12
+ * @slug mail-crypto-pgp
13
+ *
14
+ * @card
15
+ * OpenPGP detached-signature sign + verify for mail per RFC 9580
16
+ * (Nov 2024). v4 Ed25519 / RSA-PKCS#1-v1.5, multipart/signed.
17
+ *
18
+ * @intro
19
+ * OpenPGP detached-signature signing + verification for mail per
20
+ * RFC 9580 (the November 2024 OpenPGP revision that obsoletes
21
+ * RFC 4880). Produces `multipart/signed; protocol=
22
+ * "application/pgp-signature"` per RFC 3156 §5 with a v4 OpenPGP
23
+ * signature packet wrapped in ASCII armor (RFC 9580 §6).
24
+ *
25
+ * Supported v1 surface (sign + verify):
26
+ * - Ed25519 v4 signatures using OpenPGP public-key algorithm 22
27
+ * (Ed25519Legacy per RFC 9580 §9.1), the universally-supported
28
+ * Ed25519 form. RFC 9580 also defines algorithm 27 (Ed25519)
29
+ * for v6 signatures; v6 signature output is deferred — see
30
+ * the deferral note below.
31
+ * - RSA v4 signatures using OpenPGP public-key algorithm 1 (RSA)
32
+ * with EMSA-PKCS1-v1_5 padding over SHA-256, which is what
33
+ * every fielded PGP implementation expects for v4 RSA
34
+ * signatures. Keys < 2048 bits are refused at sign time
35
+ * (RFC 8301 §3.1 RSA floor; v0.7.x DKIM established the same
36
+ * posture across the mail surface).
37
+ *
38
+ * Threat model:
39
+ * - EFAIL (CVE-2017-17688 / CVE-2017-17689) attacks decrypt-and-
40
+ * render flows that (a) fetch remote content in encrypted parts,
41
+ * (b) tolerate MIME-part-structure mutation between decrypt and
42
+ * render, or (c) feed decrypted HTML to a permissive renderer.
43
+ * This v1 surface is sign + verify only, so EFAIL does not bind
44
+ * directly. When encrypt + decrypt lights up (see deferral note)
45
+ * the renderer-side gate is `b.guardHtml` strict profile,
46
+ * inline image fetches in encrypted parts are refused, and the
47
+ * MIME-part tree captured at decrypt time is compared byte-for-
48
+ * byte against the tree at render time.
49
+ * - SHA-1 collision attacks (SHAttered, 2017) on signature hash
50
+ * inputs — refuse SHA-1 as the signature hash on verify and
51
+ * never emit it from sign.
52
+ * - Hash-algorithm-confusion: the signature's `hash_alg` field is
53
+ * enforced against the locally-recomputed hash; verifying with
54
+ * a different algorithm than was signed is refused.
55
+ * - Key-fingerprint pinning: verify() returns the v4 fingerprint
56
+ * (RFC 9580 §5.5.4) of the signing key so the caller can pin
57
+ * to a known operator key rather than trusting any key that
58
+ * happens to match the signature.
59
+ *
60
+ * Deferred from v1 (each with the documented condition for opting in):
61
+ * - In-process encrypt + decrypt (Message Encrypted Session Key +
62
+ * Symmetrically Encrypted Integrity Protected Data packets,
63
+ * RFC 9580 §5.1 / §5.13). Defer condition: no operator demand
64
+ * surfaced for in-process encrypt-to-recipient yet. Operators
65
+ * wanting at-rest encrypted mail blobs compose `b.vault` +
66
+ * `b.cryptoField`; operators wanting wire-level encrypt-to-
67
+ * recipient with WKD key discovery wait for v0.9.59 once
68
+ * `b.publicSuffix` + WKD lookup are confirmed. Cheap escape
69
+ * hatch: operators wire a third-party OpenPGP library in their
70
+ * own consumer code and call sign() / verify() on the resulting
71
+ * cleartext blob.
72
+ * - v6 signature packets (RFC 9580 §5.2.3, packet version 6 with
73
+ * SHA2-512 fingerprints and salted hashes). Defer condition: v6
74
+ * is not yet emitted by GnuPG 2.4 LTS or by Sequoia stable, so
75
+ * v6 output would fail to verify on the majority of fielded
76
+ * receivers. Reopen when at least two major implementations
77
+ * ship v6 signature verification by default. Cheap escape
78
+ * hatch: operators on v6-only systems can ingest the v4
79
+ * signature from this module and re-sign with their own
80
+ * v6-capable toolchain.
81
+ *
82
+ * Surface:
83
+ * var sigBundle = b.mail.crypto.pgp.sign({
84
+ * message: "rfc822 body bytes",
85
+ * privateKeyPem: "-----BEGIN PRIVATE KEY----- ...",
86
+ * passphrase: undefined | "...", // optional
87
+ * audit: opts.audit, // optional b.audit handle
88
+ * });
89
+ * // → { armored: "-----BEGIN PGP SIGNATURE----- ...",
90
+ * // multipartSigned: "Content-Type: multipart/signed; ...",
91
+ * // signedAt: epochSeconds, fingerprint: "abcd..." }
92
+ *
93
+ * var rv = b.mail.crypto.pgp.verify({
94
+ * message: "the signed payload bytes",
95
+ * armored: "-----BEGIN PGP SIGNATURE----- ...",
96
+ * publicKeyPem: "-----BEGIN PUBLIC KEY----- ...",
97
+ * audit: opts.audit,
98
+ * });
99
+ * // → { ok: true, signerFingerprint: "abcd...", signedAt: epoch, hashAlg: "sha256" }
100
+ *
101
+ * The signer's `message` MUST be the canonicalized payload that the
102
+ * verifier will recompute over. For `multipart/signed` per RFC 3156
103
+ * §5, the canonical form is the signed part's full MIME headers +
104
+ * body with CRLF line endings — operators producing such a body
105
+ * should pass exactly those bytes here.
106
+ *
107
+ * RFC citations:
108
+ * - RFC 9580 (OpenPGP, Nov 2024; obsoletes RFC 4880)
109
+ * - RFC 3156 (MIME Security with OpenPGP)
110
+ * - RFC 8301 (DKIM RSA floor — reused as the cross-surface RSA bit floor)
111
+ *
112
+ * CVE citations:
113
+ * - CVE-2017-17688 / CVE-2017-17689 (EFAIL — informs the encrypt/
114
+ * decrypt deferral conditions above)
115
+ * - CVE-2019-13050 (PGP keyserver flood — not in scope here; out-of-
116
+ * band fingerprint pinning is the operator's responsibility)
117
+ */
118
+ var lazyRequire = require("./lazy-require");
119
+ var audit = lazyRequire(function () { return require("./audit"); });
120
+ var nodeCrypto = require("node:crypto");
121
+ var validateOpts = require("./validate-opts");
122
+ var { defineClass } = require("./framework-error");
123
+
124
+ var MailCryptoError = defineClass("MailCryptoError", { alwaysPermanent: true });
125
+
126
+ // RFC 9580 §9 public-key algorithm IDs that this module emits/accepts.
127
+ var PUB_ALG_RSA = 1; // allow:raw-byte-literal — RFC 9580 §9.1 RSA
128
+ var PUB_ALG_ED25519_LEGACY = 22; // allow:raw-byte-literal — RFC 9580 §9.1 EdDSA Ed25519Legacy
129
+
130
+ // RFC 9580 §9.5 hash algorithm IDs.
131
+ var HASH_ALG_SHA256 = 8; // allow:raw-byte-literal — RFC 9580 §9.5 SHA2-256
132
+ var HASH_ALG_SHA512 = 10; // allow:raw-byte-literal — RFC 9580 §9.5 SHA2-512
133
+
134
+ // RFC 9580 §5.2.1 signature type — Signature of a binary document.
135
+ var SIG_TYPE_BINARY = 0; // allow:raw-byte-literal — RFC 9580 §5.2.1
136
+
137
+ // RFC 9580 §5.2.3.1 subpacket types we emit / consume.
138
+ var SUBPKT_SIG_CREATION_TIME = 2; // allow:raw-byte-literal — RFC 9580 §5.2.3.4
139
+ var SUBPKT_ISSUER_FPR = 33; // allow:raw-byte-literal — RFC 9580 §5.2.3.35 Issuer Fingerprint
140
+
141
+ // RSA modulus floor — matches DKIM RFC 8301 §3.1 and the framework's
142
+ // cross-mail-surface posture (lib/mail-dkim.js RSA_WEAK_BITS).
143
+ var RSA_MIN_BITS = 2048; // allow:raw-byte-literal — RFC 8301 §3.1
144
+
145
+ // ASCII armor framing per RFC 9580 §6.2.
146
+ var ARMOR_BEGIN = "-----BEGIN PGP SIGNATURE-----";
147
+ var ARMOR_END = "-----END PGP SIGNATURE-----";
148
+
149
+ // ---- Buffer helpers ----
150
+
151
+ function _u8(n) {
152
+ var b = Buffer.alloc(1);
153
+ b.writeUInt8(n & 0xff, 0);
154
+ return b;
155
+ }
156
+
157
+ function _u16be(n) {
158
+ var b = Buffer.alloc(2);
159
+ b.writeUInt16BE(n & 0xffff, 0);
160
+ return b;
161
+ }
162
+
163
+ function _u32be(n) {
164
+ var b = Buffer.alloc(4);
165
+ b.writeUInt32BE(n >>> 0, 0);
166
+ return b;
167
+ }
168
+
169
+ // RFC 9580 §3.2 — Multi-Precision Integer encoding: 2-byte big-endian
170
+ // bit-length, followed by ceil(bits/8) value bytes. Leading zero bytes
171
+ // of the raw integer are stripped before the bit count is computed.
172
+ function _mpi(raw) {
173
+ // Strip leading zero bytes.
174
+ var i = 0;
175
+ while (i < raw.length - 1 && raw[i] === 0) i += 1;
176
+ var stripped = raw.slice(i);
177
+ // Bit-length of the most-significant byte.
178
+ var msb = stripped[0];
179
+ var bits = (stripped.length - 1) * 8;
180
+ for (var b = 7; b >= 0; b -= 1) {
181
+ if ((msb >> b) & 1) { bits += b + 1; break; }
182
+ }
183
+ if (bits === 0) bits = 1;
184
+ return Buffer.concat([_u16be(bits), stripped]);
185
+ }
186
+
187
+ // RFC 9580 §4.2.1 — new-format packet length octets.
188
+ function _encodeNewLength(length) {
189
+ if (length < 192) {
190
+ return _u8(length);
191
+ }
192
+ if (length < 8384) {
193
+ var first = ((length - 192) >> 8) + 192;
194
+ var second = (length - 192) & 0xff;
195
+ return Buffer.from([first, second]);
196
+ }
197
+ // 5-octet length: 0xff || 4-byte big-endian.
198
+ return Buffer.concat([_u8(0xff), _u32be(length)]);
199
+ }
200
+
201
+ // RFC 9580 §4.2 packet framing — new-format header for tag T:
202
+ // byte0 = 0b11TTTTTT
203
+ function _packetHeader(tag, bodyLength) {
204
+ var firstByte = 0xc0 | (tag & 0x3f);
205
+ return Buffer.concat([_u8(firstByte), _encodeNewLength(bodyLength)]);
206
+ }
207
+
208
+ // RFC 9580 §5.2.3.1 — subpacket length octets (same encoding as
209
+ // packet length octets in §4.2.1).
210
+ function _encodeSubpacketLength(length) {
211
+ return _encodeNewLength(length);
212
+ }
213
+
214
+ function _subpacket(type, body) {
215
+ // Subpacket = length-of(type-byte + body) || type-byte || body
216
+ var typeBuf = _u8(type & 0xff);
217
+ var inner = Buffer.concat([typeBuf, body]);
218
+ return Buffer.concat([_encodeSubpacketLength(inner.length), inner]);
219
+ }
220
+
221
+ // ---- Key fingerprint (RFC 9580 §5.5.4) ----
222
+ //
223
+ // v4 fingerprint = SHA-1 over (0x99 || u16be(publicPacketBodyLen) ||
224
+ // publicPacketBody). SHA-1 here is the spec — we are NOT hashing for
225
+ // signature integrity (verify-time hash alg is enforced separately);
226
+ // SHA-1's use as a fingerprint identifier is per RFC 9580 §5.5.4 v4
227
+ // fingerprint definition. RFC 9580 also defines v6 fingerprints
228
+ // (SHA-256) but v6 is deferred per the module @intro.
229
+ function _v4Fingerprint(publicPacketBody) {
230
+ var len = publicPacketBody.length;
231
+ var preimage = Buffer.concat([
232
+ _u8(0x99), _u16be(len), publicPacketBody,
233
+ ]);
234
+ return nodeCrypto.createHash("sha1").update(preimage).digest();
235
+ }
236
+
237
+ // ---- Public key packet body (RFC 9580 §5.5.2) ----
238
+
239
+ function _ed25519PublicPacketBody(rawPub32, creationTime) {
240
+ // v4 packet body:
241
+ // version(1)=4 || creationTime(4) || pubAlg(1)=22 ||
242
+ // curveOidLen(1) || curveOid || pointMpi
243
+ // Ed25519Legacy curve OID per RFC 9580 §9.2 = 1.3.6.1.4.1.11591.15.1
244
+ // encoded as: 0x2b 0x06 0x01 0x04 0x01 0xda 0x47 0x0f 0x01 (9 bytes).
245
+ var oid = Buffer.from([0x2b, 0x06, 0x01, 0x04, 0x01, 0xda, 0x47, 0x0f, 0x01]);
246
+ // The point is 0x40 || 32-byte raw Ed25519 public key (RFC 9580 §9.2).
247
+ var point = Buffer.concat([_u8(0x40), rawPub32]);
248
+ return Buffer.concat([
249
+ _u8(4),
250
+ _u32be(creationTime),
251
+ _u8(PUB_ALG_ED25519_LEGACY),
252
+ _u8(oid.length),
253
+ oid,
254
+ _mpi(point),
255
+ ]);
256
+ }
257
+
258
+ function _rsaPublicPacketBody(nBuf, eBuf, creationTime) {
259
+ // v4 packet body:
260
+ // version(1)=4 || creationTime(4) || pubAlg(1)=1 || n-mpi || e-mpi
261
+ return Buffer.concat([
262
+ _u8(4),
263
+ _u32be(creationTime),
264
+ _u8(PUB_ALG_RSA),
265
+ _mpi(nBuf),
266
+ _mpi(eBuf),
267
+ ]);
268
+ }
269
+
270
+ // ---- ASCII armor (RFC 9580 §6.2 + §6.1 CRC-24) ----
271
+
272
+ function _crc24(data) {
273
+ // RFC 9580 §6.1 CRC-24.
274
+ var crc = 0x00b704ce;
275
+ for (var i = 0; i < data.length; i += 1) {
276
+ crc ^= data[i] << 16;
277
+ for (var j = 0; j < 8; j += 1) {
278
+ crc <<= 1;
279
+ if (crc & 0x01000000) crc ^= 0x01864cfb;
280
+ }
281
+ }
282
+ return crc & 0xffffff;
283
+ }
284
+
285
+ function _armor(packetBytes) {
286
+ var b64 = packetBytes.toString("base64");
287
+ var lines = [];
288
+ for (var i = 0; i < b64.length; i += 64) {
289
+ lines.push(b64.slice(i, i + 64));
290
+ }
291
+ var crc = _crc24(packetBytes);
292
+ var crcBuf = Buffer.from([(crc >> 16) & 0xff, (crc >> 8) & 0xff, crc & 0xff]);
293
+ var crcB64 = crcBuf.toString("base64");
294
+ return [
295
+ ARMOR_BEGIN,
296
+ "",
297
+ lines.join("\r\n"),
298
+ "=" + crcB64,
299
+ ARMOR_END,
300
+ ].join("\r\n") + "\r\n";
301
+ }
302
+
303
+ function _dearmor(armored) {
304
+ if (typeof armored !== "string") {
305
+ throw new MailCryptoError("mail-crypto/pgp/bad-armor",
306
+ "armored signature must be a string");
307
+ }
308
+ var beginIdx = armored.indexOf(ARMOR_BEGIN);
309
+ var endIdx = armored.indexOf(ARMOR_END);
310
+ if (beginIdx === -1 || endIdx === -1 || endIdx < beginIdx) {
311
+ throw new MailCryptoError("mail-crypto/pgp/bad-armor",
312
+ "armored signature missing BEGIN/END framing per RFC 9580 §6.2");
313
+ }
314
+ var inner = armored.slice(beginIdx + ARMOR_BEGIN.length, endIdx);
315
+ // Skip header lines (terminated by a blank line) per RFC 9580 §6.2.
316
+ var lines = inner.replace(/\r\n/g, "\n").split("\n");
317
+ var k = 0;
318
+ // Drop leading empty lines.
319
+ while (k < lines.length && lines[k] === "") k += 1;
320
+ // Skip header lines until blank.
321
+ while (k < lines.length && lines[k].indexOf(":") !== -1) k += 1;
322
+ if (k < lines.length && lines[k] === "") k += 1;
323
+ // Collect base64 body until CRC line (leading "=").
324
+ var b64 = "";
325
+ var crcLine = null;
326
+ for (; k < lines.length; k += 1) {
327
+ var ln = lines[k];
328
+ if (ln === "") continue;
329
+ if (ln.charAt(0) === "=") { crcLine = ln.slice(1); break; }
330
+ b64 += ln;
331
+ }
332
+ if (crcLine === null) {
333
+ throw new MailCryptoError("mail-crypto/pgp/bad-armor",
334
+ "armored signature missing CRC-24 trailer per RFC 9580 §6.1");
335
+ }
336
+ var packetBytes = Buffer.from(b64, "base64");
337
+ var expectedCrc = _crc24(packetBytes);
338
+ var crcBuf = Buffer.from(crcLine, "base64");
339
+ if (crcBuf.length !== 3) {
340
+ throw new MailCryptoError("mail-crypto/pgp/bad-armor",
341
+ "armored signature CRC-24 trailer must decode to 3 bytes");
342
+ }
343
+ var seenCrc = (crcBuf[0] << 16) | (crcBuf[1] << 8) | crcBuf[2];
344
+ if (seenCrc !== expectedCrc) {
345
+ throw new MailCryptoError("mail-crypto/pgp/bad-armor",
346
+ "armored signature CRC-24 mismatch — armor is corrupt");
347
+ }
348
+ return packetBytes;
349
+ }
350
+
351
+ // ---- Key-shape extraction (node:crypto KeyObject → raw integers) ----
352
+
353
+ function _extractRsaPublicComponents(keyObject) {
354
+ // node:crypto exposes jwk export for RSA keys: { kty:"RSA", n, e }.
355
+ var jwk = keyObject.export({ format: "jwk" });
356
+ if (!jwk || jwk.kty !== "RSA") {
357
+ throw new MailCryptoError("mail-crypto/pgp/bad-key",
358
+ "expected RSA key, got " + (jwk && jwk.kty));
359
+ }
360
+ var n = Buffer.from(jwk.n, "base64url");
361
+ var e = Buffer.from(jwk.e, "base64url");
362
+ return { n: n, e: e, bits: n.length * 8 };
363
+ }
364
+
365
+ function _extractEd25519PublicRaw(keyObject) {
366
+ var jwk = keyObject.export({ format: "jwk" });
367
+ if (!jwk || jwk.kty !== "OKP" || jwk.crv !== "Ed25519") {
368
+ throw new MailCryptoError("mail-crypto/pgp/bad-key",
369
+ "expected Ed25519 key, got " + (jwk && (jwk.kty + "/" + jwk.crv)));
370
+ }
371
+ var raw = Buffer.from(jwk.x, "base64url");
372
+ if (raw.length !== 32) {
373
+ throw new MailCryptoError("mail-crypto/pgp/bad-key",
374
+ "Ed25519 public key must decode to 32 bytes");
375
+ }
376
+ return raw;
377
+ }
378
+
379
+ // ---- Sign ----
380
+
381
+ function _hashName(hashAlgId) {
382
+ if (hashAlgId === HASH_ALG_SHA256) return "sha256";
383
+ if (hashAlgId === HASH_ALG_SHA512) return "sha512";
384
+ throw new MailCryptoError("mail-crypto/pgp/bad-hash",
385
+ "hash algorithm " + hashAlgId + " not supported; only SHA-256 / SHA-512");
386
+ }
387
+
388
+ /**
389
+ * @primitive b.mail.crypto.pgp.sign
390
+ * @signature b.mail.crypto.pgp.sign(opts)
391
+ * @since 0.9.58
392
+ * @status stable
393
+ * @compliance hipaa, pci-dss, gdpr, soc2
394
+ *
395
+ * Produces a v4 OpenPGP detached signature over `opts.message` and
396
+ * returns the ASCII-armored signature plus a ready-to-emit
397
+ * `multipart/signed; protocol="application/pgp-signature"` body
398
+ * (RFC 3156 §5). Ed25519 (algorithm 22) and RSA-PKCS#1-v1.5 over
399
+ * SHA-256 (algorithm 1) are the v1 signing forms; RSA keys below
400
+ * 2048 bits are refused per RFC 8301 §3.1.
401
+ *
402
+ * @example
403
+ * var rv = b.mail.crypto.pgp.sign({
404
+ * message: "rfc822 body bytes",
405
+ * privateKeyPem: pem,
406
+ * });
407
+ * // → { armored, multipartSigned, signedAt, fingerprint }
408
+ */
409
+ function sign(opts) {
410
+ opts = validateOpts.requireObject(opts, "mail.crypto.pgp.sign", MailCryptoError, "mail-crypto/pgp/bad-opts");
411
+ validateOpts(opts, ["message", "privateKeyPem", "passphrase", "audit", "creationTime"], "mail.crypto.pgp.sign");
412
+
413
+ var message = opts.message;
414
+ if (!(typeof message === "string" || Buffer.isBuffer(message))) {
415
+ throw new MailCryptoError("mail-crypto/pgp/bad-message",
416
+ "message must be a string or Buffer");
417
+ }
418
+ if (message.length === 0) {
419
+ throw new MailCryptoError("mail-crypto/pgp/bad-message",
420
+ "message must be non-empty");
421
+ }
422
+ validateOpts.requireNonEmptyString(opts.privateKeyPem, "privateKeyPem",
423
+ MailCryptoError, "mail-crypto/pgp/bad-key");
424
+ if (opts.passphrase !== undefined && opts.passphrase !== null &&
425
+ typeof opts.passphrase !== "string") {
426
+ throw new MailCryptoError("mail-crypto/pgp/bad-passphrase",
427
+ "passphrase must be a string when provided");
428
+ }
429
+
430
+ var creationTime = (opts.creationTime === undefined)
431
+ ? Math.floor(Date.now() / 1000)
432
+ : opts.creationTime;
433
+ if (typeof creationTime !== "number" || !isFinite(creationTime) ||
434
+ creationTime < 0 || Math.floor(creationTime) !== creationTime) {
435
+ throw new MailCryptoError("mail-crypto/pgp/bad-creation-time",
436
+ "creationTime must be a non-negative integer epoch-seconds");
437
+ }
438
+
439
+ var privateKey;
440
+ try {
441
+ var keyOpts = { key: opts.privateKeyPem, format: "pem" };
442
+ if (opts.passphrase) keyOpts.passphrase = opts.passphrase;
443
+ privateKey = nodeCrypto.createPrivateKey(keyOpts);
444
+ } catch (e) {
445
+ throw new MailCryptoError("mail-crypto/pgp/bad-key",
446
+ "privateKeyPem could not be parsed: " + ((e && e.message) || String(e)));
447
+ }
448
+
449
+ var publicKey = nodeCrypto.createPublicKey(privateKey);
450
+ var keyType = privateKey.asymmetricKeyType; // "rsa" | "ed25519" | ...
451
+
452
+ var pubAlg, hashAlg, publicPacketBody;
453
+ if (keyType === "ed25519") {
454
+ pubAlg = PUB_ALG_ED25519_LEGACY;
455
+ hashAlg = HASH_ALG_SHA512;
456
+ var rawPub = _extractEd25519PublicRaw(publicKey);
457
+ publicPacketBody = _ed25519PublicPacketBody(rawPub, creationTime);
458
+ } else if (keyType === "rsa" || keyType === "rsa-pss") {
459
+ pubAlg = PUB_ALG_RSA;
460
+ hashAlg = HASH_ALG_SHA256;
461
+ var rsaPub = _extractRsaPublicComponents(publicKey);
462
+ if (rsaPub.bits < RSA_MIN_BITS) {
463
+ throw new MailCryptoError("mail-crypto/pgp/rsa-too-small",
464
+ "RSA key is " + rsaPub.bits + " bits; minimum is " + RSA_MIN_BITS +
465
+ " (RFC 8301 §3.1)");
466
+ }
467
+ publicPacketBody = _rsaPublicPacketBody(rsaPub.n, rsaPub.e, creationTime);
468
+ } else {
469
+ throw new MailCryptoError("mail-crypto/pgp/bad-key-type",
470
+ "unsupported privateKey algorithm '" + keyType +
471
+ "'; only ed25519 and rsa are supported");
472
+ }
473
+
474
+ var fingerprint = _v4Fingerprint(publicPacketBody);
475
+
476
+ // RFC 9580 §5.2.3 — hashed subpackets we always include:
477
+ // - Signature Creation Time (2)
478
+ // - Issuer Fingerprint v4 (33) — version byte 0x04 || 20-byte fpr
479
+ var hashedSub = Buffer.concat([
480
+ _subpacket(SUBPKT_SIG_CREATION_TIME, _u32be(creationTime)),
481
+ _subpacket(SUBPKT_ISSUER_FPR, Buffer.concat([_u8(4), fingerprint])),
482
+ ]);
483
+
484
+ // RFC 9580 §5.2.4 — Compute signed hash:
485
+ // data || signed-section || trailer
486
+ // where signed-section is the bytes from version through end of
487
+ // hashed subpackets, and trailer is 0x04 0xff || u32be(signedSectionLen).
488
+ // The signed-section is:
489
+ // version(1)=4 || sigType(1) || pubAlg(1) || hashAlg(1) ||
490
+ // hashedSubLen(2) || hashedSub
491
+ var signedSection = Buffer.concat([
492
+ _u8(4),
493
+ _u8(SIG_TYPE_BINARY),
494
+ _u8(pubAlg),
495
+ _u8(hashAlg),
496
+ _u16be(hashedSub.length),
497
+ hashedSub,
498
+ ]);
499
+
500
+ var trailer = Buffer.concat([
501
+ _u8(4), _u8(0xff), _u32be(signedSection.length),
502
+ ]);
503
+
504
+ var dataBuf = Buffer.isBuffer(message) ? message : Buffer.from(message, "utf8");
505
+
506
+ var hashName = _hashName(hashAlg);
507
+ var digest = nodeCrypto.createHash(hashName)
508
+ .update(dataBuf)
509
+ .update(signedSection)
510
+ .update(trailer)
511
+ .digest();
512
+
513
+ // RFC 9580 §5.2.4 — the signature packet records the leftmost 2
514
+ // octets of the hash so verifiers can fail fast on the wrong key.
515
+ var hashLeft16 = digest.slice(0, 2);
516
+
517
+ // Now produce the actual asymmetric signature over the digest.
518
+ var sigMpis;
519
+ if (pubAlg === PUB_ALG_RSA) {
520
+ // RSA EMSA-PKCS1-v1_5 over the precomputed digest.
521
+ var rsaSig = nodeCrypto.sign(hashName, Buffer.concat([dataBuf, signedSection, trailer]), {
522
+ key: privateKey,
523
+ padding: nodeCrypto.constants.RSA_PKCS1_PADDING,
524
+ });
525
+ sigMpis = _mpi(rsaSig);
526
+ } else {
527
+ // Ed25519Legacy — signs the precomputed-digest input. EdDSA signs
528
+ // the message directly; RFC 9580 §5.2.4 specifies signing over the
529
+ // same hash input as the digest computation. Per RFC 9580 §13.7
530
+ // (Ed25519Legacy) the signed message is the SHA-512 hash bytes.
531
+ var edSig = nodeCrypto.sign(null,
532
+ Buffer.concat([dataBuf, signedSection, trailer]), privateKey);
533
+ // edSig is 64 raw bytes (R || S). RFC 9580 §5.2.3 encodes R and S
534
+ // as two 256-bit MPIs.
535
+ if (edSig.length !== 64) {
536
+ throw new MailCryptoError("mail-crypto/pgp/bad-signature",
537
+ "Ed25519 raw signature must be 64 bytes; got " + edSig.length);
538
+ }
539
+ sigMpis = Buffer.concat([_mpi(edSig.slice(0, 32)), _mpi(edSig.slice(32))]);
540
+ }
541
+
542
+ // Assemble the signature packet body.
543
+ // version(1)=4 || sigType(1) || pubAlg(1) || hashAlg(1) ||
544
+ // hashedSubLen(2) || hashedSub ||
545
+ // unhashedSubLen(2)=0 ||
546
+ // hashLeft16(2) || sigMpis
547
+ var unhashedSub = Buffer.alloc(0);
548
+ var sigBody = Buffer.concat([
549
+ signedSection,
550
+ _u16be(unhashedSub.length),
551
+ unhashedSub,
552
+ hashLeft16,
553
+ sigMpis,
554
+ ]);
555
+
556
+ // Tag 2 = Signature packet (RFC 9580 §5.2).
557
+ var packet = Buffer.concat([_packetHeader(2, sigBody.length), sigBody]);
558
+
559
+ var armored = _armor(packet);
560
+
561
+ // RFC 3156 §5 multipart/signed wrapper. The signer is responsible
562
+ // for assembling the message body that gets signed; we provide the
563
+ // boundary structure once the caller hands us their canonicalized
564
+ // signed-part bytes plus the armored signature.
565
+ // MIME-boundary uniqueness only (not a security token); operator
566
+ // key/cert material flows through createSign/verify, not this path.
567
+ // allow:raw-randombytes-token — boundary string, not auth credential
568
+ var boundary = "blamejs-pgp-" + nodeCrypto.randomBytes(12).toString("hex");
569
+ var multipartSigned =
570
+ 'Content-Type: multipart/signed; micalg="pgp-' + hashName + '"; ' +
571
+ 'protocol="application/pgp-signature"; boundary="' + boundary + '"\r\n' +
572
+ "\r\n" +
573
+ "--" + boundary + "\r\n" +
574
+ (Buffer.isBuffer(message) ? message.toString("binary") : message) +
575
+ "\r\n--" + boundary + "\r\n" +
576
+ 'Content-Type: application/pgp-signature; name="signature.asc"\r\n' +
577
+ "Content-Description: OpenPGP digital signature\r\n" +
578
+ 'Content-Disposition: attachment; filename="signature.asc"\r\n' +
579
+ "\r\n" +
580
+ armored +
581
+ "--" + boundary + "--\r\n";
582
+
583
+ // Audit (drop-silent — never crash the request that triggered us).
584
+ _audit(opts.audit, "mail.crypto.pgp.sign", "success", {
585
+ keyType: keyType,
586
+ hashAlg: hashName,
587
+ fingerprint: fingerprint.toString("hex"),
588
+ signedAt: creationTime,
589
+ });
590
+
591
+ return {
592
+ armored: armored,
593
+ multipartSigned: multipartSigned,
594
+ signedAt: creationTime,
595
+ fingerprint: fingerprint.toString("hex"),
596
+ hashAlg: hashName,
597
+ boundary: boundary,
598
+ };
599
+ }
600
+
601
+ // ---- Verify ----
602
+
603
+ function _parseSignaturePacket(packetBytes) {
604
+ // RFC 9580 §4.2 — accept new-format packets only (legacy/old format
605
+ // is RFC 1991 vintage; producers since the 1998 RFC 2440 era emit
606
+ // new-format). Header byte: 0b11TTTTTT.
607
+ if (packetBytes.length < 2) {
608
+ throw new MailCryptoError("mail-crypto/pgp/bad-packet",
609
+ "signature packet too short");
610
+ }
611
+ var first = packetBytes[0];
612
+ if ((first & 0xc0) !== 0xc0) {
613
+ throw new MailCryptoError("mail-crypto/pgp/bad-packet",
614
+ "expected new-format packet header per RFC 9580 §4.2 (legacy/old-format input refused)");
615
+ }
616
+ var tag = first & 0x3f;
617
+ if (tag !== 2) {
618
+ throw new MailCryptoError("mail-crypto/pgp/bad-packet",
619
+ "expected Signature packet (tag=2) per RFC 9580 §5.2; got tag " + tag);
620
+ }
621
+ // Parse length.
622
+ var idx = 1;
623
+ var bodyLen;
624
+ var lenFirst = packetBytes[idx];
625
+ if (lenFirst < 192) {
626
+ bodyLen = lenFirst;
627
+ idx += 1;
628
+ } else if (lenFirst < 224) {
629
+ if (idx + 2 > packetBytes.length) {
630
+ throw new MailCryptoError("mail-crypto/pgp/bad-packet", "truncated length");
631
+ }
632
+ bodyLen = ((lenFirst - 192) << 8) + packetBytes[idx + 1] + 192;
633
+ idx += 2;
634
+ } else if (lenFirst === 0xff) {
635
+ if (idx + 5 > packetBytes.length) {
636
+ throw new MailCryptoError("mail-crypto/pgp/bad-packet", "truncated length");
637
+ }
638
+ bodyLen = packetBytes.readUInt32BE(idx + 1);
639
+ idx += 5;
640
+ } else {
641
+ throw new MailCryptoError("mail-crypto/pgp/bad-packet",
642
+ "partial-body length octets refused — full-length packets only");
643
+ }
644
+ if (idx + bodyLen > packetBytes.length) {
645
+ throw new MailCryptoError("mail-crypto/pgp/bad-packet",
646
+ "signature packet body truncated");
647
+ }
648
+ var body = packetBytes.slice(idx, idx + bodyLen);
649
+
650
+ if (body.length < 6 || body[0] !== 4) {
651
+ throw new MailCryptoError("mail-crypto/pgp/bad-version",
652
+ "only v4 signature packets supported (v6 deferred per @intro)");
653
+ }
654
+ var sigType = body[1];
655
+ var pubAlg = body[2];
656
+ var hashAlg = body[3];
657
+ if (sigType !== SIG_TYPE_BINARY) {
658
+ throw new MailCryptoError("mail-crypto/pgp/bad-sig-type",
659
+ "only binary-document signatures (type=0) accepted; got " + sigType);
660
+ }
661
+ if (hashAlg !== HASH_ALG_SHA256 && hashAlg !== HASH_ALG_SHA512) {
662
+ throw new MailCryptoError("mail-crypto/pgp/bad-hash",
663
+ "hash alg " + hashAlg + " refused; only SHA-256 (8) and SHA-512 (10) are accepted. " +
664
+ "SHA-1 (id=2) refused per SHAttered (CVE-2017-9006-class).");
665
+ }
666
+ var hashedSubLen = body.readUInt16BE(4);
667
+ if (6 + hashedSubLen > body.length) {
668
+ throw new MailCryptoError("mail-crypto/pgp/bad-packet",
669
+ "hashed-subpackets length overflows packet body");
670
+ }
671
+ var hashedSub = body.slice(6, 6 + hashedSubLen);
672
+ var p = 6 + hashedSubLen;
673
+ if (p + 2 > body.length) {
674
+ throw new MailCryptoError("mail-crypto/pgp/bad-packet",
675
+ "missing unhashed-subpackets length");
676
+ }
677
+ var unhashedSubLen = body.readUInt16BE(p);
678
+ p += 2;
679
+ if (p + unhashedSubLen + 2 > body.length) {
680
+ throw new MailCryptoError("mail-crypto/pgp/bad-packet",
681
+ "unhashed-subpackets length overflows packet body");
682
+ }
683
+ p += unhashedSubLen;
684
+ var hashLeft16 = body.slice(p, p + 2);
685
+ p += 2;
686
+ var sigMpisBytes = body.slice(p);
687
+
688
+ return {
689
+ body: body,
690
+ pubAlg: pubAlg,
691
+ hashAlg: hashAlg,
692
+ hashedSub: hashedSub,
693
+ hashLeft16: hashLeft16,
694
+ sigMpisBytes: sigMpisBytes,
695
+ signedSection: body.slice(0, 6 + hashedSubLen),
696
+ };
697
+ }
698
+
699
+ function _parseSubpackets(subpacketsBuf) {
700
+ var out = {};
701
+ var i = 0;
702
+ while (i < subpacketsBuf.length) {
703
+ var first = subpacketsBuf[i];
704
+ var subLen, hdrLen;
705
+ if (first < 192) { subLen = first; hdrLen = 1; }
706
+ else if (first < 255) {
707
+ if (i + 2 > subpacketsBuf.length) break;
708
+ subLen = ((first - 192) << 8) + subpacketsBuf[i + 1] + 192;
709
+ hdrLen = 2;
710
+ } else {
711
+ if (i + 5 > subpacketsBuf.length) break;
712
+ subLen = subpacketsBuf.readUInt32BE(i + 1);
713
+ hdrLen = 5;
714
+ }
715
+ if (i + hdrLen + subLen > subpacketsBuf.length) break;
716
+ var subType = subpacketsBuf[i + hdrLen] & 0x7f;
717
+ var subBody = subpacketsBuf.slice(i + hdrLen + 1, i + hdrLen + subLen);
718
+ if (subType === SUBPKT_SIG_CREATION_TIME && subBody.length === 4) {
719
+ out.signedAt = subBody.readUInt32BE(0);
720
+ } else if (subType === SUBPKT_ISSUER_FPR && subBody.length === 21) {
721
+ out.issuerFprVersion = subBody[0];
722
+ out.issuerFingerprint = subBody.slice(1).toString("hex");
723
+ }
724
+ i += hdrLen + subLen;
725
+ }
726
+ return out;
727
+ }
728
+
729
+ function _readMpi(buf, offset) {
730
+ if (offset + 2 > buf.length) {
731
+ throw new MailCryptoError("mail-crypto/pgp/bad-mpi",
732
+ "MPI truncated");
733
+ }
734
+ var bits = buf.readUInt16BE(offset);
735
+ var byteLen = Math.ceil(bits / 8);
736
+ if (offset + 2 + byteLen > buf.length) {
737
+ throw new MailCryptoError("mail-crypto/pgp/bad-mpi",
738
+ "MPI value truncated");
739
+ }
740
+ return { value: buf.slice(offset + 2, offset + 2 + byteLen),
741
+ next: offset + 2 + byteLen };
742
+ }
743
+
744
+ /**
745
+ * @primitive b.mail.crypto.pgp.verify
746
+ * @signature b.mail.crypto.pgp.verify(opts)
747
+ * @since 0.9.58
748
+ * @status stable
749
+ * @compliance hipaa, pci-dss, gdpr, soc2
750
+ *
751
+ * Verifies an ASCII-armored OpenPGP detached signature against
752
+ * `opts.message` using `opts.publicKeyPem`. The signature's hash
753
+ * algorithm is enforced against the recomputed digest; SHA-1 is
754
+ * refused. Returns the v4 signer fingerprint (RFC 9580 §5.5.4) so
755
+ * callers can pin to a known operator key rather than trusting any
756
+ * key that happens to verify.
757
+ *
758
+ * @example
759
+ * var rv = b.mail.crypto.pgp.verify({
760
+ * message: bytes,
761
+ * armored: "-----BEGIN PGP SIGNATURE----- ...",
762
+ * publicKeyPem: pubPem,
763
+ * });
764
+ * // → { ok: true, signerFingerprint, signedAt, hashAlg }
765
+ */
766
+ function verify(opts) {
767
+ opts = validateOpts.requireObject(opts, "mail.crypto.pgp.verify", MailCryptoError, "mail-crypto/pgp/bad-opts");
768
+ validateOpts(opts, ["message", "armored", "publicKeyPem", "audit"], "mail.crypto.pgp.verify");
769
+
770
+ var message = opts.message;
771
+ if (!(typeof message === "string" || Buffer.isBuffer(message))) {
772
+ throw new MailCryptoError("mail-crypto/pgp/bad-message",
773
+ "message must be a string or Buffer");
774
+ }
775
+ validateOpts.requireNonEmptyString(opts.armored, "armored",
776
+ MailCryptoError, "mail-crypto/pgp/bad-armor");
777
+ validateOpts.requireNonEmptyString(opts.publicKeyPem, "publicKeyPem",
778
+ MailCryptoError, "mail-crypto/pgp/bad-key");
779
+
780
+ var failReason = null;
781
+ function _fail(code, reason) {
782
+ failReason = { code: code, reason: reason };
783
+ _audit(opts.audit, "mail.crypto.pgp.verify_fail", "failure", {
784
+ reason: reason, code: code,
785
+ });
786
+ return { ok: false, code: code, reason: reason };
787
+ }
788
+
789
+ var packetBytes;
790
+ try { packetBytes = _dearmor(opts.armored); }
791
+ catch (e) { return _fail(e.code || "mail-crypto/pgp/bad-armor", e.message); }
792
+
793
+ var parsed;
794
+ try { parsed = _parseSignaturePacket(packetBytes); }
795
+ catch (e) { return _fail(e.code || "mail-crypto/pgp/bad-packet", e.message); }
796
+
797
+ var subs = _parseSubpackets(parsed.hashedSub);
798
+
799
+ var publicKey;
800
+ try { publicKey = nodeCrypto.createPublicKey({ key: opts.publicKeyPem, format: "pem" }); }
801
+ catch (e) { return _fail("mail-crypto/pgp/bad-key",
802
+ "publicKeyPem could not be parsed: " + ((e && e.message) || String(e))); }
803
+
804
+ var keyType = publicKey.asymmetricKeyType;
805
+ if (parsed.pubAlg === PUB_ALG_RSA && !(keyType === "rsa" || keyType === "rsa-pss")) {
806
+ return _fail("mail-crypto/pgp/key-alg-mismatch",
807
+ "signature claims RSA but provided key is " + keyType);
808
+ }
809
+ if (parsed.pubAlg === PUB_ALG_ED25519_LEGACY && keyType !== "ed25519") {
810
+ return _fail("mail-crypto/pgp/key-alg-mismatch",
811
+ "signature claims Ed25519 but provided key is " + keyType);
812
+ }
813
+ if (parsed.pubAlg !== PUB_ALG_RSA && parsed.pubAlg !== PUB_ALG_ED25519_LEGACY) {
814
+ return _fail("mail-crypto/pgp/bad-pubalg",
815
+ "public-key algorithm " + parsed.pubAlg + " not supported");
816
+ }
817
+
818
+ // Recompute the v4 fingerprint over the provided public key and
819
+ // require equality with the issuer-fingerprint subpacket so the
820
+ // caller can't be tricked into trusting a different key than the
821
+ // one that signed.
822
+ var publicPacketBody;
823
+ if (parsed.pubAlg === PUB_ALG_RSA) {
824
+ var rsaPub = _extractRsaPublicComponents(publicKey);
825
+ if (rsaPub.bits < RSA_MIN_BITS) {
826
+ return _fail("mail-crypto/pgp/rsa-too-small",
827
+ "RSA key is " + rsaPub.bits + " bits; minimum is " + RSA_MIN_BITS +
828
+ " (RFC 8301 §3.1)");
829
+ }
830
+ var creationTimeFromSub = (subs.signedAt === undefined) ? 0 : subs.signedAt;
831
+ publicPacketBody = _rsaPublicPacketBody(rsaPub.n, rsaPub.e, creationTimeFromSub);
832
+ } else {
833
+ var rawPub = _extractEd25519PublicRaw(publicKey);
834
+ var creationTimeFromSubE = (subs.signedAt === undefined) ? 0 : subs.signedAt;
835
+ publicPacketBody = _ed25519PublicPacketBody(rawPub, creationTimeFromSubE);
836
+ }
837
+ var fpr = _v4Fingerprint(publicPacketBody).toString("hex");
838
+ if (subs.issuerFingerprint && subs.issuerFingerprint !== fpr) {
839
+ return _fail("mail-crypto/pgp/fingerprint-mismatch",
840
+ "signature's Issuer Fingerprint (" + subs.issuerFingerprint +
841
+ ") does not match provided public key (" + fpr + ")");
842
+ }
843
+
844
+ var hashName = _hashName(parsed.hashAlg);
845
+ var dataBuf = Buffer.isBuffer(message) ? message : Buffer.from(message, "utf8");
846
+ var trailer = Buffer.concat([
847
+ _u8(4), _u8(0xff), _u32be(parsed.signedSection.length),
848
+ ]);
849
+ var hashInput = Buffer.concat([dataBuf, parsed.signedSection, trailer]);
850
+ var digest = nodeCrypto.createHash(hashName).update(hashInput).digest();
851
+
852
+ // Hash-left-16 fast-fail check.
853
+ if (digest[0] !== parsed.hashLeft16[0] || digest[1] !== parsed.hashLeft16[1]) {
854
+ return _fail("mail-crypto/pgp/hash-mismatch",
855
+ "leading 16 hash bits do not match — wrong key, wrong message, or wrong hash algorithm");
856
+ }
857
+
858
+ var ok;
859
+ if (parsed.pubAlg === PUB_ALG_RSA) {
860
+ var rsaMpi = _readMpi(parsed.sigMpisBytes, 0);
861
+ try {
862
+ ok = nodeCrypto.verify(hashName, hashInput, {
863
+ key: publicKey,
864
+ padding: nodeCrypto.constants.RSA_PKCS1_PADDING,
865
+ }, rsaMpi.value);
866
+ } catch (e) {
867
+ return _fail("mail-crypto/pgp/verify-error",
868
+ "RSA verify threw: " + ((e && e.message) || String(e)));
869
+ }
870
+ } else {
871
+ // Ed25519Legacy — two MPIs (R, S) reassemble into the 64-byte raw
872
+ // EdDSA signature.
873
+ var rMpi = _readMpi(parsed.sigMpisBytes, 0);
874
+ var sMpi = _readMpi(parsed.sigMpisBytes, rMpi.next);
875
+ function _padTo32(buf) {
876
+ if (buf.length === 32) return buf;
877
+ if (buf.length > 32) return buf.slice(buf.length - 32);
878
+ return Buffer.concat([Buffer.alloc(32 - buf.length), buf]);
879
+ }
880
+ var rawSig = Buffer.concat([_padTo32(rMpi.value), _padTo32(sMpi.value)]);
881
+ try {
882
+ ok = nodeCrypto.verify(null, hashInput, publicKey, rawSig);
883
+ } catch (e) {
884
+ return _fail("mail-crypto/pgp/verify-error",
885
+ "Ed25519 verify threw: " + ((e && e.message) || String(e)));
886
+ }
887
+ }
888
+
889
+ if (!ok) {
890
+ return _fail("mail-crypto/pgp/bad-signature",
891
+ "signature did not verify against provided public key");
892
+ }
893
+
894
+ void failReason;
895
+ _audit(opts.audit, "mail.crypto.pgp.verify_pass", "success", {
896
+ signerFingerprint: fpr,
897
+ hashAlg: hashName,
898
+ signedAt: subs.signedAt,
899
+ });
900
+
901
+ return {
902
+ ok: true,
903
+ signerFingerprint: fpr,
904
+ signedAt: subs.signedAt,
905
+ hashAlg: hashName,
906
+ };
907
+ }
908
+
909
+ // ---- Audit (drop-silent — RFC §audit hot-path discipline) ----
910
+
911
+ function _audit(auditHandle, action, outcome, metadata) {
912
+ try {
913
+ var a = auditHandle || audit();
914
+ if (a && typeof a.safeEmit === "function") {
915
+ a.safeEmit({
916
+ action: action,
917
+ outcome: outcome,
918
+ actor: {},
919
+ metadata: metadata,
920
+ });
921
+ }
922
+ } catch (_e) { /* drop-silent — audit failures must not crash callers */ }
923
+ }
924
+
925
+ module.exports = {
926
+ sign: sign,
927
+ verify: verify,
928
+ MailCryptoError: MailCryptoError,
929
+ // Test-only exports — operators don't call these. Stable for v1 so
930
+ // the codebase-patterns gate sees them as named.
931
+ _v4FingerprintForTest: _v4Fingerprint,
932
+ _armorForTest: _armor,
933
+ _dearmorForTest: _dearmor,
934
+ };