202 lines
4.9 KiB
Go
202 lines
4.9 KiB
Go
package dkim
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/ed25519"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
func findPublicKeys(ctx context.Context, domain, selector string) ([]*publicKey, error) {
|
|
// Subdomain where the key lives.
|
|
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.2
|
|
d := selector + "._domainkey." + domain
|
|
values, err := lookupTXT(ctx, d)
|
|
if err != nil {
|
|
trace(ctx, "TXT lookup of %q failed: %v", d, err)
|
|
return nil, err
|
|
}
|
|
|
|
// There should be only a single record; RFC 6376 says the results are
|
|
// undefined if there are multiple TXT records.
|
|
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.2.2
|
|
//
|
|
// What other implementations do:
|
|
// - dkimpy: Use the first TXT record (whatever it is).
|
|
// - OpenDKIM: Use the first TXT record (whatever it is).
|
|
// - driusan/dkim: Use the first TXT record that can be parsed as a key.
|
|
// - go-msgauth: Reject if there are multiple records.
|
|
//
|
|
// What we do: use _all_ TXT records that can be parsed as keys. This is
|
|
// possibly too much, and we could reconsider this in the future.
|
|
|
|
pks := []*publicKey{}
|
|
for _, v := range values {
|
|
trace(ctx, "TXT record for %q: %q", d, v)
|
|
pk, err := parsePublicKey(v)
|
|
if err != nil {
|
|
trace(ctx, "Skipping: %v", err)
|
|
continue
|
|
}
|
|
trace(ctx, "Parsed public key: %s", pk)
|
|
pks = append(pks, pk)
|
|
}
|
|
|
|
return pks, nil
|
|
}
|
|
|
|
// Function to verify a signature with this public key.
|
|
type verifyFunc func(h crypto.Hash, hashed, signature []byte) error
|
|
|
|
type publicKey struct {
|
|
H []crypto.Hash
|
|
K keyType
|
|
P []byte
|
|
|
|
T []string // t= tag, representing flags.
|
|
|
|
verify verifyFunc
|
|
}
|
|
|
|
func (pk *publicKey) String() string {
|
|
return fmt.Sprintf("[%s:%.8x]", pk.K, pk.P)
|
|
}
|
|
|
|
func (pk *publicKey) Matches(kt keyType, h crypto.Hash) bool {
|
|
if pk.K != kt {
|
|
return false
|
|
}
|
|
if len(pk.H) > 0 {
|
|
return slices.Contains(pk.H, h)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (pk *publicKey) StrictDomainCheck() bool {
|
|
// t=s is set.
|
|
return slices.Contains(pk.T, "s")
|
|
}
|
|
|
|
func parsePublicKey(v string) (*publicKey, error) {
|
|
// Public key is a tag-value list.
|
|
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.1
|
|
tags, err := parseTags(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// "v" is optional, but if present it must be "DKIM1".
|
|
ver, ok := tags["v"]
|
|
if ok && ver != "DKIM1" {
|
|
return nil, fmt.Errorf("%w: %q", errInvalidVersion, ver)
|
|
}
|
|
|
|
pk := &publicKey{
|
|
// The default key type is rsa.
|
|
K: keyTypeRSA,
|
|
}
|
|
|
|
// h is a colon-separated list of hashing algorithm names.
|
|
if tags["h"] != "" {
|
|
hs := strings.Split(eatWhitespace.Replace(tags["h"]), ":")
|
|
for _, h := range hs {
|
|
x, err := hashFromString(h)
|
|
if err != nil {
|
|
// Unrecognized algorithms must be ignored.
|
|
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.1
|
|
continue
|
|
}
|
|
pk.H = append(pk.H, x)
|
|
}
|
|
}
|
|
|
|
// k is key type (may not be present, rsa is used in that case).
|
|
if tags["k"] != "" {
|
|
pk.K, err = keyTypeFromString(tags["k"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// p is public-key data, base64-encoded, and whitespace in it must be
|
|
// ignored. Required.
|
|
p, err := base64.StdEncoding.DecodeString(
|
|
eatWhitespace.Replace(tags["p"]))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error decoding p=: %w", err)
|
|
}
|
|
pk.P = p
|
|
|
|
switch pk.K {
|
|
case keyTypeRSA:
|
|
pk.verify, err = parseRSAPublicKey(p)
|
|
case keyTypeEd25519:
|
|
pk.verify, err = parseEd25519PublicKey(p)
|
|
}
|
|
|
|
// t is a colon-separated list of flags.
|
|
if t := eatWhitespace.Replace(tags["t"]); t != "" {
|
|
pk.T = strings.Split(t, ":")
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pk, nil
|
|
}
|
|
|
|
var (
|
|
errInvalidRSAPublicKey = errors.New("invalid RSA public key")
|
|
errNotRSAPublicKey = errors.New("not an RSA public key")
|
|
errRSAKeyTooSmall = errors.New("RSA public key too small")
|
|
errInvalidEd25519Key = errors.New("invalid Ed25519 public key")
|
|
)
|
|
|
|
func parseRSAPublicKey(p []byte) (verifyFunc, error) {
|
|
// Either PKCS#1 or SubjectPublicKeyInfo.
|
|
// See https://www.rfc-editor.org/errata/eid3017.
|
|
pub, err := x509.ParsePKIXPublicKey(p)
|
|
if err != nil {
|
|
pub, err = x509.ParsePKCS1PublicKey(p)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %w", errInvalidRSAPublicKey, err)
|
|
}
|
|
|
|
rsaPub, ok := pub.(*rsa.PublicKey)
|
|
if !ok {
|
|
return nil, errNotRSAPublicKey
|
|
}
|
|
|
|
// Enforce 1024-bit minimum.
|
|
// https://datatracker.ietf.org/doc/html/rfc8301#section-3.2
|
|
if rsaPub.Size()*8 < 1024 {
|
|
return nil, errRSAKeyTooSmall
|
|
}
|
|
|
|
return func(h crypto.Hash, hashed, signature []byte) error {
|
|
return rsa.VerifyPKCS1v15(rsaPub, h, hashed, signature)
|
|
}, nil
|
|
}
|
|
|
|
func parseEd25519PublicKey(p []byte) (verifyFunc, error) {
|
|
// https: //datatracker.ietf.org/doc/html/rfc8463
|
|
if len(p) != ed25519.PublicKeySize {
|
|
return nil, errInvalidEd25519Key
|
|
}
|
|
|
|
pub := ed25519.PublicKey(p)
|
|
return func(h crypto.Hash, hashed, signature []byte) error {
|
|
if ed25519.Verify(pub, hashed, signature) {
|
|
return nil
|
|
}
|
|
return errors.New("signature verification failed")
|
|
}, nil
|
|
}
|