@blamejs/core 0.9.49 → 0.10.2
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 +952 -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 +78 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/cli.js +13 -0
- 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-graphql.js +37 -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-regex.js +138 -1
- package/lib/guard-smtp-command.js +58 -3
- package/lib/guard-xml.js +39 -1
- 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/otel-export.js +13 -4
- 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,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
|
+
};
|