git » chasquid » main » tree

[main] / internal / dkim / dns.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
}