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 }