Files
mailgoserver/internal/smime/identity.go
T

130 lines
5.3 KiB
Go
Raw Normal View History

// 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")
}