Files
LambdaMail/services/protocols/internal/infrastructure/arc/verifier.go
T

136 lines
4.4 KiB
Go

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)
}
}