1package dkim
2
3import (
4 "crypto/x509"
5 "encoding/base64"
6 "errors"
7 "reflect"
8 "testing"
9)
10
11func TestParseRecord(t *testing.T) {
12 test := func(txt string, expRec *Record, expIsDKIM bool, expErr error) {
13 t.Helper()
14
15 isParseErr := func(err error) bool {
16 _, ok := err.(parseErr)
17 return ok
18 }
19
20 r, isdkim, err := ParseRecord(txt)
21 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) && !(isParseErr(err) && isParseErr(expErr)) {
22 t.Fatalf("parsing record: got error %v %#v, expected %#v, txt %q", err, err, expErr, txt)
23 }
24 if isdkim != expIsDKIM {
25 t.Fatalf("got isdkim %v, expected %v", isdkim, expIsDKIM)
26 }
27 if r != nil && expRec != nil {
28 expRec.PublicKey = r.PublicKey
29 }
30 if !reflect.DeepEqual(r, expRec) {
31 t.Fatalf("got record %#v, expected %#v, for txt %q", r, expRec, txt)
32 }
33 if r != nil {
34 pk := r.Pubkey
35 for i := 0; i < 2; i++ {
36 ntxt, err := r.Record()
37 if err != nil {
38 t.Fatalf("making record: %v", err)
39 }
40 nr, _, _ := ParseRecord(ntxt)
41 r.Pubkey = pk
42 if !reflect.DeepEqual(r, nr) {
43 t.Fatalf("after packing and parsing, got %#v, expected %#v", nr, r)
44 }
45
46 // Generate again, now based on parsed public key.
47 pk = r.Pubkey
48 r.Pubkey = nil
49 }
50 }
51 }
52
53 xbase64 := func(s string) []byte {
54 t.Helper()
55 buf, err := base64.StdEncoding.DecodeString(s)
56 if err != nil {
57 t.Fatalf("parsing base64: %v", err)
58 }
59 return buf
60 }
61
62 test("", nil, false, parseErr(""))
63 test("v=DKIM1", nil, true, errRecordMissingField) // Missing p=.
64 test("p=; v=DKIM1", nil, true, errRecordVersionFirst)
65 test("v=DKIM1; p=; ", nil, true, parseErr("")) // Whitespace after last ; is not allowed.
66 test("v=dkim1; p=; ", nil, false, parseErr("")) // dkim1-value is case-sensitive.
67 test("v=DKIM1; p=JDcbZ0Hpba5NKXI4UAW3G0IDhhFOxhJTDybZEwe1FeA=", nil, true, errRecordBadPublicKey) // Not an rsa key.
68 test("v=DKIM1; p=; p=", nil, true, errRecordDuplicateTag) // Duplicate tag.
69 test("v=DKIM1; k=ed25519; p=HbawiMnQXTCopHTkR0jlKQ==", nil, true, errRecordBadPublicKey) // Short key.
70 test("v=DKIM1; k=unknown; p=", nil, true, errRecordUnknownAlgorithm)
71
72 empty := &Record{
73 Version: "DKIM1",
74 Key: "rsa",
75 Services: []string{"*"},
76 Pubkey: []uint8{},
77 }
78 test("V=DKIM2; p=;", empty, true, nil) // Tag names are case-sensitive.
79
80 record := &Record{
81 Version: "DKIM1",
82 Hashes: []string{"sha1", "SHA256", "unknown"},
83 Key: "ed25519",
84 Notes: "notes...",
85 Pubkey: xbase64("JDcbZ0Hpba5NKXI4UAW3G0IDhhFOxhJTDybZEwe1FeA="),
86 Services: []string{"email", "tlsrpt"},
87 Flags: []string{"y", "t"},
88 }
89 test("v = DKIM1 ; h\t=\tsha1 \t:\t SHA256:unknown\t;k=ed25519; n = notes...; p = JDc bZ0Hpb a5NK\tXI4UAW3G0IDhhFOxhJTDybZEwe1FeA= ;s = email : tlsrpt; t = y\t: t; unknown = bogus;", record, true, nil)
90
91 edpkix, err := x509.MarshalPKIXPublicKey(record.PublicKey)
92 if err != nil {
93 t.Fatalf("marshal ed25519 public key")
94 }
95 recordx := &Record{
96 Version: "DKIM1",
97 Key: "rsa",
98 Pubkey: edpkix,
99 }
100 txtx, err := recordx.Record()
101 if err != nil {
102 t.Fatalf("making record: %v", err)
103 }
104 test(txtx, nil, true, errRecordBadPublicKey)
105
106 record2 := &Record{
107 Version: "DKIM1",
108 Key: "rsa",
109 Services: []string{"*"},
110 Pubkey: xbase64("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy3Z9ffZe8gUTJrdGuKj6IwEembmKYpp0jMa8uhudErcI4gFVUaFiiRWxc4jP/XR9NAEv3XwHm+CVcHu+L/n6VWt6g59U7vHXQicMfKGmEp2VplsgojNy/Y5X9HdVYM0azsI47NcJCDW9UVfeOHdOSgFME4F8dNtUKC4KTB2d1pqj/yixz+V8Sv8xkEyPfSRHcNXIw0LvelqJ1MRfN3hO/3uQSVrPYYk4SyV0b6wfnkQs28fpiIpGQvzlGI5WkrdOQT5k4YHaEvZDLNdwiMeVZOEL7dDoFs2mQsovm+tH0StUAZTnr61NLVFfD5V6Ip1V9zVtspPHvYSuOWwyArFZ9QIDAQAB"),
111 }
112 test("v=DKIM1;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy3Z9ffZe8gUTJrdGuKj6IwEembmKYpp0jMa8uhudErcI4gFVUaFiiRWxc4jP/XR9NAEv3XwHm+CVcHu+L/n6VWt6g59U7vHXQicMfKGmEp2VplsgojNy/Y5X9HdVYM0azsI47NcJCDW9UVfeOHdOSgFME4F8dNtUKC4KTB2d1pqj/yixz+V8Sv8xkEyPfSRHcNXIw0LvelqJ1MRfN3hO/3uQSVrPYYk4SyV0b6wfnkQs28fpiIpGQvzlGI5WkrdOQT5k4YHaEvZDLNdwiMeVZOEL7dDoFs2mQsovm+tH0StUAZTnr61NLVFfD5V6Ip1V9zVtspPHvYSuOWwyArFZ9QIDAQAB", record2, true, nil)
113
114}
115
116func TestQPSection(t *testing.T) {
117 var tests = []struct {
118 input string
119 expect string
120 }{
121 {"test", "test"},
122 {"hi=", "hi=3D"},
123 {"hi there", "hi there"},
124 {" hi", "=20hi"},
125 {"t\x7f", "t=7F"},
126 }
127 for _, v := range tests {
128 r := qpSection(v.input)
129 if r != v.expect {
130 t.Fatalf("qpSection: input %q, expected %q, got %q", v.input, v.expect, r)
131 }
132 }
133}
134