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
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular expression that matches the "b=" tag.
|
// Regular expression that matches the "b=" tag, in a (canonicalized)
|
||||||
// First capture group is the "b=" part (including any whitespace up to the
|
// 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,
|
func verifySignature(ctx context.Context, sigH header,
|
||||||
headers headers, body string) *OneResult {
|
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.
|
// with an empty b= tag, and without a trailing \r\n.
|
||||||
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.7
|
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.7
|
||||||
sigC := sig.cH.header(sigH)
|
sigC := sig.cH.header(sigH)
|
||||||
sigCStr := bTag.ReplaceAllString(sigC.Source, "$1")
|
sigCStr := bTag.ReplaceAllString(sigC.Source, "$1$2")
|
||||||
trace(ctx, "Hashing header: %q", sigCStr)
|
trace(ctx, "Hashing header: %q", sigCStr)
|
||||||
b.Write([]byte(sigCStr))
|
b.Write([]byte(sigCStr))
|
||||||
bSum := b.Sum(nil)
|
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) {
|
func TestHeadersToInclude(t *testing.T) {
|
||||||
// Test that headersToInclude returns the expected headers.
|
// Test that headersToInclude returns the expected headers.
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user