Files
mailgoserver/internal/smime/smime_test.go
T

216 lines
6.6 KiB
Go

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