#17: implement DANE TLSA, MTA-STS policy resolution, ARC sealing and mail authentication
This commit is contained in:
1 parent
df8b809683
commit
bd0af98076
22 files changed
+3296
No files matched your search
@@ -0,0 +1,135 @@
|
||||
package arc
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// VerifyChain checks an ARC chain against the public keys of the sealing
|
||||
// domains and returns the chain validation state a downstream hop should
|
||||
// report in its own "cv=" tag.
|
||||
//
|
||||
// publicKeyFor resolves the key for a domain and selector; it is injected so
|
||||
// the caller decides how lookups happen (DNS in production, a fixed map in a
|
||||
// test).
|
||||
func VerifyChain(message []byte, publicKeyFor func(domain, selector string) (crypto.PublicKey, error)) (ChainValidation, error) {
|
||||
headers, body := splitMessage(string(message))
|
||||
|
||||
highest := 0
|
||||
for _, h := range headers {
|
||||
if strings.EqualFold(h.name, HeaderSeal) {
|
||||
if i, err := strconv.Atoi(parseTags(h.value)["i"]); err == nil && i > highest {
|
||||
highest = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if highest == 0 {
|
||||
return ChainNone, nil
|
||||
}
|
||||
if highest > maxChainInstance {
|
||||
return ChainFail, fmt.Errorf("arc: chain exceeds the maximum of %d instances", maxChainInstance)
|
||||
}
|
||||
|
||||
// RFC 8617 section 5.2: the most recent ARC-Message-Signature must
|
||||
// validate, and every ARC-Seal in the chain must validate.
|
||||
if err := verifyMessageSignature(headers, body, highest, publicKeyFor); err != nil {
|
||||
return ChainFail, err
|
||||
}
|
||||
|
||||
for instance := 1; instance <= highest; instance++ {
|
||||
if err := verifySeal(headers, instance, publicKeyFor); err != nil {
|
||||
return ChainFail, err
|
||||
}
|
||||
}
|
||||
|
||||
return ChainPass, nil
|
||||
}
|
||||
|
||||
func verifyMessageSignature(headers []header, body string, instance int, publicKeyFor func(string, string) (crypto.PublicKey, error)) error {
|
||||
value, ok := findInstanceHeader(headers, HeaderMessageSignature, instance)
|
||||
if !ok {
|
||||
return fmt.Errorf("arc: no %s for instance %d", HeaderMessageSignature, instance)
|
||||
}
|
||||
|
||||
tags := parseTags(value)
|
||||
if tags["bh"] != bodyHash(body) {
|
||||
return fmt.Errorf("arc: body hash mismatch at instance %d", instance)
|
||||
}
|
||||
|
||||
signedNames := strings.Split(tags["h"], ":")
|
||||
material := canonicalHeaderBlock(headers, signedNames)
|
||||
material += canonicalizeHeaderRelaxed(HeaderMessageSignature, stripSignature(value))
|
||||
|
||||
return verifySignature(tags, []byte(material), publicKeyFor)
|
||||
}
|
||||
|
||||
func verifySeal(headers []header, instance int, publicKeyFor func(string, string) (crypto.PublicKey, error)) error {
|
||||
value, ok := findInstanceHeader(headers, HeaderSeal, instance)
|
||||
if !ok {
|
||||
return fmt.Errorf("arc: no %s for instance %d", HeaderSeal, instance)
|
||||
}
|
||||
|
||||
var material strings.Builder
|
||||
for i := 1; i < instance; i++ {
|
||||
for _, name := range []string{HeaderAuthenticationResults, HeaderMessageSignature, HeaderSeal} {
|
||||
if previous, ok := findInstanceHeader(headers, name, i); ok {
|
||||
material.WriteString(canonicalizeHeaderRelaxed(name, previous))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if aar, ok := findInstanceHeader(headers, HeaderAuthenticationResults, instance); ok {
|
||||
material.WriteString(canonicalizeHeaderRelaxed(HeaderAuthenticationResults, aar))
|
||||
}
|
||||
if ams, ok := findInstanceHeader(headers, HeaderMessageSignature, instance); ok {
|
||||
material.WriteString(canonicalizeHeaderRelaxed(HeaderMessageSignature, ams))
|
||||
}
|
||||
material.WriteString(canonicalizeHeaderRelaxed(HeaderSeal, stripSignature(value)))
|
||||
|
||||
return verifySignature(parseTags(value), []byte(material.String()), publicKeyFor)
|
||||
}
|
||||
|
||||
// stripSignature blanks the "b=" tag, which is how the signature over a header
|
||||
// that contains itself is defined (RFC 6376 section 3.7).
|
||||
func stripSignature(value string) string {
|
||||
index := strings.LastIndex(value, "b=")
|
||||
if index < 0 {
|
||||
return value
|
||||
}
|
||||
return value[:index+2]
|
||||
}
|
||||
|
||||
func verifySignature(tags map[string]string, material []byte, publicKeyFor func(string, string) (crypto.PublicKey, error)) error {
|
||||
signature, err := base64.StdEncoding.DecodeString(tags["b"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("arc: signature is not valid base64: %w", err)
|
||||
}
|
||||
|
||||
key, err := publicKeyFor(tags["d"], tags["s"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("arc: cannot resolve key for %s/%s: %w", tags["d"], tags["s"], err)
|
||||
}
|
||||
|
||||
switch typed := key.(type) {
|
||||
case ed25519.PublicKey:
|
||||
if !ed25519.Verify(typed, material, signature) {
|
||||
return fmt.Errorf("arc: ed25519 signature does not verify for %s", tags["d"])
|
||||
}
|
||||
return nil
|
||||
case *rsa.PublicKey:
|
||||
digest := sha256.Sum256(material)
|
||||
if err := rsa.VerifyPKCS1v15(typed, crypto.SHA256, digest[:], signature); err != nil {
|
||||
return fmt.Errorf("arc: rsa signature does not verify for %s: %w", tags["d"], err)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("arc: unsupported key type %T", key)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user