// Package smime implements S/MIME certificate-based email signing and encryption: // generating or importing a mailbox's own identity (certificate + private key), // signing outbound mail (RFC 8551 multipart/signed, detached CMS SignedData), // verifying a signature, encrypting outbound mail (application/pkcs7-mime, CMS // EnvelopedData), and decrypting it again. // // This package is deliberately certificate-chain-agnostic: it does not validate a // certificate against any CA trust store. A verified signature here means "this // message was cryptographically signed by the private key matching this exact // certificate," not "this certificate is trusted by a PKI" — the same posture this // codebase's own self-signed TLS certificate already has. Callers that want to // display a warning for unrecognized signers should compare the signer's certificate // against their own address book (see the mailbox_smime_contacts table in // internal/db), not chain validation. package smime import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "fmt" "math/big" "time" "go.mozilla.org/pkcs7" pkcs12 "software.sslmate.com/src/go-pkcs12" ) func init() { // go.mozilla.org/pkcs7 defaults ContentEncryptionAlgorithm to legacy DES-CBC for // backward compatibility with old clients — AES-256-GCM is the only acceptable // choice for anything generated here. pkcs7.ContentEncryptionAlgorithm = pkcs7.EncryptionAlgorithmAES256GCM } // DefaultValidity mirrors a typical S/MIME certificate lifetime (1 year), matching // what most CAs issue for individual email certificates. const DefaultValidity = 365 * 24 * time.Hour // GenerateSelfSigned creates a fresh RSA-2048 keypair and a self-signed certificate // scoped to email — same key size internal/tlsutil already uses for the server's own // TLS certificate. KeyUsage/ExtKeyUsage/EmailAddresses are set per RFC 8551 so // mainstream mail clients recognize it as a valid S/MIME certificate. func GenerateSelfSigned(email string, validity time.Duration) (certPEM, keyPEM []byte, err error) { priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, nil, fmt.Errorf("smime: generate key: %w", err) } serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return nil, nil, fmt.Errorf("smime: generate serial: %w", err) } subject := pkix.Name{CommonName: email} template := x509.Certificate{ SerialNumber: serial, Subject: subject, Issuer: subject, NotBefore: time.Now(), NotAfter: time.Now().Add(validity), SignatureAlgorithm: x509.SHA256WithRSA, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection}, EmailAddresses: []string{email}, BasicConstraintsValid: true, } certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { return nil, nil, fmt.Errorf("smime: create certificate: %w", err) } keyDER, err := x509.MarshalPKCS8PrivateKey(priv) if err != nil { return nil, nil, fmt.Errorf("smime: marshal private key: %w", err) } certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) keyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}) return certPEM, keyPEM, nil } // ImportPKCS12 parses a .p12/.pfx bundle — the usual export format from a CA or // another mail client — into a certificate + private key. Only RSA keys are // supported (the only key type go.mozilla.org/pkcs7's encrypt/decrypt operations // actually support for key transport). func ImportPKCS12(data []byte, password string) (certPEM, keyPEM []byte, err error) { priv, cert, err := pkcs12.Decode(data, password) if err != nil { return nil, nil, fmt.Errorf("smime: decode PKCS#12: %w", err) } rsaKey, ok := priv.(*rsa.PrivateKey) if !ok { return nil, nil, errors.New("smime: only RSA keys are supported") } keyDER, err := x509.MarshalPKCS8PrivateKey(rsaKey) if err != nil { return nil, nil, fmt.Errorf("smime: marshal private key: %w", err) } certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) keyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}) return certPEM, keyPEM, nil } // ParseCertPEM decodes a stored certificate back into a usable *x509.Certificate. func ParseCertPEM(certPEM []byte) (*x509.Certificate, error) { block, _ := pem.Decode(certPEM) if block == nil { return nil, errors.New("smime: invalid certificate PEM") } return x509.ParseCertificate(block.Bytes) } // ParseKeyPEM decodes a stored private key back into a usable crypto.PrivateKey. // Tries PKCS#8 first (what GenerateSelfSigned/ImportPKCS12 both produce), falling // back to PKCS#1 for a hand-imported PEM that used the older format. func ParseKeyPEM(keyPEM []byte) (crypto.PrivateKey, error) { block, _ := pem.Decode(keyPEM) if block == nil { return nil, errors.New("smime: invalid private key PEM") } if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil { return key, nil } if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { return key, nil } return nil, errors.New("smime: unrecognized private key format") }