216 lines
6.6 KiB
Go
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")
|
|
}
|
|
}
|