Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions stevea/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
go.work.sum

# env file
.env

# Editor/IDE
# .idea/
# .vscode/
/cmd/dkim2sign/dkim2sign
/.idea
47 changes: 47 additions & 0 deletions stevea/INTEROP-NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## 1. Email address angle brackets

It's not clear to me whether we expect the email addresses stored
in mf= and rt=, and the same addresses passed in via API, to be
surrounded with angle brackets or not.

## 2. RSA public key type

We've inherited the [uncertainty about public key format](https://www.rfc-editor.org/errata/eid3017)
for RSA keys from DKIM. That means we need to attempt to parse it
in both formats and use the one that works.

## 3. Retrieving i= / m= for error messages

If a Dkim2-Signature or Message-Instance header cannot be parsed then
we need to return an error. That error should contain the i=/m= to
identify which header, but we weren't able to parse it. I've added
fallback parsing to just look for the identifier tag when parsing
fails.

## 4. Error messages missing

spec-01 includes a set of standard error messages, but they don't
cover some error cases:

- Duplicate i=/m= in header
- Header doesn't contain i=/m=

## 5. Hash comparisons

Malicious input could cause non-canonical base64 encoded hashes,
so comparing hashes as base64 encoded text isn't safe. I'm
erroring out for non-canonical encoding (and comparing the
decoded binary rather than the text).

## 6. Identifying s=

During verification we need to modify the s= tag in the
most recent Dkim2-Signature to blank out the signature.
To do that we need to find the s= tag.

A simple search for "s=" may fail, as "s=" could appear
in base64 encoded fields.

Searching for a regex `;\s*s=` looks more reasonable, as
semicolons don't appear in base64. But if s= is the first
tag in the header that'd fail.
24 changes: 24 additions & 0 deletions stevea/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
BSD 2-Clause License

Copyright (c) 2025, Turscar Ltd

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15 changes: 15 additions & 0 deletions stevea/NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## Error Messages

We don't have a "duplicate" i=/m= error. Using Syntax Error for now.

We don't have "these headers are so mangled I can't get i= or m=".

Having a "in which tag" for syntax error would be nice.

Added "FAIL: DKIM2-Signature i=<x> validation failed"

## RSA

https://www.rfc-editor.org/errata/eid3017

Min RSA key length
22 changes: 22 additions & 0 deletions stevea/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# dkim2
DKIM2 library and tools

## Tests

Run `go test . -count=1 -v` to run library test and interop tests
(parsing expected results from python and brong).

Our signed messages are in stevea/testdata/golden/expected.

## Status

The library seems to generate and parse messages correctly.

The CLI tools are not ready for use.

## Missing

Validation of MAIL FROM / RCPT TO chains.

Results of message-instance recipes that return a "we
can't reconstruct this" result.
89 changes: 89 additions & 0 deletions stevea/cf_python_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package dkim2

import (
"context"
"os"
"path/filepath"
"testing"
)

func TestPython_Verify(t *testing.T) {
tests := map[string]struct {
MailFrom string
RcptTo []string
}{
"simple-ed25519": {
MailFrom: "sender@test1.dkim2.com",
RcptTo: []string{"recipient@example.com"},
},
"simple-rsa1024": {
MailFrom: "sender@test1.dkim2.com",
RcptTo: []string{"recipient@example.com"},
},
"simple-rsa2048": {},
"simple-sel2": {},
"simple-sel3": {},
"dsn-ed25519": {},
"dupheaders-ed25519": {},
"emptybody-ed25519": {},
"multiheader-ed25519": {},
"trailingblank-ed25519": {},
"multirecipient-ed25519": {},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
f, err := os.Open(filepath.Join("testdata", "interop", "python", "expected", name+".eml"))
if err != nil {
t.Fatalf("Failed to open file: %v", err)
}
defer func(f *os.File) {
_ = f.Close()
}(f)
result := Verify(context.Background(), f,
VerifyOptions{
IgnoreTimestamp: true,
Resolver: NewTestResolver(),
MailFrom: tc.MailFrom,
RcptTo: tc.RcptTo,
})
if result.State() != StatePass {
t.Fatalf("Failed to verify message: %v", result.AuthenticationResult())
}
})
}
}

func TestPython_VerifyAll(t *testing.T) {
tests := map[string]struct {
MailFrom string
RcptTo []string
}{
"multihop-3hop-dup-headers": {},
"multihop-body-footer": {},
"multihop-dup-headers": {},
"multihop-header-add": {},
"multihop-header-replace": {},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
f, err := os.Open(filepath.Join("testdata", "interop", "python", "expected", name+".eml"))
if err != nil {
t.Fatalf("Failed to open file: %v", err)
}
defer func(f *os.File) {
_ = f.Close()
}(f)
result := VerifyAll(context.Background(), f,
VerifyOptions{
IgnoreTimestamp: true,
Resolver: NewTestResolver(),
MailFrom: tc.MailFrom,
RcptTo: tc.RcptTo,
})
if result.State() != StatePass {
t.Fatalf("Failed to verify message: %v", result.AuthenticationResult())
}
})
}
}
Loading