MFA fix, added IP blacklist, update webmail client

This commit is contained in:
2026-08-14 13:04:55 +01:00
parent 6063f95504
commit 892f366a16
122 changed files with 13362 additions and 251 deletions
+64
View File
@@ -0,0 +1,64 @@
package smime
import (
"crypto"
"crypto/x509"
"errors"
"fmt"
"mime"
"strings"
"go.mozilla.org/pkcs7"
)
// Encrypt wraps entity's canonical bytes as CMS EnvelopedData addressed to
// recipients, per RFC 8551 application/pkcs7-mime; smime-type=enveloped-data. Pass
// every recipient's certificate, including the sender's own, so a copy kept in Sent
// stays readable.
func Encrypt(entity Entity, recipients []*x509.Certificate) (Entity, error) {
if len(recipients) == 0 {
return Entity{}, errors.New("smime: no recipient certificates provided")
}
envelopedDER, err := pkcs7.Encrypt(entity.bytes(), recipients)
if err != nil {
return Entity{}, fmt.Errorf("smime: encrypt: %w", err)
}
return Entity{
Headers: []string{
`Content-Type: application/pkcs7-mime; smime-type=enveloped-data; name="smime.p7m"`,
"Content-Transfer-Encoding: base64",
`Content-Disposition: attachment; filename="smime.p7m"`,
},
Body: []byte(wrapBase64(envelopedDER)),
}, nil
}
// Decrypt reverses Encrypt, returning the inner MIME entity that was originally
// wrapped.
func Decrypt(entity Entity, cert *x509.Certificate, key crypto.PrivateKey) (Entity, error) {
ct := HeaderValue(entity.Headers, "Content-Type")
mediaType, params, err := mime.ParseMediaType(ct)
if err != nil || mediaType != "application/pkcs7-mime" {
return Entity{}, errors.New("smime: not an application/pkcs7-mime message")
}
if st := params["smime-type"]; st != "" && !strings.EqualFold(st, "enveloped-data") {
return Entity{}, fmt.Errorf("smime: unsupported smime-type %q", st)
}
raw := entity.Body
if isBase64CTE(HeaderValue(entity.Headers, "Content-Transfer-Encoding")) {
if raw, err = decodeBase64(entity.Body); err != nil {
return Entity{}, fmt.Errorf("smime: decode envelope: %w", err)
}
}
p7, err := pkcs7.Parse(raw)
if err != nil {
return Entity{}, fmt.Errorf("smime: parse envelope: %w", err)
}
plaintext, err := p7.Decrypt(cert, key)
if err != nil {
return Entity{}, fmt.Errorf("smime: decrypt: %w", err)
}
return parseEntity(plaintext)
}
+151
View File
@@ -0,0 +1,151 @@
package smime
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"strings"
)
// Entity is a MIME entity: its own part-level headers (Content-Type,
// Content-Transfer-Encoding, Content-Disposition — never envelope headers like
// From/To/Subject/Date) plus its body. Sign/Encrypt/Decrypt/VerifySigned all operate
// on an Entity, not a flat raw RFC822 message — the caller (webui's compose/read
// handlers) is responsible for keeping envelope headers separate, since S/MIME only
// ever transforms the message body's own MIME entity, never the envelope.
type Entity struct {
Headers []string
Body []byte
}
// bytes renders the entity as it would appear on the wire: headers, a blank line,
// then the body normalized to CRLF line endings — MIME's canonical form, which is
// what gets hashed/signed/encrypted. Both Sign and Encrypt must operate on exactly
// this rendering so a receiving client's own canonicalization matches ours.
func (e Entity) bytes() []byte {
var buf bytes.Buffer
for _, h := range e.Headers {
buf.WriteString(h)
buf.WriteString("\r\n")
}
buf.WriteString("\r\n")
buf.Write(toCRLF(e.Body))
return buf.Bytes()
}
// parseEntity splits raw bytes (headers, a blank line, then body) back into an
// Entity — used to recover the inner MIME entity after Decrypt or the signed part
// after VerifySigned, both of which hand back a full "headers+body" byte blob.
func parseEntity(raw []byte) (Entity, error) {
idx := bytes.Index(raw, []byte("\r\n\r\n"))
sep := 4
if idx < 0 {
idx = bytes.Index(raw, []byte("\n\n"))
sep = 2
}
if idx < 0 {
return Entity{Body: raw}, nil
}
var headers []string
for _, line := range strings.Split(string(raw[:idx]), "\n") {
line = strings.TrimRight(line, "\r")
if line == "" {
continue
}
headers = append(headers, line)
}
return Entity{Headers: headers, Body: raw[idx+sep:]}, nil
}
// toCRLF normalizes line endings to CRLF — first collapsing any existing CRLF to a
// bare LF so a mixed or already-CRLF input doesn't end up double-terminated.
func toCRLF(b []byte) []byte {
b = bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n"))
return bytes.ReplaceAll(b, []byte("\n"), []byte("\r\n"))
}
// HeaderValue is a case-insensitive lookup over a MIME entity's header lines —
// exported so callers outside this package (e.g. webui's read-integration, which
// must inspect a message's Content-Type before deciding whether to unwrap it) don't
// need to reimplement it.
func HeaderValue(headers []string, name string) string {
for _, h := range headers {
idx := strings.Index(h, ":")
if idx < 0 {
continue
}
if strings.EqualFold(strings.TrimSpace(h[:idx]), name) {
return strings.TrimSpace(h[idx+1:])
}
}
return ""
}
func isBase64CTE(cte string) bool {
return strings.EqualFold(strings.TrimSpace(cte), "base64")
}
func decodeBase64(data []byte) ([]byte, error) {
return base64.StdEncoding.DecodeString(stripWhitespace(string(data)))
}
func stripWhitespace(s string) string {
var b strings.Builder
for _, r := range s {
switch r {
case ' ', '\t', '\r', '\n':
continue
default:
b.WriteRune(r)
}
}
return b.String()
}
func newBoundary() string {
b := make([]byte, 16)
rand.Read(b)
return "----=_SMIME_" + hex.EncodeToString(b)
}
// wrapBase64 base64-encodes data at the RFC 2045-recommended 76 characters per line —
// cosmetic (a decoder doesn't care), but matches what every real MTA/MUA produces.
func wrapBase64(data []byte) string {
encoded := base64.StdEncoding.EncodeToString(data)
var b strings.Builder
for i := 0; i < len(encoded); i += 76 {
end := min(i+76, len(encoded))
b.WriteString(encoded[i:end])
b.WriteString("\r\n")
}
return strings.TrimRight(b.String(), "\r\n")
}
// splitMultipartRaw extracts each part's *exact* original bytes between boundary
// delimiters — deliberately not using mime/multipart.Reader, whose Part API parses
// headers away from the raw body and would require re-serializing them to recover
// signable bytes. A detached S/MIME signature covers the literal octets of the
// signed part (RFC 8551 §3.4.3), so reconstruction-from-parsed-headers risks a
// byte-for-byte mismatch (header order, casing, whitespace) that breaks verification
// even for semantically-identical content. Real S/MIME implementations extract raw
// byte ranges for exactly this reason.
func splitMultipartRaw(body []byte, boundary string) ([][]byte, error) {
delim := []byte("--" + boundary)
segments := bytes.Split(body, delim)
if len(segments) < 3 {
return nil, errors.New("smime: malformed multipart body")
}
// segments[0] is the preamble (ignored); the last segment starts with "--" (the
// closing delimiter) and anything after is the epilogue (ignored). Everything in
// between is one part, each still wrapped in the CRLF that separated it from its
// boundary line.
parts := make([][]byte, 0, len(segments)-2)
for _, seg := range segments[1 : len(segments)-1] {
seg = bytes.TrimPrefix(seg, []byte("\r\n"))
seg = bytes.TrimSuffix(seg, []byte("\r\n"))
parts = append(parts, seg)
}
return parts, nil
}
+129
View File
@@ -0,0 +1,129 @@
// 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")
}
+99
View File
@@ -0,0 +1,99 @@
package smime
import (
"crypto"
"crypto/x509"
"errors"
"fmt"
"mime"
"go.mozilla.org/pkcs7"
)
// Sign wraps entity in RFC 8551 multipart/signed: part 1 is the entity's own
// canonical bytes (unmodified — this is what the signature covers), part 2 is a
// detached CMS SignedData over those same bytes.
func Sign(entity Entity, cert *x509.Certificate, key crypto.PrivateKey) (Entity, error) {
content := entity.bytes()
sd, err := pkcs7.NewSignedData(content)
if err != nil {
return Entity{}, fmt.Errorf("smime: sign: %w", err)
}
sd.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
if err := sd.AddSigner(cert, key, pkcs7.SignerInfoConfig{}); err != nil {
return Entity{}, fmt.Errorf("smime: sign: %w", err)
}
sd.Detach()
sigDER, err := sd.Finish()
if err != nil {
return Entity{}, fmt.Errorf("smime: sign: %w", err)
}
boundary := newBoundary()
body := make([]byte, 0, len(content)+len(sigDER)*2)
body = append(body, []byte("--"+boundary+"\r\n")...)
body = append(body, content...)
body = append(body, []byte("\r\n--"+boundary+"\r\n")...)
body = append(body, []byte("Content-Type: application/pkcs7-signature; name=\"smime.p7s\"\r\n")...)
body = append(body, []byte("Content-Transfer-Encoding: base64\r\n")...)
body = append(body, []byte("Content-Disposition: attachment; filename=\"smime.p7s\"\r\n\r\n")...)
body = append(body, []byte(wrapBase64(sigDER))...)
body = append(body, []byte("\r\n--"+boundary+"--\r\n")...)
return Entity{
Headers: []string{
fmt.Sprintf(`Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="%s"`, boundary),
},
Body: body,
}, nil
}
// VerifySigned parses a multipart/signed entity produced by Sign (or any RFC
// 8551-compliant sender), checks the detached signature against the exact original
// bytes of part 1, and returns that inner entity plus the signer's certificate. On a
// signature mismatch it still returns the inner entity — so a tampered or
// unverifiable message can be shown with a warning rather than hidden — alongside a
// non-nil error and a nil signer.
func VerifySigned(entity Entity) (inner Entity, signer *x509.Certificate, err error) {
ct := HeaderValue(entity.Headers, "Content-Type")
mediaType, params, err := mime.ParseMediaType(ct)
if err != nil || mediaType != "multipart/signed" {
return Entity{}, nil, errors.New("smime: not a multipart/signed message")
}
boundary := params["boundary"]
if boundary == "" {
return Entity{}, nil, errors.New("smime: missing multipart boundary")
}
parts, err := splitMultipartRaw(entity.Body, boundary)
if err != nil || len(parts) < 2 {
return Entity{}, nil, errors.New("smime: malformed signed message")
}
signedContent := parts[0]
sigPart, err := parseEntity(parts[1])
if err != nil {
return Entity{}, nil, err
}
sigDER := sigPart.Body
if isBase64CTE(HeaderValue(sigPart.Headers, "Content-Transfer-Encoding")) {
if sigDER, err = decodeBase64(sigPart.Body); err != nil {
return Entity{}, nil, fmt.Errorf("smime: decode signature: %w", err)
}
}
p7, err := pkcs7.Parse(sigDER)
if err != nil {
return Entity{}, nil, fmt.Errorf("smime: parse signature: %w", err)
}
p7.Content = signedContent
inner, perr := parseEntity(signedContent)
if perr != nil {
return Entity{}, nil, perr
}
if err := p7.Verify(); err != nil {
return inner, nil, fmt.Errorf("smime: signature verification failed: %w", err)
}
return inner, p7.GetOnlySigner(), nil
}
+215
View File
@@ -0,0 +1,215 @@
package smime
import (
"bytes"
"crypto"
"crypto/x509"
"strings"
"testing"
)
func testIdentity(t *testing.T, email string) (*x509.Certificate, crypto.PrivateKey) {
t.Helper()
certPEM, keyPEM, err := GenerateSelfSigned(email, DefaultValidity)
if err != nil {
t.Fatalf("GenerateSelfSigned: %v", err)
}
cert, err := ParseCertPEM(certPEM)
if err != nil {
t.Fatalf("ParseCertPEM: %v", err)
}
key, err := ParseKeyPEM(keyPEM)
if err != nil {
t.Fatalf("ParseKeyPEM: %v", err)
}
return cert, key
}
// testEntity uses CRLF line endings already, since Sign/Encrypt canonicalize the
// body to CRLF (MIME's wire form) before signing/encrypting — a round trip through
// either normalizes bare LF to CRLF, so tests compare against the canonical form.
func testEntity() Entity {
return Entity{
Headers: []string{"Content-Type: text/plain; charset=utf-8"},
Body: []byte("hello world\r\nsecond line\r\n"),
}
}
func TestSignVerifyRoundTrip(t *testing.T) {
cert, key := testIdentity(t, "alice@example.com")
orig := testEntity()
signed, err := Sign(orig, cert, key)
if err != nil {
t.Fatalf("Sign: %v", err)
}
if ct := HeaderValue(signed.Headers, "Content-Type"); !strings.HasPrefix(ct, "multipart/signed") {
t.Fatalf("unexpected Content-Type: %q", ct)
}
inner, signer, err := VerifySigned(signed)
if err != nil {
t.Fatalf("VerifySigned: %v", err)
}
if signer == nil || signer.Subject.CommonName != "alice@example.com" {
t.Fatalf("unexpected signer: %+v", signer)
}
if !bytes.Equal(inner.Body, orig.Body) {
t.Fatalf("body mismatch: got %q want %q", inner.Body, orig.Body)
}
if HeaderValue(inner.Headers, "Content-Type") != HeaderValue(orig.Headers, "Content-Type") {
t.Fatalf("header mismatch: got %v want %v", inner.Headers, orig.Headers)
}
}
func TestVerifySignedDetectsTampering(t *testing.T) {
cert, key := testIdentity(t, "alice@example.com")
signed, err := Sign(testEntity(), cert, key)
if err != nil {
t.Fatalf("Sign: %v", err)
}
tampered := string(signed.Body)
tampered = strings.Replace(tampered, "hello world", "hello WORLD", 1)
signed.Body = []byte(tampered)
inner, signer, err := VerifySigned(signed)
if err == nil {
t.Fatal("expected verification error for tampered content, got nil")
}
if signer != nil {
t.Fatalf("expected nil signer on failed verification, got %+v", signer)
}
// The tampered body should still come back for display purposes even though
// verification failed.
if !bytes.Contains(inner.Body, []byte("hello WORLD")) {
t.Fatalf("expected tampered body returned alongside the error, got %q", inner.Body)
}
}
func TestVerifySignedWrongSignerCert(t *testing.T) {
cert, key := testIdentity(t, "alice@example.com")
other, _ := testIdentity(t, "mallory@example.com")
signed, err := Sign(testEntity(), cert, key)
if err != nil {
t.Fatalf("Sign: %v", err)
}
_, signer, err := VerifySigned(signed)
if err != nil {
t.Fatalf("VerifySigned: %v", err)
}
if signer.Subject.CommonName == other.Subject.CommonName {
t.Fatal("signer should not match an unrelated certificate")
}
}
func TestEncryptDecryptRoundTrip(t *testing.T) {
cert, key := testIdentity(t, "bob@example.com")
orig := testEntity()
encrypted, err := Encrypt(orig, []*x509.Certificate{cert})
if err != nil {
t.Fatalf("Encrypt: %v", err)
}
if ct := HeaderValue(encrypted.Headers, "Content-Type"); !strings.HasPrefix(ct, "application/pkcs7-mime") {
t.Fatalf("unexpected Content-Type: %q", ct)
}
if bytes.Contains(encrypted.Body, orig.Body) {
t.Fatal("encrypted body should not contain the plaintext")
}
decrypted, err := Decrypt(encrypted, cert, key)
if err != nil {
t.Fatalf("Decrypt: %v", err)
}
if !bytes.Equal(decrypted.Body, orig.Body) {
t.Fatalf("body mismatch: got %q want %q", decrypted.Body, orig.Body)
}
if HeaderValue(decrypted.Headers, "Content-Type") != HeaderValue(orig.Headers, "Content-Type") {
t.Fatalf("header mismatch: got %v want %v", decrypted.Headers, orig.Headers)
}
}
func TestDecryptWrongKeyFails(t *testing.T) {
cert, _ := testIdentity(t, "bob@example.com")
otherCert, otherKey := testIdentity(t, "mallory@example.com")
encrypted, err := Encrypt(testEntity(), []*x509.Certificate{cert})
if err != nil {
t.Fatalf("Encrypt: %v", err)
}
if _, err := Decrypt(encrypted, otherCert, otherKey); err == nil {
t.Fatal("expected decryption with the wrong key to fail")
}
}
func TestEncryptMultipleRecipientsBothCanDecrypt(t *testing.T) {
senderCert, senderKey := testIdentity(t, "alice@example.com")
recipCert, recipKey := testIdentity(t, "bob@example.com")
orig := testEntity()
encrypted, err := Encrypt(orig, []*x509.Certificate{senderCert, recipCert})
if err != nil {
t.Fatalf("Encrypt: %v", err)
}
senderCopy, err := Decrypt(encrypted, senderCert, senderKey)
if err != nil {
t.Fatalf("sender Decrypt: %v", err)
}
if !bytes.Equal(senderCopy.Body, orig.Body) {
t.Fatal("sender's own copy did not decrypt to the original body")
}
recipCopy, err := Decrypt(encrypted, recipCert, recipKey)
if err != nil {
t.Fatalf("recipient Decrypt: %v", err)
}
if !bytes.Equal(recipCopy.Body, orig.Body) {
t.Fatal("recipient's copy did not decrypt to the original body")
}
}
// TestSignThenEncryptNestedRoundTrip covers the "sign and encrypt" compose option:
// the plaintext is signed, then the whole signed entity is encrypted (opaque
// nesting), matching how webui's compose handler applies both transforms together.
func TestSignThenEncryptNestedRoundTrip(t *testing.T) {
senderCert, senderKey := testIdentity(t, "alice@example.com")
recipCert, recipKey := testIdentity(t, "bob@example.com")
orig := testEntity()
signed, err := Sign(orig, senderCert, senderKey)
if err != nil {
t.Fatalf("Sign: %v", err)
}
encrypted, err := Encrypt(signed, []*x509.Certificate{recipCert})
if err != nil {
t.Fatalf("Encrypt: %v", err)
}
decrypted, err := Decrypt(encrypted, recipCert, recipKey)
if err != nil {
t.Fatalf("Decrypt: %v", err)
}
if !strings.HasPrefix(HeaderValue(decrypted.Headers, "Content-Type"), "multipart/signed") {
t.Fatalf("expected the decrypted layer to still be multipart/signed, got %q", HeaderValue(decrypted.Headers, "Content-Type"))
}
inner, signer, err := VerifySigned(decrypted)
if err != nil {
t.Fatalf("VerifySigned: %v", err)
}
if signer.Subject.CommonName != "alice@example.com" {
t.Fatalf("unexpected signer: %+v", signer)
}
if !bytes.Equal(inner.Body, orig.Body) {
t.Fatalf("body mismatch after unwrapping both layers: got %q want %q", inner.Body, orig.Body)
}
}
func TestImportPKCS12RejectsBadPassword(t *testing.T) {
if _, _, err := ImportPKCS12([]byte("not a real pkcs12 file"), "whatever"); err == nil {
t.Fatal("expected an error decoding garbage PKCS#12 data")
}
}