Mail PGP
OpenPGP detached-signature signing + verification for mail per RFC 9580 (the November 2024 OpenPGP revision that obsoletes RFC 4880). Produces multipart/signed; protocol= "application/pgp-signature" per RFC 3156 §5 with a v4 OpenPGP signature packet wrapped in ASCII armor (RFC 9580 §6).
Supported v1 surface (sign + verify): - Ed25519 v4 signatures using OpenPGP public-key algorithm 22 (Ed25519Legacy per RFC 9580 §9.1), the universally-supported Ed25519 form. RFC 9580 also defines algorithm 27 (Ed25519) for v6 signatures; v6 signature output is deferred — see the deferral note below. - RSA v4 signatures using OpenPGP public-key algorithm 1 (RSA) with EMSA-PKCS1-v1_5 padding over SHA-256, which is what every fielded PGP implementation expects for v4 RSA signatures. Keys < 2048 bits are refused at sign time (RFC 8301 §3.1 RSA floor; v0.7.x DKIM established the same posture across the mail surface).
Threat model: - EFAIL (CVE-2017-17688 / CVE-2017-17689) attacks decrypt-and- render flows that (a) fetch remote content in encrypted parts, (b) tolerate MIME-part-structure mutation between decrypt and render, or (c) feed decrypted HTML to a permissive renderer. This v1 surface is sign + verify only, so EFAIL does not bind directly. When encrypt + decrypt lights up (see deferral note) the renderer-side gate is b.guardHtml strict profile, inline image fetches in encrypted parts are refused, and the MIME-part tree captured at decrypt time is compared byte-for- byte against the tree at render time. - SHA-1 collision attacks (SHAttered, 2017) on signature hash inputs — refuse SHA-1 as the signature hash on verify and never emit it from sign. - Hash-algorithm-confusion: the signature's hash_alg field is enforced against the locally-recomputed hash; verifying with a different algorithm than was signed is refused. - Key-fingerprint pinning: verify() returns the v4 fingerprint (RFC 9580 §5.5.4) of the signing key so the caller can pin to a known operator key rather than trusting any key that happens to match the signature.
Now live (promoted to the stable top-level surface in v0.11.32): - In-process encrypt + decrypt (Message Encrypted Session Key + Symmetrically Encrypted Integrity Protected Data packets, RFC 9580 §5.1 / §5.13) as b.mail.crypto.pgp.encrypt / .decrypt, and WKD key discovery (draft-koch-openpgp-webkey- service) as b.mail.crypto.pgp.wkd — all on the same b.cms substrate that backs S/MIME sign/verify.
Deferred (with the documented condition for opting in): - v6 signature packets (RFC 9580 §5.2.3, packet version 6 with SHA2-512 fingerprints and salted hashes). Defer condition: v6 is not yet emitted by GnuPG 2.4 LTS or by Sequoia stable, so v6 output would fail to verify on the majority of fielded receivers. Reopen when at least two major implementations ship v6 signature verification by default. Cheap escape hatch: operators on v6-only systems can ingest the v4 signature from this module and re-sign with their own v6-capable toolchain.
Surface: var sigBundle = b.mail.crypto.pgp.sign({ message: "rfc822 body bytes", privateKeyPem: "-----BEGIN PRIVATE KEY----- ...", passphrase: undefined | "...", // optional audit: opts.audit, // optional b.audit handle }); // → { armored: "-----BEGIN PGP SIGNATURE----- ...", // multipartSigned:
var rv = b.mail.crypto.pgp.verify({ message: "the signed payload bytes", armored: "-----BEGIN PGP SIGNATURE----- ...", publicKeyPem: "-----BEGIN PUBLIC KEY----- ...", audit: opts.audit, }); // → { ok: true, signerFingerprint: "abcd...", signedAt: epoch, hashAlg: "sha256" }
The signer's message MUST be the canonicalized payload that the verifier will recompute over. For multipart/signed per RFC 3156 §5, the canonical form is the signed part's full MIME headers + body with CRLF line endings — operators producing such a body should pass exactly those bytes here.
RFC citations: - RFC 9580 (OpenPGP, Nov 2024; obsoletes RFC 4880) - RFC 3156 (MIME Security with OpenPGP) - RFC 8301 (DKIM RSA floor — reused as the cross-surface RSA bit floor)
CVE citations: - CVE-2017-17688 / CVE-2017-17689 (EFAIL — informs the encrypt/ decrypt deferral conditions above) - CVE-2019-13050 (PGP keyserver flood — not in scope here; out-of- band fingerprint pinning is the operator's responsibility)
b.mail.crypto.pgp.sign(opts) #
{
audit:,
creationTime:,
message:,
passphrase:,
privateKeyPem:,
}
Produces a v4 OpenPGP detached signature over opts.message and returns the ASCII-armored signature plus a ready-to-emit multipart/signed; protocol="application/pgp-signature" body (RFC 3156 §5). Ed25519 (algorithm 22) and RSA-PKCS#1-v1.5 over SHA-256 (algorithm 1) are the v1 signing forms; RSA keys below 2048 bits are refused per RFC 8301 §3.1.
var rv = b.mail.crypto.pgp.sign({
message: "rfc822 body bytes",
privateKeyPem: pem,
});
// → { armored, multipartSigned, signedAt, fingerprint }
b.mail.crypto.pgp.verify(opts) #
{
armored:,
audit:,
message:,
publicKeyPem:,
}
Verifies an ASCII-armored OpenPGP detached signature against opts.message using opts.publicKeyPem. The signature's hash algorithm is enforced against the recomputed digest; SHA-1 is refused. Returns the v4 signer fingerprint (RFC 9580 §5.5.4) so callers can pin to a known operator key rather than trusting any key that happens to verify.
var rv = b.mail.crypto.pgp.verify({
message: bytes,
armored: "-----BEGIN PGP SIGNATURE----- ...",
publicKeyPem: pubPem,
});
// → { ok: true, signerFingerprint, signedAt, hashAlg }
b.mail.crypto.pgp.experimental.wkd.fetch(email, opts) #
{
httpsGet: Function, // (url) → Promise<{ status, body }>; REQUIRED
advancedHost: string, // passed through to computeUrl
maxKeyBytes: number, // default 256 KiB
}
Fetch a WKD key for email per draft-koch-openpgp-webkey-service. Tries the direct URL first; on 404 / network failure falls back to the advanced URL. opts.httpsGet(url) → Promise<{ status, body: Buffer }> is operator-supplied so the framework doesn't couple to a specific HTTP client. Returns { keyBytes, source: "direct" | "advanced", url } or throws mail-crypto/pgp/wkd-not-found when both URLs fail.
var key = await b.mail.crypto.pgp.experimental.wkd.fetch("alice@example.com", {
httpsGet: function (url) {
return b.httpClient.request({ url: url, method: "GET" });
},
});
Last updated 2026-08-08T16:39:49.652Z by seeder.