mirror of
https://blitiri.com.ar/repos/chasquid
synced 2026-10-29 15:02:30 +00:00
b09ef96eab
Operating on large RSA keys can be computationally expensive. Several implementations (e.g. crypto/tls, OpenSSL, rspamd) limit their size to prevent DoS via work amplification. This patch introduces that limit in our dkim library, set to 8192 bits. That's the same limit used by other implementations, like crypto/tls (which limits size of RSA keys in certificates) and rspamd (specifically for DKIM). It is also in line with the most recent DMARC.org report of key sizes in the wild (https://dmarc.org/stats/dkim/), where 8192 is already rare, and the largest one found.
231 lines
6.1 KiB
Go
231 lines
6.1 KiB
Go
package dkim
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/ed25519"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// Maximum number of TXT records we consider when looking up the public key.
|
|
// See findPublicKeys for details.
|
|
const maxKeyRecords = 3
|
|
|
|
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 the TXT records that can be parsed as keys, but only
|
|
// look at the first few. The records are attacker-controlled, and each
|
|
// key can cost a signature verification, so we need to cap them.
|
|
// This is still more lenient than the implementations above, and we
|
|
// could reconsider this in the future.
|
|
if len(values) > maxKeyRecords {
|
|
trace(ctx, "Too many TXT records for %q (%d), using the first %d",
|
|
d, len(values), maxKeyRecords)
|
|
values = values[:maxKeyRecords]
|
|
}
|
|
|
|
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.SplitSeq(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")
|
|
errRSAKeyTooLarge = errors.New("RSA public key too large")
|
|
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
|
|
}
|
|
|
|
// Enforce 8192-bit maximum.
|
|
//
|
|
// The RFC requires verifiers to handle keys of 1024 to 4096 bits, and
|
|
// does not set a maximum. But the key is attacker-controlled, and the
|
|
// cost of verifying grows quickly with its size: a key that fits in a
|
|
// TXT record can take seconds to verify.
|
|
//
|
|
// 8192 is chosen to be well above what legitimate signers use:
|
|
// - DMARC.org saw 277 keys of 8192 bits in 2025 (out of ~11.8M), and
|
|
// none larger.
|
|
// - rspamd and Go's crypto/tls (for certificates) use the same limit.
|
|
// https://datatracker.ietf.org/doc/html/rfc8301#section-3.2
|
|
if rsaPub.Size()*8 > 8192 {
|
|
return nil, errRSAKeyTooLarge
|
|
}
|
|
|
|
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
|
|
}
|