package dkim import ( "context" "crypto" "encoding/base64" "errors" "fmt" "net" "strings" "testing" "github.com/google/go-cmp/cmp" ) func toCRLF(s string) string { return strings.ReplaceAll(s, "\n", "\r\n") } func makeLookupTXT(results map[string][]string) lookupTXTFunc { return func(ctx context.Context, domain string) ([]string, error) { return results[domain], nil } } func TestVerifyRF6376CExample(t *testing.T) { ctx := t.Context() ctx = WithTraceFunc(ctx, t.Logf) // Use the public key from the example in RFC 6376 appendix C. // https://datatracker.ietf.org/doc/html/rfc6376#appendix-C ctx = WithLookupTXTFunc(ctx, makeLookupTXT(map[string][]string{ "brisbane._domainkey.example.com": { "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ" + "KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt" + "IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v" + "/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi" + "tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB", }, })) // Note that the examples in the RFC text have multiple issues: // - The double space in "game. Are" should be a single // space. Otherwise, the body hash does not match. // https://www.rfc-editor.org/errata/eid3192 // - The header indentation is incorrect. This causes // signature validation failure (because the example uses simple // canonicalization, which leaves the indentation untouched). // https://www.rfc-editor.org/errata/eid4926 message := toCRLF( `DKIM-Signature: v=1; a=rsa-sha256; s=brisbane; d=example.com; c=simple/simple; q=dns/txt; i=joe@football.example.com; h=Received : From : To : Subject : Date : Message-ID; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=; b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB 4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV 4bmp/YzhwvcubU4=; Received: from client1.football.example.com [192.0.2.1] by submitserver.example.com with SUBMISSION; Fri, 11 Jul 2003 21:01:54 -0700 (PDT) From: Joe SixPack To: Suzie Q Subject: Is dinner ready? Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT) Message-ID: <20030712040037.46341.5F8J@football.example.com> Hi. We lost the game. Are you hungry yet? Joe. `) res, err := VerifyMessage(ctx, message) if res.Valid != 1 || err != nil { t.Errorf("VerifyMessage: wanted 1 valid / nil; got %v / %v", res, err) } // Extend the message, check it does not pass validation. res, err = VerifyMessage(ctx, message+"Extra line.\r\n") if res.Valid != 0 || err != nil { t.Errorf("VerifyMessage: wanted 0 valid / nil; got %v / %v", res, err) } // Alter a header, check it does not pass validation. res, err = VerifyMessage(ctx, strings.Replace(message, "Subject", "X-Subject", 1)) if res.Valid != 0 || err != nil { t.Errorf("VerifyMessage: wanted 0 valid / nil; got %v / %v", res, err) } } func TestVerifyRFC8463Example(t *testing.T) { ctx := t.Context() ctx = WithTraceFunc(ctx, t.Logf) // Use the public keys from the example in RFC 8463 appendix A.2. // https://datatracker.ietf.org/doc/html/rfc6376#appendix-C ctx = WithLookupTXTFunc(ctx, makeLookupTXT(map[string][]string{ "brisbane._domainkey.football.example.com": { "v=DKIM1; k=ed25519; " + "p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="}, "test._domainkey.football.example.com": { "v=DKIM1; k=rsa; " + "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWR" + "iGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/b" + "yYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKr" + "M3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K" + "4w3QIDAQAB"}, })) message := toCRLF( `DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed; d=football.example.com; i=@football.example.com; q=dns/txt; s=brisbane; t=1528637909; h=from : to : subject : date : message-id : from : subject : date; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=; b=/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQUdt9OdqQehSwhEIug4D11Bus Fa3bT3FY5OsU7ZbnKELq+eXdp1Q1Dw== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=football.example.com; i=@football.example.com; q=dns/txt; s=test; t=1528637909; h=from : to : subject : date : message-id : from : subject : date; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=; b=F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8GwpsRe0IeIixNTe3 DhCVlUrSjV4BwcVcOF6+FF3Zo9Rpo1tFOeS9mPYQTnGdaSGsgeefOsk2Jz dA+L10TeYt9BgDfQNZtKdN1WO//KgIqXP7OdEFE4LjFYNcUxZQ4FADY+8= From: Joe SixPack To: Suzie Q Subject: Is dinner ready? Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT) Message-ID: <20030712040037.46341.5F8J@football.example.com> Hi. We lost the game. Are you hungry yet? Joe. `) expected := &VerifyResult{ Found: 2, Valid: 2, Results: []*OneResult{ { SignatureHeader: toCRLF( ` v=1; a=ed25519-sha256; c=relaxed/relaxed; d=football.example.com; i=@football.example.com; q=dns/txt; s=brisbane; t=1528637909; h=from : to : subject : date : message-id : from : subject : date; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=; b=/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQUdt9OdqQehSwhEIug4D11Bus Fa3bT3FY5OsU7ZbnKELq+eXdp1Q1Dw==`), Domain: "football.example.com", Selector: "brisbane", B: "/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQUdt9OdqQehSwhEIug4D11" + "BusFa3bT3FY5OsU7ZbnKELq+eXdp1Q1Dw==", State: SUCCESS, Error: nil, }, { SignatureHeader: toCRLF( ` v=1; a=rsa-sha256; c=relaxed/relaxed; d=football.example.com; i=@football.example.com; q=dns/txt; s=test; t=1528637909; h=from : to : subject : date : message-id : from : subject : date; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=; b=F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8GwpsRe0IeIixNTe3 DhCVlUrSjV4BwcVcOF6+FF3Zo9Rpo1tFOeS9mPYQTnGdaSGsgeefOsk2Jz dA+L10TeYt9BgDfQNZtKdN1WO//KgIqXP7OdEFE4LjFYNcUxZQ4FADY+8=`), Domain: "football.example.com", Selector: "test", B: "F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8GwpsRe0IeIixNTe" + "3DhCVlUrSjV4BwcVcOF6+FF3Zo9Rpo1tFOeS9mPYQTnGdaSGsgeefO" + "sk2JzdA+L10TeYt9BgDfQNZtKdN1WO//KgIqXP7OdEFE4LjFYNcUxZ" + "Q4FADY+8=", State: SUCCESS, Error: nil, }, }, } res, err := VerifyMessage(ctx, message) if err != nil { t.Fatalf("VerifyMessage returned error: %v", err) } if diff := cmp.Diff(expected, res); diff != "" { t.Errorf("VerifyMessage diff (-want +got):\n%s", diff) } // Extend the message, check it does not pass validation. res, err = VerifyMessage(ctx, message+"Extra line.\r\n") if res.Found != 2 || res.Valid != 0 || err != nil { t.Errorf("VerifyMessage: wanted 2 found, 0 valid / nil; got %v / %v", res, err) } // Alter a header, check it does not pass validation. res, err = VerifyMessage(ctx, strings.Replace(message, "Subject", "X-Subject", 1)) if res.Found != 2 || res.Valid != 0 || err != nil { t.Errorf("VerifyMessage: wanted 2 found, 0 valid / nil; got %v / %v", res, err) } } func TestVerifyBodyLength(t *testing.T) { ctx := t.Context() ctx = WithTraceFunc(ctx, t.Logf) // Any valid key works, since none of the signatures here are valid. We // use the one from the example in RFC 8463 appendix A.2. ctx = WithLookupTXTFunc(ctx, makeLookupTXT(map[string][]string{ "brisbane._domainkey.example.com": { "v=DKIM1; k=ed25519; " + "p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="}, })) // Use a valid body hash (of the "signed" part of the body), so we can // tell the l= cases apart: when l= covers exactly the signed part, the // body hash matches and we fail later on (because b= is bogus). body := "Hi.\r\n" cases := []struct { l string signed string want error }{ {"0", "", ErrVerificationFailed}, {"0", body, ErrBodyHashMismatch}, {"4", body, ErrBodyHashMismatch}, {"5", body, ErrVerificationFailed}, {"6", body, errInvalidTag}, {"18446744073709551615", body, errInvalidTag}, } for _, c := range cases { bh := base64.StdEncoding.EncodeToString( hashWith(crypto.SHA256, []byte(c.signed))) message := toCRLF( "DKIM-Signature: v=1; a=ed25519-sha256; c=simple/simple;\n"+ " d=example.com; s=brisbane; h=from; l="+c.l+";\n"+ " bh="+bh+"; b=AAAA\n"+ "From: Joe SixPack \n"+ "\n") + body res, err := VerifyMessage(ctx, message) if err != nil { t.Fatalf("l=%s signed=%q: VerifyMessage returned error: %v", c.l, c.signed, err) } if res.Found != 1 || res.Valid != 0 { t.Fatalf("l=%s signed=%q: wanted 1 found, 0 valid; got %v", c.l, c.signed, res) } r := res.Results[0] if r.State != PERMFAIL || !errors.Is(r.Error, c.want) { t.Errorf("l=%s signed=%q: wanted PERMFAIL / %v; got %v / %v", c.l, c.signed, c.want, r.State, r.Error) } } } func TestVerifyInvalidDomain(t *testing.T) { ctx := t.Context() ctx = WithTraceFunc(ctx, t.Logf) ctx = WithLookupTXTFunc(ctx, makeLookupTXT(map[string][]string{})) // A d= tag that is not a valid domain must be rejected, and must not // make it to the Authentication-Results header, where it could inject // content. message := toCRLF( "DKIM-Signature: v=1; a=ed25519-sha256; c=simple/simple;\n" + " d=example.com dkim=pass header.d=example.net; s=sel;\n" + " h=from; bh=AAAA; b=AAAA\n" + "From: Joe SixPack \n" + "\n" + "Hi.\n") res, err := VerifyMessage(ctx, message) if err != nil { t.Fatalf("VerifyMessage returned error: %v", err) } if res.Found != 1 || res.Valid != 0 { t.Fatalf("wanted 1 found, 0 valid; got %v", res) } r := res.Results[0] if r.State != PERMFAIL || !errors.Is(r.Error, errInvalidTag) { t.Errorf("wanted PERMFAIL / %v; got %v / %v", errInvalidTag, r.State, r.Error) } ar := res.AuthenticationResults() if strings.Contains(ar, "example.net") { t.Errorf("Authentication-Results contains the invalid d=: %q", ar) } } func TestBTag(t *testing.T) { // Test that we remove the value of the b= tag (and only that) from the // canonicalized DKIM-Signature header. // The expected values match what Mail::DKIM does (except for the cases // without a b= tag, where it appends one). cases := []struct { in, want string }{ // Relaxed canonicalization, b= in different positions. {"dkim-signature:v=1; bh=AAAA; b=SIG", "dkim-signature:v=1; bh=AAAA; b="}, {"dkim-signature:v=1; bh=AAAA; b=SIG;", "dkim-signature:v=1; bh=AAAA; b=;"}, {"dkim-signature:b=SIG; v=1", "dkim-signature:b=; v=1"}, {"dkim-signature:v=1; b=SIG; bh=AAAA; a=ed25519-sha256", "dkim-signature:v=1; b=; bh=AAAA; a=ed25519-sha256"}, // Simple canonicalization: the header name is unchanged, and // whitespace (including folding) is preserved. {"DKIM-Signature: b=SIG; v=1", "DKIM-Signature: b=; v=1"}, {"DKIM-Signature: v=1;\r\n\tb=SIG1\r\n\t SIG2;\r\n\tbh=AAAA", "DKIM-Signature: v=1;\r\n\tb=;\r\n\tbh=AAAA"}, {"DKIM-Signature: v=1;\r\n\t b=SIG", "DKIM-Signature: v=1;\r\n\t b="}, {"DKIM-Signature: v=1;\r\n b\r\n =\r\n SIG\r\n ;", "DKIM-Signature: v=1;\r\n b\r\n =;"}, // Whitespace around and inside the value is removed. {"dkim-signature:v=1; b = SI G ;", "dkim-signature:v=1; b =;"}, // Empty b= tag. {"dkim-signature:v=1; b=; bh=AAAA", "dkim-signature:v=1; b=; bh=AAAA"}, {"dkim-signature:v=1; b=", "dkim-signature:v=1; b="}, // The whole value is removed, even if it's not valid base64. {"dkim-signature:v=1; b=SI!G; x=1", "dkim-signature:v=1; b=; x=1"}, // Degenerate folding, with a ':' at the beginning of a continuation // line. {"DKIM-Signature: v=1; h=from:to\r\n\t:subject;\r\n\tb=SIG", "DKIM-Signature: v=1; h=from:to\r\n\t:subject;\r\n\tb="}, // Tags and values that contain "b=" but are not the b= tag must be // left alone. {"dkim-signature:v=1; bh=AAAA", "dkim-signature:v=1; bh=AAAA"}, {"dkim-signature:v=1; bh=AAb=; b=SIG", "dkim-signature:v=1; bh=AAb=; b="}, {"dkim-signature:v=1; xb=1; b=SIG", "dkim-signature:v=1; xb=1; b="}, {"dkim-signature:v=1; x=b=1; b=SIG", "dkim-signature:v=1; x=b=1; b="}, {"dkim-signature:v=1; x=y b=1; b=SIG", "dkim-signature:v=1; x=y b=1; b="}, {"dkim-signature:v=1; z=Subject:Is=20the=20club=20open?; b=SIG", "dkim-signature:v=1; z=Subject:Is=20the=20club=20open?; b="}, {"dkim-signature:z=Subject:b=20x; b=SIG", "dkim-signature:z=Subject:b=20x; b="}, {"dkim-signature:v=1; i=bob=2B@example.com; b=SIG", "dkim-signature:v=1; i=bob=2B@example.com; b="}, {"dkim-signature:v=1; xb=1; z=a:b=20", "dkim-signature:v=1; xb=1; z=a:b=20"}, // Tag names are case-sensitive. {"dkim-signature:v=1; B=SIG; b=SIG", "dkim-signature:v=1; B=SIG; b="}, } for _, c := range cases { got := bTag.ReplaceAllString(c.in, "$1$2") if got != c.want { t.Errorf("bTag replace on %q:", c.in) t.Errorf(" want %q", c.want) t.Errorf(" got %q", got) } // Cross-check against the tag parser: the result must have the // same tags as the input, with only b= (if present) emptied. _, inV, _ := strings.Cut(c.in, ":") _, gotV, _ := strings.Cut(got, ":") inTags, err := parseTags(inV) if err != nil { t.Fatalf("parseTags(%q) error: %v", inV, err) } gotTags, err := parseTags(gotV) if err != nil { t.Fatalf("parseTags(%q) error: %v", gotV, err) } if _, ok := inTags["b"]; ok { inTags["b"] = "" } if diff := cmp.Diff(inTags, gotTags); diff != "" { t.Errorf("bTag replace on %q: tags diff (-want +got):\n%s", c.in, diff) } } } func TestHeadersToInclude(t *testing.T) { // Test that headersToInclude returns the expected headers. cases := []struct { sigH header hTag []string headers headers want []header }{ // Check that if a header appears more than once, we pick the latest // first. { sigH: header{ Name: "DKIM-Signature", Value: "v=1; a=rsa-sha256; s=brisbane; d=example.com;", }, hTag: []string{"From", "To", "Subject"}, headers: headers{ {Name: "From", Value: "from1"}, {Name: "To", Value: "to1"}, {Name: "Subject", Value: "subject1"}, {Name: "From", Value: "from2"}, }, want: []header{ {Name: "From", Value: "from2"}, {Name: "To", Value: "to1"}, {Name: "Subject", Value: "subject1"}, }, }, // Check that if a header is requested twice but only appears once, we // only return it once. // This is a common technique suggested by the RFC to make signatures // fail if a header is added. { sigH: header{ Name: "DKIM-Signature", Value: "v=1; a=rsa-sha256; s=brisbane; d=example.com;", }, hTag: []string{"From", "From", "To", "Subject"}, headers: headers{ {Name: "From", Value: "from1"}, {Name: "To", Value: "to1"}, {Name: "Subject", Value: "subject1"}, }, want: []header{ {Name: "From", Value: "from1"}, {Name: "To", Value: "to1"}, {Name: "Subject", Value: "subject1"}, }, }, // Check that if DKIM-Signature is included, we do *not* include the // one we're currently verifying in the headers to include. // https://datatracker.ietf.org/doc/html/rfc6376#section-3.7 { sigH: header{ Name: "DKIM-Signature", Value: "v=1; a=rsa-sha256; s=brisbane; d=example.com;", }, hTag: []string{"From", "From", "DKIM-Signature", "DKIM-Signature"}, headers: headers{ {Name: "From", Value: "from1"}, {Name: "To", Value: "to1"}, { Name: "DKIM-Signature", Value: "v=1; a=rsa-sha256; s=sidney; d=example.com;", }, { Name: "DKIM-Signature", Value: "v=1; a=rsa-sha256; s=brisbane; d=example.com;", }, }, want: []header{ {Name: "From", Value: "from1"}, { Name: "DKIM-Signature", Value: "v=1; a=rsa-sha256; s=sidney; d=example.com;", }, }, }, // Check that header names are compared case-insensitively, and that // headers that are not present are skipped. { sigH: header{ Name: "DKIM-Signature", Value: "v=1; a=rsa-sha256; s=brisbane; d=example.com;", }, hTag: []string{"from", "X-Missing", "FROM", "subject"}, headers: headers{ {Name: "FROM", Value: "from1"}, {Name: "Subject", Value: "subject1"}, {Name: "From", Value: "from2"}, }, want: []header{ {Name: "From", Value: "from2"}, {Name: "FROM", Value: "from1"}, {Name: "Subject", Value: "subject1"}, }, }, } for _, c := range cases { got := headersToInclude(c.sigH, c.hTag, c.headers) if diff := cmp.Diff(c.want, got); diff != "" { t.Errorf("headersToInclude(%q, %v, %v) diff (-want +got):\n%s", c.sigH, c.hTag, c.headers, diff) } } } func TestHeadersToIncludeLarge(t *testing.T) { // Regression test for a DoS where headersToInclude was O(len(h=) * // len(headers)), which with large (attacker-controlled) inputs could take // hours. This takes milliseconds, while the quadratic implementation // took minutes with this input. We don't check the timing, so a // regression will show up as this test being very slow. const n = 100_000 sigH := header{Name: "DKIM-Signature", Value: "v=1;"} hTag := make([]string, n) hdrs := make(headers, n) for i := range n { hTag[i] = "a" hdrs[i] = header{Name: "a", Value: fmt.Sprint(i)} } got := headersToInclude(sigH, hTag, hdrs) if len(got) != n { t.Fatalf("wanted %d headers, got %d", n, len(got)) } // Last instance first. if got[0].Value != fmt.Sprint(n-1) || got[n-1].Value != "0" { t.Errorf("unexpected order: first %q, last %q", got[0].Value, got[n-1].Value) } } func TestAuthenticationResults(t *testing.T) { resBrisbane := &OneResult{ Domain: "football.example.com", Selector: "brisbane", B: "/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQUdt9OdqQehSwhEIug4D11" + "BusFa3bT3FY5OsU7ZbnKELq+eXdp1Q1Dw==", State: SUCCESS, Error: nil, } resTest := &OneResult{ Domain: "football.example.com", Selector: "test", B: "F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8GwpsRe0IeIixNTe" + "3DhCVlUrSjV4BwcVcOF6+FF3Zo9Rpo1tFOeS9mPYQTnGdaSGsgeefO" + "sk2JzdA+L10TeYt9BgDfQNZtKdN1WO//KgIqXP7OdEFE4LjFYNcUxZ" + "Q4FADY+8=", State: SUCCESS, Error: nil, } resFail := &OneResult{ Domain: "football.example.com", Selector: "paris", B: "slfkdMSDFeslif39seFfjl93sljisdsdlif923l", State: PERMFAIL, Error: ErrVerificationFailed, } resPermFail := &OneResult{ Domain: "football.example.com", Selector: "paris", // No B tag on purpose. State: PERMFAIL, Error: errMissingRequiredTag, } resTempFail := &OneResult{ Domain: "football.example.com", Selector: "paris", B: "shorty", // Less than 12 characters to check we include it well. State: TEMPFAIL, Error: &net.DNSError{ Err: "dns temp error (for testing)", IsTemporary: true, }, } cases := []struct { results *VerifyResult want string }{ { results: &VerifyResult{}, want: ";dkim=none\r\n", }, { results: &VerifyResult{ Found: 1, Valid: 1, Results: []*OneResult{resBrisbane}, }, want: ";dkim=pass" + " header.b=/gCrinpcQOoI header.d=football.example.com\r\n", }, { results: &VerifyResult{ Found: 2, Valid: 2, Results: []*OneResult{resBrisbane, resTest}, }, want: ";dkim=pass" + " header.b=/gCrinpcQOoI header.d=football.example.com\r\n" + ";dkim=pass" + " header.b=F45dVWDfMbQD header.d=football.example.com\r\n", }, { results: &VerifyResult{ Found: 2, Valid: 2, Results: []*OneResult{resBrisbane, resTest}, }, want: ";dkim=pass" + " header.b=/gCrinpcQOoI header.d=football.example.com\r\n" + ";dkim=pass" + " header.b=F45dVWDfMbQD header.d=football.example.com\r\n", }, { results: &VerifyResult{ Found: 2, Valid: 1, Results: []*OneResult{resFail, resTest}, }, want: ";dkim=fail reason=\"verification failed\"\r\n" + " header.b=slfkdMSDFesl header.d=football.example.com\r\n" + ";dkim=pass" + " header.b=F45dVWDfMbQD header.d=football.example.com\r\n", }, { results: &VerifyResult{ Found: 1, Results: []*OneResult{resPermFail}, }, want: ";dkim=permerror reason=\"missing required tag\"\r\n" + " header.d=football.example.com\r\n", }, { results: &VerifyResult{ Found: 1, Results: []*OneResult{resTempFail}, }, want: ";dkim=temperror reason=\"lookup : dns temp error (for testing)\"\r\n" + " header.b=shorty header.d=football.example.com\r\n", }, } for i, c := range cases { got := c.results.AuthenticationResults() if diff := cmp.Diff(c.want, got); diff != "" { t.Errorf("case %d: AuthenticationResults() diff (-want +got):\n%s", i, diff) } } } func FuzzVerifyMessage(f *testing.F) { addTestdataCorpus(f) // Return valid keys for every domain, so we can get past the key lookup // and exercise the rest of the verification. The ed25519 key is the one // used by testdata/01-rfc8463. ctx := WithLookupTXTFunc(context.Background(), func(ctx context.Context, domain string) ([]string, error) { return []string{ "v=DKIM1; k=ed25519; p=" + exampleEd25519KeyB64, "v=DKIM1; k=rsa; p=" + exampleRSAKeyB64, }, nil }) f.Fuzz(func(t *testing.T, msg string) { res, err := VerifyMessage(ctx, msg) if err != nil { return } if res.Found != uint(len(res.Results)) { t.Errorf("found %d signatures, but got %d results", res.Found, len(res.Results)) } if res.Valid > res.Found { t.Errorf("%d valid signatures, but only %d found", res.Valid, res.Found) } // This is always called on the result when receiving a message, so // exercise it too. res.AuthenticationResults() }) }