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 (b.mail.dkim.RSA_MIN_BITS, the framework RSA floor, following the RFC 8301 §3.2 recommendation across 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 §3.2 (DKIM RSA key sizes; the framework RSA floor follows its 2048-bit recommendation)

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
  trustAnchorCertsPem: string[],          // PEM X.509 roots; omitted or empty verifies the signature and leaves the chain unchecked
  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 },
  trustAnchorCertsPem: string[],                   // PEM X.509 roots, passed through to each per-signer verify
  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.

mail-crypto/smime/bad-opts refuses the call shape: opts that is not an object, a signerPublicKeys that is not a map, a message that is neither a Buffer nor a string, a signature that is not a Buffer. A SignedData that carries no SignerInfo refuses with mail-crypto/smime/no-signers.

Each signer is verified by the same path b.mail.crypto.smime.verify uses, so every refusal that primitive makes for one signer refuses the envelope here: mail-crypto/smime/parse-failed for a CMS structure it cannot read, mail-crypto/smime/bad-sig-alg and mail-crypto/smime/bad-digest for an algorithm it does not accept, mail-crypto/smime/no-signed-attrs, mail-crypto/smime/no-content-type-attr, mail-crypto/smime/no-message-digest-attr, mail-crypto/smime/content-type-mismatch and mail-crypto/smime/message-digest-mismatch for signed attributes that are absent or disagree with the content, and mail-crypto/smime/signature-mismatch or mail-crypto/smime/verify-failed when the signature does not verify.

With trustAnchorCertsPem the chain walk adds its own, per signer: mail-crypto/smime/no-certs when the envelope carries no certificates, mail-crypto/smime/signer-not-in-chain when none carries the verifying key, mail-crypto/smime/unbindable-sid and mail-crypto/smime/signer-cert-not-named when the SignerInfo cannot be bound to the certificate it names, mail-crypto/smime/cert-expired and mail-crypto/smime/cert-not-yet-valid for a certificate outside its validity window, mail-crypto/smime/chain-pathlen-exceeded and mail-crypto/smime/name-constraint-violation for a chain the constraints refuse, and mail-crypto/smime/untrusted-chain when it reaches no supplied anchor. The inputs to that walk are checked first: mail-crypto/smime/bad-chain-cert for a certificate in the envelope that does not parse, and mail-crypto/smime/bad-trust-anchor for a trustAnchorCertsPem entry that is not a string or does not parse as a certificate.

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:          string,               // required. One PEM-encoded X.509 certificate; a non-string or empty value throws
}

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.

Each refusal carries its own code. A SHA-1 or MD5 certificate signature throws mail-crypto/smime/refused-hash, and an RSA key under the 2048-bit floor mail-crypto/smime/rsa-too-small. mail-crypto/smime/bad-cert covers a certPem that is not a non-empty string or that does not parse as X.509, and mail-crypto/smime/bad-opts an opts that is not an object. A cert whose validity dates do not parse throws mail-crypto/smime/bad-validity; one outside its validity window, either not yet valid or expired, throws mail-crypto/smime/expired-cert.

var info = b.mail.crypto.smime.checkCert({ certPem: pem });
// → { subject, issuer, validFrom, validTo, sigAlgName, sigAlgOid, keyType, fingerprint256 }

Last updated 2026-10-12T14:21:56.879Z by seeder.