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) #

stable0.10.16hipaapci-dssgdprsoc2
{
  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) #

stable0.10.16hipaapci-dssgdprsoc2
{
  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) #

stable0.10.16hipaapci-dssgdprsoc2
{
  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) #

stable0.9.58hipaapci-dssgdprsoc2
{
  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.