1package dkim
2
3import (
4 "encoding/base64"
5 "errors"
6 "reflect"
7 "strings"
8 "testing"
9
10 "github.com/mjl-/mox/dns"
11 "github.com/mjl-/mox/smtp"
12)
13
14func TestSig(t *testing.T) {
15 test := func(s string, smtputf8 bool, expSig *Sig, expErr error) {
16 t.Helper()
17
18 isParseErr := func(err error) bool {
19 _, ok := err.(parseErr)
20 return ok
21 }
22
23 sig, _, err := parseSignature([]byte(s), smtputf8)
24 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) && !(isParseErr(err) && isParseErr(expErr)) {
25 t.Fatalf("got err %v, expected %v", err, expErr)
26 }
27 if !reflect.DeepEqual(sig, expSig) {
28 t.Fatalf("got sig %#v, expected %#v", sig, expSig)
29 }
30
31 if sig == nil {
32 return
33 }
34 h, err := sig.Header()
35 if err != nil {
36 t.Fatalf("making signature header: %v", err)
37 }
38 nsig, _, err := parseSignature([]byte(h), smtputf8)
39 if err != nil {
40 t.Fatalf("parse signature again: %v", err)
41 }
42 if !reflect.DeepEqual(nsig, sig) {
43 t.Fatalf("parsed signature again, got %#v, expected %#v", nsig, sig)
44 }
45 }
46
47 xbase64 := func(s string) []byte {
48 t.Helper()
49 buf, err := base64.StdEncoding.DecodeString(s)
50 if err != nil {
51 t.Fatalf("parsing base64: %v", err)
52 }
53 return buf
54 }
55
56 xdomain := func(s string) dns.Domain {
57 t.Helper()
58 d, err := dns.ParseDomain(s)
59 if err != nil {
60 t.Fatalf("parsing domain: %v", err)
61 }
62 return d
63 }
64
65 var empty smtp.Localpart
66 sig1 := &Sig{
67 Version: 1,
68 AlgorithmSign: "ed25519",
69 AlgorithmHash: "sha256",
70 Signature: xbase64("dGVzdAo="),
71 BodyHash: xbase64("LjkN2rUhrS3zKXfH2vNgUzz5ERRJkgP9CURXBX0JP0Q="),
72 Domain: xdomain("mox.example"),
73 SignedHeaders: []string{"from", "to", "cc", "bcc", "date", "subject", "message-id"},
74 Selector: xdomain("test"),
75 Canonicalization: "simple/relaxed",
76 Length: 10,
77 Identity: &Identity{&empty, xdomain("sub.mox.example")},
78 QueryMethods: []string{"dns/txt", "other"},
79 SignTime: 10,
80 ExpireTime: 100,
81 CopiedHeaders: []string{"From:<mjl@mox.example>", "Subject:test | with pipe"},
82 }
83 test("dkim-signature: v = 1 ; a=ed25519-sha256; s=test; d=mox.example; h=from:to:cc:bcc:date:subject:message-id; b=dGVzdAo=; bh=LjkN2rUhrS3zKXfH2vNgUzz5ERRJkgP9CURXBX0JP0Q= ; c=simple/relaxed; l=10; i=\"\"@sub.mox.example; q= dns/txt:other; t=10; x=100; z=From:<mjl@mox.example>|Subject:test=20=7C=20with=20pipe; unknown = must be ignored \r\n", true, sig1, nil)
84
85 ulp := smtp.Localpart("møx")
86 sig2 := &Sig{
87 Version: 1,
88 AlgorithmSign: "ed25519",
89 AlgorithmHash: "sha256",
90 Signature: xbase64("dGVzdAo="),
91 BodyHash: xbase64("LjkN2rUhrS3zKXfH2vNgUzz5ERRJkgP9CURXBX0JP0Q="),
92 Domain: xdomain("xn--mx-lka.example"), // møx.example
93 SignedHeaders: []string{"from"},
94 Selector: dns.Domain{ASCII: "xn--tst-bma"},
95 Identity: &Identity{&ulp, xdomain("xn--tst-bma.xn--mx-lka.example")}, // tést.møx.example
96 Canonicalization: "simple/simple",
97 Length: -1,
98 SignTime: -1,
99 ExpireTime: -1,
100 }
101 test("dkim-signature: v = 1 ; a=ed25519-sha256; s=xn--tst-bma; d=xn--mx-lka.example; h=from; b=dGVzdAo=; bh=LjkN2rUhrS3zKXfH2vNgUzz5ERRJkgP9CURXBX0JP0Q= ; i=møx@xn--tst-bma.xn--mx-lka.example;\r\n", true, sig2, nil)
102 test("dkim-signature: v = 1 ; a=ed25519-sha256; s=xn--tst-bma; d=xn--mx-lka.example; h=from; b=dGVzdAo=; bh=LjkN2rUhrS3zKXfH2vNgUzz5ERRJkgP9CURXBX0JP0Q= ; i=møx@xn--tst-bma.xn--mx-lka.example;\r\n", false, nil, parseErr("")) // No UTF-8 allowed.
103
104 multiatom := smtp.Localpart("a.b.c")
105 sig3 := &Sig{
106 Version: 1,
107 AlgorithmSign: "ed25519",
108 AlgorithmHash: "sha256",
109 Signature: xbase64("dGVzdAo="),
110 BodyHash: xbase64("LjkN2rUhrS3zKXfH2vNgUzz5ERRJkgP9CURXBX0JP0Q="),
111 Domain: xdomain("mox.example"),
112 SignedHeaders: []string{"from"},
113 Selector: xdomain("test"),
114 Identity: &Identity{&multiatom, xdomain("mox.example")},
115 Canonicalization: "simple/simple",
116 Length: -1,
117 SignTime: -1,
118 ExpireTime: -1,
119 }
120 test("dkim-signature: v = 1 ; a=ed25519-sha256; s=test; d=mox.example; h=from; b=dGVzdAo=; bh=LjkN2rUhrS3zKXfH2vNgUzz5ERRJkgP9CURXBX0JP0Q= ; i=a.b.c@mox.example\r\n", true, sig3, nil)
121
122 quotedlp := smtp.Localpart(`test "\test`)
123 sig4 := &Sig{
124 Version: 1,
125 AlgorithmSign: "ed25519",
126 AlgorithmHash: "sha256",
127 Signature: xbase64("dGVzdAo="),
128 BodyHash: xbase64("LjkN2rUhrS3zKXfH2vNgUzz5ERRJkgP9CURXBX0JP0Q="),
129 Domain: xdomain("mox.example"),
130 SignedHeaders: []string{"from"},
131 Selector: xdomain("test"),
132 Identity: &Identity{&quotedlp, xdomain("mox.example")},
133 Canonicalization: "simple/simple",
134 Length: -1,
135 SignTime: -1,
136 ExpireTime: -1,
137 }
138 test("dkim-signature: v = 1 ; a=ed25519-sha256; s=test; d=mox.example; h=from; b=dGVzdAo=; bh=LjkN2rUhrS3zKXfH2vNgUzz5ERRJkgP9CURXBX0JP0Q= ; i=\"test \\\"\\\\test\"@mox.example\r\n", true, sig4, nil)
139
140 test("", true, nil, errSigMissingCRLF)
141 test("other: ...\r\n", true, nil, errSigHeader)
142 test("dkim-signature: v=2\r\n", true, nil, errSigUnknownVersion)
143 test("dkim-signature: v=1\r\n", true, nil, errSigMissingTag)
144 test("dkim-signature: v=1;v=1\r\n", true, nil, errSigDuplicateTag)
145 test("dkim-signature: v=1; d=mox.example; i=@unrelated.example; s=test; a=ed25519-sha256; h=from; b=dGVzdAo=; bh=LjkN2rUhrS3zKXfH2vNgUzz5ERRJkgP9CURXBX0JP0Q=\r\n", true, nil, errSigIdentityDomain)
146 test("dkim-signature: v=1; t=10; x=9; d=mox.example; s=test; a=ed25519-sha256; h=from; b=dGVzdAo=; bh=LjkN2rUhrS3zKXfH2vNgUzz5ERRJkgP9CURXBX0JP0Q=\r\n", true, nil, errSigExpired)
147 test("dkim-signature: v=1; d=møx.example\r\n", true, nil, parseErr("")) // Unicode domain not allowed.
148 test("dkim-signature: v=1; s=tést\r\n", true, nil, parseErr("")) // Unicode selector not allowed.
149 test("dkim-signature: v=1; ;\r\n", true, nil, parseErr("")) // Empty tag not allowed.
150 test("dkim-signature: v=1; \r\n", true, nil, parseErr("")) // Cannot have whitespace after last colon.
151 test("dkim-signature: v=1; d=mox.example; s=test; a=ed25519-sha256; h=from; b=dGVzdAo=; bh=dGVzdAo=\r\n", true, nil, errSigBodyHash)
152 test("dkim-signature: v=1; d=mox.example; s=test; a=rsa-sha1; h=from; b=dGVzdAo=; bh=dGVzdAo=\r\n", true, nil, errSigBodyHash)
153}
154
155func TestCopiedHeadersSig(t *testing.T) {
156 // ../rfc/6376:1391
157 sigHeader := strings.ReplaceAll(`DKIM-Signature: v=1; a=rsa-sha256; d=example.net; s=brisbane;
158 c=simple; q=dns/txt; i=@eng.example.net;
159 t=1117574938; x=1118006938;
160 h=from:to:subject:date;
161 z=From:foo@eng.example.net|To:joe@example.com|
162 Subject:demo=20run|Date:July=205,=202005=203:44:08=20PM=20-0700;
163 bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;
164 b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZVoG4ZHRNiYzR
165`, "\n", "\r\n")
166
167 sig, _, err := parseSignature([]byte(sigHeader), false)
168 if err != nil {
169 t.Fatalf("parsing dkim signature with copied headers: %v", err)
170 }
171 exp := []string{
172 "From:foo@eng.example.net",
173 "To:joe@example.com",
174 "Subject:demo run",
175 "Date:July 5, 2005 3:44:08 PM -0700",
176 }
177 if !reflect.DeepEqual(sig.CopiedHeaders, exp) {
178 t.Fatalf("copied headers, got %v, expected %v", sig.CopiedHeaders, exp)
179 }
180}
181