dkim_test.go raw

   1  package smtp
   2  
   3  import (
   4  	"crypto/ed25519"
   5  	"crypto/rand"
   6  	"crypto/rsa"
   7  	"crypto/x509"
   8  	"encoding/pem"
   9  	"os"
  10  	"path/filepath"
  11  	"strings"
  12  	"testing"
  13  )
  14  
  15  func TestDKIMSigner_SignMessage(t *testing.T) {
  16  	// Generate a test key
  17  	key, err := rsa.GenerateKey(rand.Reader, 2048)
  18  	if err != nil {
  19  		t.Fatalf("generate key: %v", err)
  20  	}
  21  
  22  	signer := NewDKIMSignerFromKey("example.com", "test", key)
  23  
  24  	msg := "From: user@example.com\r\nTo: recipient@other.com\r\nSubject: Test\r\nDate: Mon, 01 Jan 2024 00:00:00 +0000\r\nMessage-Id: <test@example.com>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain\r\n\r\nHello world"
  25  
  26  	signed, err := signer.Sign([]byte(msg))
  27  	if err != nil {
  28  		t.Fatalf("Sign: %v", err)
  29  	}
  30  
  31  	// Signed message should contain DKIM-Signature header
  32  	if !strings.Contains(string(signed), "DKIM-Signature") {
  33  		t.Error("signed message missing DKIM-Signature header")
  34  	}
  35  
  36  	// Should still contain original content
  37  	if !strings.Contains(string(signed), "Hello world") {
  38  		t.Error("signed message missing original body")
  39  	}
  40  	if !strings.Contains(string(signed), "Subject: Test") {
  41  		t.Error("signed message missing Subject header")
  42  	}
  43  }
  44  
  45  func TestNewDKIMSigner_FromFile(t *testing.T) {
  46  	dir := t.TempDir()
  47  	keyPath := filepath.Join(dir, "dkim.key")
  48  
  49  	// Generate and save a key
  50  	key, err := rsa.GenerateKey(rand.Reader, 2048)
  51  	if err != nil {
  52  		t.Fatalf("generate key: %v", err)
  53  	}
  54  
  55  	privPEM, _, err := GenerateDKIMKeyPair()
  56  	if err != nil {
  57  		t.Fatalf("GenerateDKIMKeyPair: %v", err)
  58  	}
  59  
  60  	if err := os.WriteFile(keyPath, privPEM, 0600); err != nil {
  61  		t.Fatalf("write key: %v", err)
  62  	}
  63  
  64  	signer, err := NewDKIMSigner("example.com", "marmot", keyPath)
  65  	if err != nil {
  66  		t.Fatalf("NewDKIMSigner: %v", err)
  67  	}
  68  
  69  	// Sign a test message
  70  	msg := "From: user@example.com\r\nTo: recipient@other.com\r\nSubject: File Key Test\r\nDate: Mon, 01 Jan 2024 00:00:00 +0000\r\nMessage-Id: <test2@example.com>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain\r\n\r\nBody"
  71  
  72  	signed, err := signer.Sign([]byte(msg))
  73  	if err != nil {
  74  		t.Fatalf("Sign from file key: %v", err)
  75  	}
  76  
  77  	if !strings.Contains(string(signed), "DKIM-Signature") {
  78  		t.Error("signed message missing DKIM-Signature")
  79  	}
  80  
  81  	_ = key // suppress unused warning
  82  }
  83  
  84  func TestNewDKIMSigner_InvalidFile(t *testing.T) {
  85  	_, err := NewDKIMSigner("example.com", "test", "/nonexistent/path")
  86  	if err == nil {
  87  		t.Error("expected error for nonexistent file")
  88  	}
  89  }
  90  
  91  func TestNewDKIMSigner_InvalidPEM(t *testing.T) {
  92  	dir := t.TempDir()
  93  	path := filepath.Join(dir, "bad.key")
  94  	os.WriteFile(path, []byte("not a PEM file"), 0600)
  95  
  96  	_, err := NewDKIMSigner("example.com", "test", path)
  97  	if err == nil {
  98  		t.Error("expected error for invalid PEM")
  99  	}
 100  }
 101  
 102  func TestGenerateDKIMKeyPair(t *testing.T) {
 103  	privPEM, dns, err := GenerateDKIMKeyPair()
 104  	if err != nil {
 105  		t.Fatalf("GenerateDKIMKeyPair: %v", err)
 106  	}
 107  
 108  	if len(privPEM) == 0 {
 109  		t.Error("private key PEM is empty")
 110  	}
 111  	if !strings.Contains(string(privPEM), "RSA PRIVATE KEY") {
 112  		t.Error("private key PEM missing RSA header")
 113  	}
 114  
 115  	if dns == "" {
 116  		t.Error("DNS record is empty")
 117  	}
 118  	if !strings.HasPrefix(dns, "v=DKIM1; k=rsa; p=") {
 119  		t.Errorf("DNS record has wrong format: %s", dns[:50])
 120  	}
 121  }
 122  
 123  func TestNewDKIMSigner_UnsupportedPEMType(t *testing.T) {
 124  	dir := t.TempDir()
 125  	path := filepath.Join(dir, "bad.key")
 126  	// Write a properly-formed PEM block with an unsupported type
 127  	pemData := pem.EncodeToMemory(&pem.Block{
 128  		Type:  "EC PRIVATE KEY",
 129  		Bytes: []byte("fake key data"),
 130  	})
 131  	os.WriteFile(path, pemData, 0600)
 132  
 133  	_, err := NewDKIMSigner("example.com", "test", path)
 134  	if err == nil {
 135  		t.Error("expected error for unsupported PEM type")
 136  	}
 137  	if !strings.Contains(err.Error(), "unsupported PEM type") {
 138  		t.Errorf("expected unsupported PEM type error, got: %v", err)
 139  	}
 140  }
 141  
 142  func TestNewDKIMSignerFromPEM_InvalidPEM(t *testing.T) {
 143  	_, err := NewDKIMSignerFromPEM("example.com", "test", []byte("not a PEM"))
 144  	if err == nil {
 145  		t.Error("expected error for invalid PEM")
 146  	}
 147  }
 148  
 149  func TestNewDKIMSignerFromPEM_ValidKey(t *testing.T) {
 150  	privPEM, _, err := GenerateDKIMKeyPair()
 151  	if err != nil {
 152  		t.Fatalf("generate: %v", err)
 153  	}
 154  
 155  	signer, err := NewDKIMSignerFromPEM("example.com", "test", privPEM)
 156  	if err != nil {
 157  		t.Fatalf("NewDKIMSignerFromPEM: %v", err)
 158  	}
 159  
 160  	msg := "From: user@example.com\r\nTo: bob@other.com\r\nSubject: Test\r\nDate: Mon, 01 Jan 2024 00:00:00 +0000\r\nMessage-Id: <test@example.com>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain\r\n\r\nBody"
 161  	signed, err := signer.Sign([]byte(msg))
 162  	if err != nil {
 163  		t.Fatalf("Sign: %v", err)
 164  	}
 165  	if !strings.Contains(string(signed), "DKIM-Signature") {
 166  		t.Error("missing DKIM-Signature")
 167  	}
 168  }
 169  
 170  func TestNewDKIMSignerFromPEM_PKCS8Key(t *testing.T) {
 171  	// Generate key and marshal as PKCS8 for NewDKIMSignerFromPEM (different code path)
 172  	key, err := rsa.GenerateKey(rand.Reader, 2048)
 173  	if err != nil {
 174  		t.Fatalf("generate key: %v", err)
 175  	}
 176  
 177  	pkcs8Bytes, err := x509.MarshalPKCS8PrivateKey(key)
 178  	if err != nil {
 179  		t.Fatalf("marshal PKCS8: %v", err)
 180  	}
 181  
 182  	pemBlock := pem.EncodeToMemory(&pem.Block{
 183  		Type:  "PRIVATE KEY",
 184  		Bytes: pkcs8Bytes,
 185  	})
 186  
 187  	signer, err := NewDKIMSignerFromPEM("example.com", "test", pemBlock)
 188  	if err != nil {
 189  		t.Fatalf("NewDKIMSignerFromPEM PKCS8: %v", err)
 190  	}
 191  
 192  	msg := "From: user@example.com\r\nTo: bob@other.com\r\nSubject: PKCS8 FromPEM\r\nDate: Mon, 01 Jan 2024 00:00:00 +0000\r\nMessage-Id: <test@example.com>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain\r\n\r\nBody"
 193  	signed, err := signer.Sign([]byte(msg))
 194  	if err != nil {
 195  		t.Fatalf("Sign: %v", err)
 196  	}
 197  	if !strings.Contains(string(signed), "DKIM-Signature") {
 198  		t.Error("missing DKIM-Signature from PKCS8 key via FromPEM")
 199  	}
 200  }
 201  
 202  func TestNewDKIMSignerFromPEM_NonRSAPKCS8(t *testing.T) {
 203  	// PKCS8 key that isn't RSA — should fail with "not RSA" error
 204  	// We'll use an EC key marshaled as PKCS8
 205  	ecKey, err := rsa.GenerateKey(rand.Reader, 2048) // We need a real PKCS8 block
 206  	if err != nil {
 207  		t.Fatalf("generate: %v", err)
 208  	}
 209  	// Create a valid PKCS8 PEM but with wrong type label to force the PKCS1 parse to fail
 210  	// then PKCS8 parse to succeed but it IS RSA, so we need a different approach.
 211  	// Instead, create a PEM that fails PKCS1 but also fails PKCS8
 212  	_ = ecKey
 213  
 214  	// Construct a PEM block that decodes but fails both PKCS1 and PKCS8 parsing
 215  	badPEM := pem.EncodeToMemory(&pem.Block{
 216  		Type:  "RSA PRIVATE KEY", // PKCS1 type but garbage bytes
 217  		Bytes: []byte("not a valid key structure at all"),
 218  	})
 219  
 220  	_, err = NewDKIMSignerFromPEM("example.com", "test", badPEM)
 221  	if err == nil {
 222  		t.Error("expected error for invalid key bytes")
 223  	}
 224  }
 225  
 226  func TestNewDKIMSigner_PKCS8NonRSA(t *testing.T) {
 227  	// Test the "PKCS8 key is not RSA" error path in NewDKIMSigner (file-based)
 228  	dir := t.TempDir()
 229  	keyPath := filepath.Join(dir, "non-rsa.key")
 230  
 231  	// Create a valid PKCS8 PEM with an Ed25519 key (not RSA)
 232  	_, edKey, err := ed25519Keys()
 233  	if err != nil {
 234  		t.Fatalf("generate ed25519: %v", err)
 235  	}
 236  	pkcs8Bytes, err := x509.MarshalPKCS8PrivateKey(edKey)
 237  	if err != nil {
 238  		t.Fatalf("marshal PKCS8: %v", err)
 239  	}
 240  	pemBlock := pem.EncodeToMemory(&pem.Block{
 241  		Type:  "PRIVATE KEY",
 242  		Bytes: pkcs8Bytes,
 243  	})
 244  	os.WriteFile(keyPath, pemBlock, 0600)
 245  
 246  	_, err = NewDKIMSigner("example.com", "test", keyPath)
 247  	if err == nil {
 248  		t.Error("expected error for non-RSA PKCS8 key")
 249  	}
 250  	if !strings.Contains(err.Error(), "not RSA") {
 251  		t.Errorf("expected 'not RSA' error, got: %v", err)
 252  	}
 253  }
 254  
 255  func ed25519Keys() ([]byte, any, error) {
 256  	seed := make([]byte, 32)
 257  	for i := range seed {
 258  		seed[i] = byte(i)
 259  	}
 260  	key := ed25519.NewKeyFromSeed(seed)
 261  	return seed, key, nil
 262  }
 263  
 264  func TestNewDKIMSigner_CorruptRSAPKCS1(t *testing.T) {
 265  	// RSA PRIVATE KEY block but corrupt bytes — ParsePKCS1 fails
 266  	dir := t.TempDir()
 267  	keyPath := filepath.Join(dir, "corrupt-rsa.key")
 268  	pemBlock := pem.EncodeToMemory(&pem.Block{
 269  		Type:  "RSA PRIVATE KEY",
 270  		Bytes: []byte("corrupt key bytes"),
 271  	})
 272  	os.WriteFile(keyPath, pemBlock, 0600)
 273  
 274  	_, err := NewDKIMSigner("example.com", "test", keyPath)
 275  	if err == nil {
 276  		t.Error("expected error for corrupt RSA PKCS1 key")
 277  	}
 278  	if !strings.Contains(err.Error(), "parse RSA key") {
 279  		t.Errorf("expected 'parse RSA key' error, got: %v", err)
 280  	}
 281  }
 282  
 283  func TestNewDKIMSigner_CorruptPKCS8(t *testing.T) {
 284  	// PRIVATE KEY block but corrupt bytes — ParsePKCS8 fails
 285  	dir := t.TempDir()
 286  	keyPath := filepath.Join(dir, "corrupt-pkcs8.key")
 287  	pemBlock := pem.EncodeToMemory(&pem.Block{
 288  		Type:  "PRIVATE KEY",
 289  		Bytes: []byte("corrupt pkcs8 bytes"),
 290  	})
 291  	os.WriteFile(keyPath, pemBlock, 0600)
 292  
 293  	_, err := NewDKIMSigner("example.com", "test", keyPath)
 294  	if err == nil {
 295  		t.Error("expected error for corrupt PKCS8 key")
 296  	}
 297  	if !strings.Contains(err.Error(), "parse PKCS8 key") {
 298  		t.Errorf("expected 'parse PKCS8 key' error, got: %v", err)
 299  	}
 300  }
 301  
 302  func TestNewDKIMSignerFromPEM_PKCS8NonRSA(t *testing.T) {
 303  	// Valid PKCS8 Ed25519 key — should fail with "not RSA" in FromPEM path
 304  	_, edKey, err := ed25519Keys()
 305  	if err != nil {
 306  		t.Fatalf("generate ed25519: %v", err)
 307  	}
 308  	pkcs8Bytes, err := x509.MarshalPKCS8PrivateKey(edKey)
 309  	if err != nil {
 310  		t.Fatalf("marshal PKCS8: %v", err)
 311  	}
 312  	pemBlock := pem.EncodeToMemory(&pem.Block{
 313  		Type:  "PRIVATE KEY",
 314  		Bytes: pkcs8Bytes,
 315  	})
 316  
 317  	_, err = NewDKIMSignerFromPEM("example.com", "test", pemBlock)
 318  	if err == nil {
 319  		t.Error("expected error for non-RSA PKCS8 key in FromPEM")
 320  	}
 321  	if !strings.Contains(err.Error(), "not RSA") {
 322  		t.Errorf("expected 'not RSA' error, got: %v", err)
 323  	}
 324  }
 325  
 326  func TestNewDKIMSigner_PKCS8Key(t *testing.T) {
 327  	dir := t.TempDir()
 328  	keyPath := filepath.Join(dir, "pkcs8.key")
 329  
 330  	// Generate key and marshal as PKCS8
 331  	key, err := rsa.GenerateKey(rand.Reader, 2048)
 332  	if err != nil {
 333  		t.Fatalf("generate key: %v", err)
 334  	}
 335  
 336  	pkcs8Bytes, err := x509.MarshalPKCS8PrivateKey(key)
 337  	if err != nil {
 338  		t.Fatalf("marshal PKCS8: %v", err)
 339  	}
 340  
 341  	pemBlock := pem.EncodeToMemory(&pem.Block{
 342  		Type:  "PRIVATE KEY",
 343  		Bytes: pkcs8Bytes,
 344  	})
 345  
 346  	if err := os.WriteFile(keyPath, pemBlock, 0600); err != nil {
 347  		t.Fatalf("write: %v", err)
 348  	}
 349  
 350  	signer, err := NewDKIMSigner("example.com", "test", keyPath)
 351  	if err != nil {
 352  		t.Fatalf("NewDKIMSigner PKCS8: %v", err)
 353  	}
 354  
 355  	msg := "From: user@example.com\r\nTo: bob@other.com\r\nSubject: PKCS8\r\nDate: Mon, 01 Jan 2024 00:00:00 +0000\r\nMessage-Id: <test@example.com>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain\r\n\r\nBody"
 356  	signed, err := signer.Sign([]byte(msg))
 357  	if err != nil {
 358  		t.Fatalf("Sign: %v", err)
 359  	}
 360  	if !strings.Contains(string(signed), "DKIM-Signature") {
 361  		t.Error("missing DKIM-Signature from PKCS8 key")
 362  	}
 363  }
 364