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