mirror of
https://blitiri.com.ar/repos/chasquid
synced 2026-10-28 15:02:28 +00:00
dkim: Make b= tag stripping more precise
When verifying a signature we need to strip the value of the b= tag. Today, we do this with a regexp that can accidentally match other tag values. For example, "z=Subject:club=20". This can cause valid emails to fail verification. But it can't cause an invalid email to pass verification, so it's not a significant security concern. To fix this, this patch updates the regular expression to match the tag more precisely. It also adds a few tests to validate the behaviour is correct, which were cross-checked with other implementations. Thanks to John JEAN <john@wargan.com> for reporting this bug.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
brisbane._domainkey.football.example.com: \
|
||||
v=DKIM1; k=ed25519; \
|
||||
p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed;
|
||||
d=football.example.com; s=brisbane; t=1528637909;
|
||||
h=from:to:subject:date:message-id;
|
||||
b=xjoLeGfcxnO99aQ8Tlc/unAH+heqD8dXVjKLdvzbUr7sctU9pf/V3FRluuVyLtxiZieesO7xRWJAcThkMDIqAA==;
|
||||
bh=W3XNOuZiySVQVWa4kPgRp9uVKwEhAmhwKC5NysNOjhg=;
|
||||
xb=1;
|
||||
z=Subject:Is=20the=20club=20open?
|
||||
From: Joe SixPack <joe@football.example.com>
|
||||
To: Suzie Q <suzie@shopping.example.net>
|
||||
Subject: Is the club open?
|
||||
Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
|
||||
Message-ID: <20030712040037.46341.5F8J@football.example.com>
|
||||
|
||||
Hi.
|
||||
|
||||
Is the club open tonight? We could watch the game there.
|
||||
|
||||
Joe.
|
||||
@@ -0,0 +1,9 @@
|
||||
This tests a DKIM signature with tags that contain "b=" but are not the b=
|
||||
tag: an unknown "xb=" tag, and a z= tag with a quoted-printable "club=20".
|
||||
The b= tag is also not the last one.
|
||||
|
||||
When verifying, only the value of the real b= tag must be removed before
|
||||
hashing the DKIM-Signature header.
|
||||
|
||||
It was signed with Mail::DKIM (Perl), using the RFC 8463 ed25519 key, and
|
||||
verified with both Mail::DKIM and dkimpy.
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Found": 1,
|
||||
"Valid": 1,
|
||||
"Results": [
|
||||
{
|
||||
"Error": "",
|
||||
"SignatureHeader": " v=1; a=ed25519-sha256; c=relaxed/relaxed;\r\n d=football.example.com; s=brisbane; t=1528637909;\r\n h=from:to:subject:date:message-id;\r\n b=xjoLeGfcxnO99aQ8Tlc/unAH+heqD8dXVjKLdvzbUr7sctU9pf/V3FRluuVyLtxiZieesO7xRWJAcThkMDIqAA==;\r\n bh=W3XNOuZiySVQVWa4kPgRp9uVKwEhAmhwKC5NysNOjhg=;\r\n xb=1;\r\n z=Subject:Is=20the=20club=20open?",
|
||||
"Domain": "football.example.com",
|
||||
"Selector": "brisbane",
|
||||
"B": "xjoLeGfcxnO99aQ8Tlc/unAH+heqD8dXVjKLdvzbUr7sctU9pf/V3FRluuVyLtxiZieesO7xRWJAcThkMDIqAA==",
|
||||
"State": "SUCCESS"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -141,10 +141,15 @@ func VerifyMessage(ctx context.Context, message string) (*VerifyResult, error) {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Regular expression that matches the "b=" tag.
|
||||
// First capture group is the "b=" part (including any whitespace up to the
|
||||
// Regular expression that matches the "b=" tag, in a (canonicalized)
|
||||
// DKIM-Signature header, including the header name.
|
||||
// It must be anchored at the beginning of a tag, so it doesn't match other
|
||||
// tags or values that contain "b=" (e.g. "xb=", or "z=...club=20...").
|
||||
// First capture group is what precedes the tag: the header name and ':' if
|
||||
// it is the first tag, or ';' otherwise.
|
||||
// Second capture group is the "b=" part (including any whitespace up to the
|
||||
// '=').
|
||||
var bTag = regexp.MustCompile(`(b[ \t\r\n]*=)[^;]+`)
|
||||
var bTag = regexp.MustCompile(`(^[^:]*:|;)([ \t\r\n]*b[ \t\r\n]*=)[^;]*`)
|
||||
|
||||
func verifySignature(ctx context.Context, sigH header,
|
||||
headers headers, body string) *OneResult {
|
||||
@@ -233,7 +238,7 @@ func verifySignature(ctx context.Context, sigH header,
|
||||
// with an empty b= tag, and without a trailing \r\n.
|
||||
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.7
|
||||
sigC := sig.cH.header(sigH)
|
||||
sigCStr := bTag.ReplaceAllString(sigC.Source, "$1")
|
||||
sigCStr := bTag.ReplaceAllString(sigC.Source, "$1$2")
|
||||
trace(ctx, "Hashing header: %q", sigCStr)
|
||||
b.Write([]byte(sigCStr))
|
||||
bSum := b.Sum(nil)
|
||||
|
||||
@@ -297,6 +297,110 @@ func TestVerifyInvalidDomain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user