18	"github.com/mjl-/mox/dns"
 
19	"github.com/mjl-/mox/mlog"
 
22var pkglog = mlog.New("dkim", nil)
 
24func policyOK(sig *Sig) error {
 
28func parseRSAKey(t *testing.T, rsaText string) *rsa.PrivateKey {
 
29	rsab, _ := pem.Decode([]byte(rsaText))
 
31		t.Fatalf("no pem in privKey")
 
34	key, err := x509.ParsePKCS8PrivateKey(rsab.Bytes)
 
36		t.Fatalf("parsing private key: %s", err)
 
38	return key.(*rsa.PrivateKey)
 
41func getRSAKey(t *testing.T) *rsa.PrivateKey {
 
43	// openssl genrsa -out pkcs1.pem 2048
 
44	// openssl pkcs8 -topk8 -inform pem -in pkcs1.pem -outform pem -nocrypt -out pkcs8.pem
 
45	const rsaText = `-----BEGIN PRIVATE KEY-----
 
46MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCu7iTF/AAvJQ3U
 
47WRlcXd+n6HXOSYvmDlqjLsuCKn6/T+Ma0ZtobCRfzyXh5pFQBCHffW6fpEzJs/2o
 
48+e896zb1QKjD8Xxsjarjdw1iXzgMj/lhDGWyNyUHC34+k77UfpQBZgPLvZHyYyQG
 
49sVMzzmvURE+GMFmXYUiGI581PdCx4bNba/4gYQnc/eqQ8oX0T//2RdRqdhdDM2d7
 
50CYALtkxKetH1F+Rz7XDjFmI3GjPs1KwVdh+Cl8kejThi0SVxXpqnoqB2WGsr/lGG
 
51GxsxcpLb/+KWFjI0go3OJjMaxFCmhB0pGdW8I7kNwNrZsCdSvmjMDojNuegx6WMg
 
52/T7go3CvAgMBAAECggEAQA3AlmSDtr+lNDvZ7voKwwN6W6qPmRJpevZQG54u4iPA
 
53/5mAA/kRSqnh77mLPRb+RkU6RCeX3IXVXNIEGhKugZiHE5Sx4FfxmrAFzR8buXHg
 
54uXoeJOdPXiiFtilIh6u/y1FNE4YbUnud/fthgYdU8Zl/2x2KOMWtFj0l94tmhzOI
 
55b2y8/U8r85anI5XGYuzRCqKS1WskXhkXH8LZUB+9yAxX7V5ysgxjofM4FW8ns7yj
 
56K4cBS8KY2v3t7TZ4FgwkAhPcTfBc/E2UWT1Ztmr+18LFV5bqI8g2YlN+BgCxU7U/
 
571tawxqFhs+xowEpzNwAvjAIPpptIRiY1rz7sBB9g5QKBgQDLo/5rTUwNOPR9dYvA
 
58+DYUSCfxvNamI4GI66AgwOeN8O+W+dRDF/Ewbk/SJsBPSLIYzEiQ2uYKcNEmIjo+
 
597WwSCJZjKujovw77s9JAHexhpd8uLD2w9l3KeTg41LEYm2uVwoXWEHYSYJ9Ynz0M
 
60PWxvi2Hm0IoQ7gJIfxng/wIw3QKBgQDb6GFvPH/OTs40+dopwtm3irmkBAmT8N0b
 
613TpehONCOiL4GPxmn2DN6ELhHFV27Jj/1CfpGVbcBlaS1xYUGUGsB9gYukhdaBST
 
62KGHRoeZDcf0gaQLKG15EEfFOvcKI9aGljV8FdFfG+Z4fW3LA8khvpvjLLkv1A1jM
 
63MrEBthco+wKBgD45EM9GohtUMNh450gCT7voxFPICKphJP5qSNZZOyeS3BJ8qdAK
 
64a8cJndgvwQk4xDpxiSbBzBKaoD2Prc52i1QDTbhlbx9W6cQdEPxIaGb54PThzcPZ
 
65s5Tfbz9mNeq36qqq8mwTQZCh926D0YqA5jY7F6IITHeZ0hbGx2iJYuj9AoGARIyK
 
66ms8kE95y3wanX+8ySMmAlsT/a1NgyUfL4xzPbpyKvAWl4CN8XJMzDdL0PS8BfnXW
 
67vw28CrgbEojjg/5ff02uqf6fgiZoi3rCC0PJcGq++fRh/zhKyTNCokX6txDCg8Wu
 
68wheDKS40gRfTjJu5wrwsv8E9wjF546VFkf/99jMCgYEAm/x+kEfWKuzx8pQT66TY
 
69pxnC41upJOO1htTHNIN24J7XrrFI5+OZq90G+t/VgWX08Z8RlhejX+ukBf+SRu3u
 
705VMGcAs4px+iECX/FHo21YQFnrmArN1zdFxPU3rBWoBueqmGO6FT0HBbKzTuS7N0
 
717fIv3GQqImz3+ZbYWlXfkPI=
 
72-----END PRIVATE KEY-----`
 
73	return parseRSAKey(t, rsaText)
 
76func getWeakRSAKey(t *testing.T) *rsa.PrivateKey {
 
77	const rsaText = `-----BEGIN PRIVATE KEY-----
 
78MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEAsQo3ATJAZ4aAZz+l
 
79ndXl27ODOY+49DjYxwhgtg+OU8A1WEYCfWaZ7ozYtpsqH8GNFvlKtK38eKbdDuLw
 
80gsFYMQIDAQABAkBwstb2/P1Aqb9deoe8JOiw5eJYJySO2w0sDio6W0a4Cqi7XQ7r
 
81/yZ1gOp+ZnShX/sJq0Pd16UkJUUEtEPoZyptAiEA4KLP8pz/9R0t7Envqph1oVjQ
 
82CVDIL/UKRmdnMiwwDosCIQDJwiu08UgNNeliAygbkC2cdszjf4a3laGmYbfWrtAn
 
83swIgUBfc+w0degDgadpm2LWpY1DuRBQIfIjrE/U0Z0A4FkcCIHxEuoLycjygziTu
 
84aM/BWDac/cnKDIIbCbvfSEpU1iT9AiBsbkAcYCQ8mR77BX6gZKEc74nSce29gmR7
 
86-----END PRIVATE KEY-----`
 
87	return parseRSAKey(t, rsaText)
 
90func TestParseSignature(t *testing.T) {
 
91	// Domain name must always be A-labels, not U-labels. We do allow localpart with non-ascii.
 
92	hdr := `DKIM-Signature: v=1; a=rsa-sha256; d=xn--h-bga.mox.example; s=xn--yr2021-pua;
 
93        i=møx@xn--h-bga.mox.example; t=1643719203; h=From:To:Cc:Bcc:Reply-To:
 
94        References:In-Reply-To:Subject:Date:Message-ID:Content-Type:From:To:Subject:
 
95        Date:Message-ID:Content-Type;
 
96        bh=g3zLYH4xKxcPrHOD18z9YfpQcnk/GaJedfustWU5uGs=; b=dtgAOl71h/dNPQrmZTi3SBVkm+
 
97        EjMnF7sWGT123fa5g+m6nGpPue+I+067wwtkWQhsedbDkqT7gZb5WaG5baZsr9e/XpJ/iX4g6YXpr
 
98        07aLY8eF9jazcGcRCVCqLtyq0UJQ2Oz/ML74aYu1beh3jXsoI+k3fJ+0/gKSVC7enCFpNe1HhbXVS
 
99        4HRy/Rw261OEIy2e20lyPT4iDk2oODabzYa28HnXIciIMELjbc/sSawG68SAnhwdkWBrRzBDMCCHm
 
100        wvkmgDsVJWtdzjJqjxK2mYVxBMJT0lvsutXgYQ+rr6BLtjHsOb8GMSbQGzY5SJ3N8TP02pw5OykBu
 
104	_, _, err := parseSignature([]byte(strings.ReplaceAll(hdr, "\n", "\r\n")), smtputf8)
 
106		t.Fatalf("parsing signature: %s", err)
 
110func TestVerifyRSA(t *testing.T) {
 
111	message := strings.ReplaceAll(`Return-Path: <mechiel@ueber.net>
 
112X-Original-To: mechiel@ueber.net
 
113Delivered-To: mechiel@ueber.net
 
114Received: from [IPV6:2a02:a210:4a3:b80:ca31:30ee:74a7:56e0] (unknown [IPv6:2a02:a210:4a3:b80:ca31:30ee:74a7:56e0])
 
115	by koriander.ueber.net (Postfix) with ESMTPSA id E119EDEB0B
 
116	for <mechiel@ueber.net>; Fri, 10 Dec 2021 20:09:08 +0100 (CET)
 
117DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=ueber.net;
 
118	s=koriander; t=1639163348;
 
119	bh=g3zLYH4xKxcPrHOD18z9YfpQcnk/GaJedfustWU5uGs=;
 
120	h=Date:To:From:Subject:From;
 
121	b=rpWruWprs2TB7/MnulA2n2WtfUIfrrnAvRoSrip1ruX5ORN4AOYPPMmk/gGBDdc6O
 
122	 grRpSsNzR9BrWcooYfbNfSbl04nPKMp0acsZGfpvkj0+mqk5b8lqZs3vncG1fHlQc7
 
123	 0KXfnAHyEs7bjyKGbrw2XG1p/EDoBjIjUsdpdCAtamMGv3A3irof81oSqvwvi2KQks
 
124	 17aB1YAL9Xzkq9ipo1aWvDf2W6h6qH94YyNocyZSVJ+SlVm3InNaF8APkV85wOm19U
 
125	 9OW81eeuQbvSPcQZJVOmrWzp7XKHaXH0MYE3+hdH/2VtpCnPbh5Zj9SaIgVbaN6NPG
 
127Message-ID: <427999f6-114f-e59c-631e-ab2a5f6bfe4c@ueber.net>
 
128Date: Fri, 10 Dec 2021 20:09:08 +0100
 
130User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101
 
134From: Mechiel Lukkien <mechiel@ueber.net>
 
136Content-Type: text/plain; charset=UTF-8; format=flowed
 
137Content-Transfer-Encoding: 7bit
 
142	resolver := dns.MockResolver{
 
143		TXT: map[string][]string{
 
144			"koriander._domainkey.ueber.net.": {"v=DKIM1; k=rsa; s=email; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy3Z9ffZe8gUTJrdGuKj6IwEembmKYpp0jMa8uhudErcI4gFVUaFiiRWxc4jP/XR9NAEv3XwHm+CVcHu+L/n6VWt6g59U7vHXQicMfKGmEp2VplsgojNy/Y5X9HdVYM0azsI47NcJCDW9UVfeOHdOSgFME4F8dNtUKC4KTB2d1pqj/yixz+V8Sv8xkEyPfSRHcNXIw0LvelqJ1MRfN3hO/3uQSVrPYYk4SyV0b6wfnkQs28fpiIpGQvzlGI5WkrdOQT5k4YHaEvZDLNdwiMeVZOEL7dDoFs2mQsovm+tH0StUAZTnr61NLVFfD5V6Ip1V9zVtspPHvYSuOWwyArFZ9QIDAQAB"},
 
148	results, err := Verify(context.Background(), pkglog.Logger, resolver, false, policyOK, strings.NewReader(message), false)
 
150		t.Fatalf("dkim verify: %v", err)
 
152	if len(results) != 1 || results[0].Status != StatusPass {
 
153		t.Fatalf("verify: unexpected results %v", results)
 
157func TestVerifyEd25519(t *testing.T) {
 
159	message := strings.ReplaceAll(`DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed;
 
160 d=football.example.com; i=@football.example.com;
 
161 q=dns/txt; s=brisbane; t=1528637909; h=from : to :
 
162 subject : date : message-id : from : subject : date;
 
163 bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
 
164 b=/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQUdt9OdqQehSwhEIug4D11Bus
 
165 Fa3bT3FY5OsU7ZbnKELq+eXdp1Q1Dw==
 
166DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 
167 d=football.example.com; i=@football.example.com;
 
168 q=dns/txt; s=test; t=1528637909; h=from : to : subject :
 
169 date : message-id : from : subject : date;
 
170 bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
 
171 b=F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8GwpsRe0IeIixNTe3
 
172 DhCVlUrSjV4BwcVcOF6+FF3Zo9Rpo1tFOeS9mPYQTnGdaSGsgeefOsk2Jz
 
173 dA+L10TeYt9BgDfQNZtKdN1WO//KgIqXP7OdEFE4LjFYNcUxZQ4FADY+8=
 
174From: Joe SixPack <joe@football.example.com>
 
175To: Suzie Q <suzie@shopping.example.net>
 
176Subject: Is dinner ready?
 
177Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
 
178Message-ID: <20030712040037.46341.5F8J@football.example.com>
 
182We lost the game.  Are you hungry yet?
 
188	resolver := dns.MockResolver{
 
189		TXT: map[string][]string{
 
190			"brisbane._domainkey.football.example.com.": {"v=DKIM1; k=ed25519; p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="},
 
191			"test._domainkey.football.example.com.":     {"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQAB"},
 
195	results, err := Verify(context.Background(), pkglog.Logger, resolver, false, policyOK, strings.NewReader(message), false)
 
197		t.Fatalf("dkim verify: %v", err)
 
199	if len(results) != 2 || results[0].Status != StatusPass || results[1].Status != StatusPass {
 
200		t.Fatalf("verify: unexpected results %#v", results)
 
204func TestSign(t *testing.T) {
 
205	message := strings.ReplaceAll(`Message-ID: <427999f6-114f-e59c-631e-ab2a5f6bfe4c@ueber.net>
 
206Date: Fri, 10 Dec 2021 20:09:08 +0100
 
208User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101
 
212From: Mechiel Lukkien <mechiel@ueber.net>
 
215Content-Type: text/plain; charset=UTF-8; format=flowed
 
216Content-Transfer-Encoding: 7bit
 
221	rsaKey := getRSAKey(t)
 
222	ed25519Key := ed25519.NewKeyFromSeed(make([]byte, 32))
 
227		Headers:    strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-ID,Content-Type", ","),
 
228		Domain:     dns.Domain{ASCII: "testrsa"},
 
231	// Now with sha1 and relaxed canonicalization.
 
235		Headers:    strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-ID,Content-Type", ","),
 
236		Domain:     dns.Domain{ASCII: "testrsa2"},
 
238	selrsa2.HeaderRelaxed = true
 
239	selrsa2.BodyRelaxed = true
 
242	seled25519 := Selector{
 
244		PrivateKey: ed25519Key,
 
245		Headers:    strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-ID,Content-Type", ","),
 
246		Domain:     dns.Domain{ASCII: "tested25519"},
 
248	// Again ed25519, but without sealing headers. Use sha256 again, for reusing the body hash from the previous dkim-signature.
 
249	seled25519b := Selector{
 
251		PrivateKey:  ed25519Key,
 
252		Headers:     strings.Split("From,To,Cc,Bcc,Reply-To,Subject,Date", ","),
 
254		Domain:      dns.Domain{ASCII: "tested25519b"},
 
256	selectors := []Selector{selrsa, selrsa2, seled25519, seled25519b}
 
258	ctx := context.Background()
 
259	headers, err := Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader(message))
 
261		t.Fatalf("sign: %v", err)
 
264	makeRecord := func(k string, publicKey any) string {
 
268			PublicKey: publicKey,
 
269			Flags:     []string{"s"},
 
271		txt, err := tr.Record()
 
273			t.Fatalf("making dns txt record: %s", err)
 
275		//log.Infof("txt record: %s", txt)
 
279	resolver := dns.MockResolver{
 
280		TXT: map[string][]string{
 
281			"testrsa._domainkey.mox.example.":      {makeRecord("rsa", rsaKey.Public())},
 
282			"testrsa2._domainkey.mox.example.":     {makeRecord("rsa", rsaKey.Public())},
 
283			"tested25519._domainkey.mox.example.":  {makeRecord("ed25519", ed25519Key.Public())},
 
284			"tested25519b._domainkey.mox.example.": {makeRecord("ed25519", ed25519Key.Public())},
 
288	nmsg := headers + message
 
290	results, err := Verify(ctx, pkglog.Logger, resolver, false, policyOK, strings.NewReader(nmsg), false)
 
292		t.Fatalf("verify: %s", err)
 
294	if len(results) != 4 || results[0].Status != StatusPass || results[1].Status != StatusPass || results[2].Status != StatusPass || results[3].Status != StatusPass {
 
295		t.Fatalf("verify: unexpected results %v\nheaders:\n%s", results, headers)
 
297	//log.Infof("headers:%s", headers)
 
298	//log.Infof("nmsg\n%s", nmsg)
 
300	// Multiple From headers.
 
301	_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader("From: <mjl@mox.example>\r\nFrom: <mjl@mox.example>\r\n\r\ntest"))
 
302	if !errors.Is(err, ErrFrom) {
 
303		t.Fatalf("sign, got err %v, expected ErrFrom", err)
 
307	_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader("Brom: <mjl@mox.example>\r\n\r\ntest"))
 
308	if !errors.Is(err, ErrFrom) {
 
309		t.Fatalf("sign, got err %v, expected ErrFrom", err)
 
312	// Malformed headers.
 
313	_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader(":\r\n\r\ntest"))
 
314	if !errors.Is(err, ErrHeaderMalformed) {
 
315		t.Fatalf("sign, got err %v, expected ErrHeaderMalformed", err)
 
317	_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader(" From:<mjl@mox.example>\r\n\r\ntest"))
 
318	if !errors.Is(err, ErrHeaderMalformed) {
 
319		t.Fatalf("sign, got err %v, expected ErrHeaderMalformed", err)
 
321	_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader("Frøm:<mjl@mox.example>\r\n\r\ntest"))
 
322	if !errors.Is(err, ErrHeaderMalformed) {
 
323		t.Fatalf("sign, got err %v, expected ErrHeaderMalformed", err)
 
325	_, err = Sign(ctx, pkglog.Logger, "mjl", dns.Domain{ASCII: "mox.example"}, selectors, false, strings.NewReader("From:<mjl@mox.example>"))
 
326	if !errors.Is(err, ErrHeaderMalformed) {
 
327		t.Fatalf("sign, got err %v, expected ErrHeaderMalformed", err)
 
331func TestVerify(t *testing.T) {
 
332	// We do many Verify calls, each time starting out with a valid configuration, then
 
333	// we modify one thing to trigger an error, which we check for.
 
335	const message = `From: <mjl@mox.example>
 
336To: <other@mox.example>
 
338Date: Fri, 10 Dec 2021 20:09:08 +0100
 
339Message-ID: <test@mox.example>
 
341Content-Type: text/plain; charset=UTF-8; format=flowed
 
342Content-Transfer-Encoding: 7bit
 
347	key := ed25519.NewKeyFromSeed(make([]byte, 32))
 
348	var resolver dns.MockResolver
 
352	var policy func(*Sig) error
 
354	var selectors []Selector
 
356	var signDomain dns.Domain
 
361		policy = DefaultPolicy
 
362		signDomain = dns.Domain{ASCII: "mox.example"}
 
367			PublicKey: key.Public(),
 
368			Flags:     []string{"s"},
 
371		txt, err := record.Record()
 
373			t.Fatalf("making dns txt record: %s", err)
 
377		resolver = dns.MockResolver{
 
378			TXT: map[string][]string{
 
379				"test._domainkey.mox.example.": {txt},
 
386			Headers:    strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-ID,Content-Type", ","),
 
387			Domain:     dns.Domain{ASCII: "test"},
 
389		selectors = []Selector{sel}
 
398		msg = strings.ReplaceAll(msg, "\n", "\r\n")
 
400		headers, err := Sign(context.Background(), pkglog.Logger, "mjl", signDomain, selectors, false, strings.NewReader(msg))
 
402			t.Fatalf("sign: %v", err)
 
408	test := func(expErr error, expStatus Status, expResultErr error, mod func()) {
 
417		results, err := Verify(context.Background(), pkglog.Logger, resolver, true, policy, strings.NewReader(msg), false)
 
418		if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
 
419			t.Fatalf("got verify error %v, expected %v", err, expErr)
 
421		if expStatus != "" && (len(results) == 0 || results[0].Status != expStatus) {
 
423			if len(results) > 0 {
 
424				status = results[0].Status
 
426			t.Fatalf("got status %q, expected %q", status, expStatus)
 
429		if len(results) > 0 {
 
430			resultErr = results[0].Err
 
432		if (resultErr == nil) != (expResultErr == nil) || resultErr != nil && !errors.Is(resultErr, expResultErr) {
 
433			t.Fatalf("got result error %v, expected %v", resultErr, expResultErr)
 
437	test(nil, StatusPass, nil, func() {})
 
439	// Cannot parse message, so not much more to do.
 
440	test(ErrHeaderMalformed, "", nil, func() {
 
442		msg = ":\r\n\r\n" // Empty header key.
 
447	test(nil, StatusPermerror, ErrNoRecord, func() {
 
450	// DNS request is failing temporarily.
 
451	test(nil, StatusTemperror, ErrDNS, func() {
 
452		resolver.Fail = []string{
 
453			"txt test._domainkey.mox.example.",
 
457	test(nil, StatusPermerror, ErrSyntax, func() {
 
458		resolver.TXT = map[string][]string{
 
459			"test._domainkey.mox.example.": {"v=DKIM1; bogus"},
 
463	test(nil, StatusTemperror, ErrSyntax, func() {
 
464		resolver.TXT = map[string][]string{
 
465			"test._domainkey.mox.example.": {"bogus"},
 
469	test(nil, StatusTemperror, ErrMultipleRecords, func() {
 
470		resolver.TXT["test._domainkey.mox.example."] = []string{recordTxt, recordTxt}
 
474	test(nil, StatusPermerror, errSigMissingTag, func() {
 
475		msg = strings.ReplaceAll("DKIM-Signature: v=1\n"+msg, "\n", "\r\n")
 
479	// Signature has valid syntax, but parameters aren't acceptable.
 
481	test(nil, StatusPermerror, ErrFrom, func() {
 
483		// Remove "from" from signed headers (h=).
 
484		msg = strings.ReplaceAll(msg, ":From:", ":")
 
485		msg = strings.ReplaceAll(msg, "=From:", "=")
 
489	test(nil, StatusPermerror, ErrTLD, func() {
 
490		// Pretend to sign as .com
 
491		msg = strings.ReplaceAll(msg, "From: <mjl@mox.example>\n", "From: <mjl@com>\n")
 
492		signDomain = dns.Domain{ASCII: "com"}
 
493		resolver.TXT = map[string][]string{
 
494			"test._domainkey.com.": {recordTxt},
 
497	// Unknown hash algorithm.
 
498	test(nil, StatusPermerror, ErrHashAlgorithmUnknown, func() {
 
500		msg = strings.ReplaceAll(msg, "sha256", "sha257")
 
502	// Unknown canonicalization.
 
503	test(nil, StatusPermerror, ErrCanonicalizationUnknown, func() {
 
504		sel.HeaderRelaxed = true
 
505		sel.BodyRelaxed = true
 
506		selectors = []Selector{sel}
 
509		msg = strings.ReplaceAll(msg, "relaxed/relaxed", "bogus/bogus")
 
512	test(nil, StatusPermerror, ErrQueryMethod, func() {
 
514		msg = strings.ReplaceAll(msg, "DKIM-Signature: ", "DKIM-Signature: q=other;")
 
518	test(nil, StatusPolicy, ErrPolicy, func() {
 
520		msg = strings.ReplaceAll(msg, "DKIM-Signature: ", "DKIM-Signature: l=1;")
 
523	test(nil, StatusPermerror, ErrHashAlgNotAllowed, func() {
 
524		recordTxt += ";h=sha1"
 
525		resolver.TXT = map[string][]string{
 
526			"test._domainkey.mox.example.": {recordTxt},
 
530	test(nil, StatusPermerror, ErrSigAlgMismatch, func() {
 
531		record.PublicKey = getRSAKey(t).Public()
 
533		txt, err := record.Record()
 
535			t.Fatalf("making dns txt record: %s", err)
 
537		resolver.TXT = map[string][]string{
 
538			"test._domainkey.mox.example.": {txt},
 
542	test(nil, StatusPermerror, ErrKeyRevoked, func() {
 
543		record.PublicKey = nil
 
544		txt, err := record.Record()
 
546			t.Fatalf("making dns txt record: %s", err)
 
548		resolver.TXT = map[string][]string{
 
549			"test._domainkey.mox.example.": {txt},
 
552	// We refuse rsa keys smaller than 1024 bits.
 
553	test(nil, StatusPermerror, ErrWeakKey, func() {
 
554		key := getWeakRSAKey(t)
 
556		record.PublicKey = key.Public()
 
557		txt, err := record.Record()
 
559			t.Fatalf("making dns txt record: %s", err)
 
561		resolver.TXT = map[string][]string{
 
562			"test._domainkey.mox.example.": {txt},
 
565		selectors = []Selector{sel}
 
568	test(nil, StatusPermerror, ErrKeyNotForEmail, func() {
 
569		recordTxt += ";s=other"
 
570		resolver.TXT = map[string][]string{
 
571			"test._domainkey.mox.example.": {recordTxt},
 
574	// todo: Record has flag "s" but identity does not have exact domain match. Cannot currently easily implement this test because Sign() always uses the same domain. 
../rfc/6376:1575 
575	// Wrong signature, different datahash, and thus signature.
 
576	test(nil, StatusFail, ErrSigVerify, func() {
 
578		msg = strings.ReplaceAll(msg, "Subject: test\r\n", "Subject: modified header\r\n")
 
580	// Signature is correct for bodyhash, but the body has changed.
 
581	test(nil, StatusFail, ErrBodyhashMismatch, func() {
 
583		msg = strings.ReplaceAll(msg, "\r\ntest\r\n", "\r\nmodified body\r\n")
 
586	// Check that last-occurring header field is used.
 
587	test(nil, StatusFail, ErrSigVerify, func() {
 
588		sel.SealHeaders = false
 
589		selectors = []Selector{sel}
 
591		msg = strings.ReplaceAll(msg, "\r\n\r\n", "\r\nsubject: another\r\n\r\n")
 
593	test(nil, StatusPass, nil, func() {
 
594		sel.SealHeaders = false
 
595		selectors = []Selector{sel}
 
597		msg = "subject: another\r\n" + msg
 
601func TestBodyHash(t *testing.T) {
 
602	simpleGot, err := bodyHash(crypto.SHA256.New(), true, bufio.NewReader(strings.NewReader("")))
 
604		t.Fatalf("body hash, simple, empty string: %s", err)
 
606	simpleWant := base64Decode("frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=")
 
607	if !bytes.Equal(simpleGot, simpleWant) {
 
608		t.Fatalf("simple body hash for empty string, got %s, expected %s", base64Encode(simpleGot), base64Encode(simpleWant))
 
611	relaxedGot, err := bodyHash(crypto.SHA256.New(), false, bufio.NewReader(strings.NewReader("")))
 
613		t.Fatalf("body hash, relaxed, empty string: %s", err)
 
615	relaxedWant := base64Decode("47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=")
 
616	if !bytes.Equal(relaxedGot, relaxedWant) {
 
617		t.Fatalf("relaxed body hash for empty string, got %s, expected %s", base64Encode(relaxedGot), base64Encode(relaxedWant))
 
620	compare := func(a, b []byte) {
 
622		if !bytes.Equal(a, b) {
 
623			t.Fatalf("hash not equal")
 
627	// NOTE: the trailing space in the strings below are part of the test for canonicalization.
 
630	exampleIn := strings.ReplaceAll(` c
 
635	relaxedOut := strings.ReplaceAll(` c
 
638	relaxedBh, err := bodyHash(crypto.SHA256.New(), false, bufio.NewReader(strings.NewReader(exampleIn)))
 
640		t.Fatalf("bodyhash: %s", err)
 
642	relaxedOutHash := sha256.Sum256([]byte(relaxedOut))
 
643	compare(relaxedBh, relaxedOutHash[:])
 
645	simpleOut := strings.ReplaceAll(` c
 
648	simpleBh, err := bodyHash(crypto.SHA256.New(), true, bufio.NewReader(strings.NewReader(exampleIn)))
 
650		t.Fatalf("bodyhash: %s", err)
 
652	simpleOutHash := sha256.Sum256([]byte(simpleOut))
 
653	compare(simpleBh, simpleOutHash[:])
 
656	relaxedBody := strings.ReplaceAll(`Hi.
 
658We lost the game.  Are you hungry yet?
 
663	relaxedGot, err = bodyHash(crypto.SHA256.New(), false, bufio.NewReader(strings.NewReader(relaxedBody)))
 
665		t.Fatalf("body hash, relaxed, ed25519 example: %s", err)
 
667	relaxedWant = base64Decode("2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=")
 
668	if !bytes.Equal(relaxedGot, relaxedWant) {
 
669		t.Fatalf("relaxed body hash for ed25519 example, got %s, expected %s", base64Encode(relaxedGot), base64Encode(relaxedWant))
 
673func base64Decode(s string) []byte {
 
674	buf, err := base64.StdEncoding.DecodeString(s)
 
681func base64Encode(buf []byte) string {
 
682	return base64.StdEncoding.EncodeToString(buf)