Mail S/MIME
S/MIME 4.0 (RFC 8551, replacing RFC 5751) multipart/signed; protocol="application/pkcs7-signature" signature verification for inbound mail. CMS SignedData (RFC 5652) carries the actual signature; the signed payload travels in the first MIME part of the multipart/signed wrapper with the SignedData attached to the second part as base64-encoded DER.
Posture (when the surface lights up): - Refuses SHA-1 as the signature hash (SHAttered, 2017 — practical SHA-1 collision; RFC 8551 §2.5 mandates SHA-256+ for S/MIME) and as the certificate signature algorithm. - Refuses RSA keys < 2048 bits (RFC 8301 §3.1 — same posture as the rest of the mail surface). - Refuses MD5 anywhere (the historical S/MIME-v2 default; long broken). - Validates the signer certificate's chain against an operator- supplied trust anchor set; never falls back to a system root store implicitly (the system store binds operator trust to whatever the host happens to ship with). - Refuses certificate algorithms outside the modern set (RSA-PKCS1-v1_5 with SHA-256 / SHA-384 / SHA-512, ECDSA over P-256 / P-384 with SHA-256 / SHA-384, Ed25519). RFC 8551 §2.5 mandates SHA-256 as the MUST-support floor.
Threat model: - EFAIL (CVE-2017-17688 / CVE-2017-17689) — the S/MIME variant attacks decrypt+render pipelines. Same gate as PGP: when encrypt/decrypt lights up, decrypted HTML routes through b.guardHtml strict profile, remote-content fetches in encrypted parts are refused, and the MIME-part tree at decrypt time is compared byte-for-byte against the tree at render time. - PKCS#7 / CMS parser confusion — only the SignedData (ContentType 1.2.840.113549.1.7.2) ContentInfo shape is accepted; degenerate, certs-only-bag, AuthEnvelopedData, and encrypted-content variants are refused at parse time.
v0.10.16 status — LIVE on b.cms substrate:
sign() and verify() ship working on the CMS substrate landed in v0.10.13 + the SignedData walker (b.cms.parseSignedData) landed in v0.10.16. sign() composes b.cms.encodeSignedData + wraps the result in an RFC 8551 multipart/signed envelope. verify() parses the CMS SignedData payload, recomputes the message digest, compares against the signed-attrs messageDigest attribute (refuses tamper), and verifies the PQC signature against the operator-supplied signer public key. Multi-signer envelopes route through verifyAll() which walks every SignerInfo against an operator-supplied key map keyed by serial-number hex.
opts.trustAnchorCertsPem (array of PEM-encoded X.509 trust roots) enables in-call chain validation. Walks leaf → ... → trust anchor, verifies each link's signature against the parent's public key via node:crypto X509Certificate.verify, and checks notBefore/notAfter at the current wall-clock. Refuses with mail-crypto/smime/untrusted-chain when no link reaches a trust anchor; mail-crypto/smime/cert-expired or mail-crypto/smime/cert-not-yet-valid when a chain cert is outside its validity window. Revocation (OCSP / CRL) is not performed inline — operators wire b.network.tls.ocsp against the signer cert when revocation freshness is required.
RFC citations: - RFC 8551 (S/MIME 4.0 Message Specification, April 2019; obsoletes RFC 5751) - RFC 5652 (Cryptographic Message Syntax — CMS) - RFC 8550 (S/MIME 4.0 Certificate Handling) - RFC 5280 (X.509 PKI) - RFC 8301 (RSA bit floor — reused as cross-mail-surface RSA posture)
CVE citations: - CVE-2017-17688 / CVE-2017-17689 (EFAIL — S/MIME variant; informs the encrypt+decrypt deferral when that surface lights up) - SHAttered (2017 practical SHA-1 collision) + RFC 8551 §2.5 (SHA-256 floor for S/MIME) — inform the SHA-1 signature-hash refusal posture - CVE-2018-5407 (PortSmash — informs the side-channel hardening posture when private operations land in v2)
b.mail.crypto.smime.sign(opts) #
{
message: Buffer|string, // message bytes to sign (signed-as-is)
certificate: Buffer, // DER-encoded signer cert
secretKey: Uint8Array, // PQC private key (b.pqcSoftware.ml_dsa_*.keygen())
sigAlg: "ML-DSA-65"|"ML-DSA-87"|"SLH-DSA-SHAKE-256f",
digestAlg: "sha3-256"|"sha3-512", // default sha3-512
boundary: string, // optional; auto-generated if omitted
audit: object, // optional b.audit handle
}
Sign an RFC 5322 message with S/MIME 4.0 (RFC 8551) producing a multipart/signed; protocol="application/pkcs7-signature" wrapper. The CMS SignedData payload is encoded via b.cms.encodeSignedData with PQC signers (ML-DSA-65 / ML-DSA-87 / SLH-DSA-SHAKE-256f). Returns { multipart, signature } where multipart is the wire representation (Content-Type + body) and signature is the raw CMS DER for operators that want to handle the MIME framing themselves.
var kp = b.pqcSoftware.ml_dsa_65.keygen();
var out = b.mail.crypto.smime.sign({
message: "From: x@y\r\nSubject: hi\r\n\r\nbody",
certificate: certDer,
secretKey: kp.secretKey,
sigAlg: "ML-DSA-65",
});
out.multipart; // → "Content-Type: multipart/signed; ..."
b.mail.crypto.smime.verify(opts) #
{
message: Buffer|string, // original signed bytes (use sign().multipart's first part)
signature: Buffer, // raw CMS DER (sign().signature)
signerPublicKey: Uint8Array, // PQC public key of the expected signer
audit: object,
}
Verify an RFC 8551 multipart/signed S/MIME envelope. Parses the CMS SignedData payload, recomputes the message digest, compares against the message-digest signed-attribute, and verifies the signature against the signer's PQC public key. Returns { valid, signerPublicKey, sigAlg, digestAlg } on success; throws on any mismatch.
var ok = b.mail.crypto.smime.verify({
message: msgBytes,
signature: cmsDer,
signerPublicKey: kp.publicKey,
});
ok.valid; // → true
b.mail.crypto.smime.verifyAll(opts) #
{
message: Buffer|string,
signature: Buffer,
signerPublicKeys: { [serialHex]: Uint8Array },
audit: object,
}
Multi-signer verify. The CMS SignedData can carry multiple SignerInfos; this routes each through verify() against the matching key in opts.signerPublicKeys (a map keyed by signer identifier serial-number-hex). Returns { valid, signers: [{ sid, sigAlg, digestAlg }] } where valid is true only when EVERY SignerInfo verified. Refuses with mail-crypto/smime/missing-key when a SignerInfo's sid has no operator-supplied public key.
var v = b.mail.crypto.smime.verifyAll({
message: msg,
signature: cmsDer,
signerPublicKeys: {
"01": signer1Pub,
"02": signer2Pub,
},
});
v.valid; // → true only when every signer verified
v.signers.length; // → 2
b.mail.crypto.smime.checkCert(opts) #
{
certPem:,
}
Operator-side cert preflight that lights up at boot: refuses SHA-1 / MD5 signatures, RSA keys < 2048 bits, MD2 / MD5 / SHA-1 as the certificate-signature algorithm. Returns the parsed cert shape: the full subject / issuer DN strings, the validity window, the signature algorithm (name + OID), the key type, and the SHA-256 fingerprint. Throws mail-crypto/smime/bad-cert on any of the above; throws mail-crypto/smime/expired-cert if the cert is outside its validity window.
var info = b.mail.crypto.smime.checkCert({ certPem: pem });
// → { subject, issuer, validFrom, validTo, sigAlgName, sigAlgOid, keyType, fingerprint256 }
Last updated 2026-08-08T16:39:49.652Z by seeder.