coredns-rfc2136/config.go
Ryan Malloy eba6313ec0 Phase 1.2: wire parser → typed config + 13 unit tests
The Corefile parser now fully populates typed fields on RFC2136 instead
of just recognising directives. Validation happens at parse-time so
configuration errors fail loud at CoreDNS startup rather than silent at
request time.

Added:
- config.go: tsigKey type, TSIG algorithm allowlist (rejects HMAC-MD5
  deliberately), base64 secret decoder with 8-byte minimum length check,
  canonical-key-name normalisation (lowercase + trailing dot).
- plugin.go: RFC2136 struct now carries TSIGKeys map, TTL uint32,
  PersistPath string. DefaultTTL=60.
- setup.go: parse() validates and stores tsig-key/ttl/persist directives.
  Duplicate key names rejected. Multiple TSIG keys allowed (for rotation).
  At-least-one-zone is enforced.
- setup_test.go: 13 table-driven cases (5 happy + 8 error paths) using
  caddy.NewTestController. All pass.

ServeDNS still passes through — UPDATE handling lands in Phase 1.4.

Module path: git.supported.systems/rsp2k/coredns-rfc2136
2026-05-21 10:31:22 -06:00

79 lines
2.7 KiB
Go

package rfc2136
import (
"encoding/base64"
"fmt"
"strings"
"github.com/miekg/dns"
)
// tsigKey is a single pre-shared TSIG credential. The plugin holds one
// per declared `tsig-key` directive, keyed in RFC2136.TSIGKeys by the
// canonical (lowercased, trailing-dot) key name.
//
// Phase 1.2 stores the algorithm and decoded secret. Phase 1.4 will
// use them for actual signature verification via dns.TsigSecret +
// dns.Msg.IsTsig() / dns.TsigVerify().
type tsigKey struct {
// Algorithm is the canonical miekg/dns algorithm identifier, e.g.
// dns.HmacSHA256 ("hmac-sha256."). Stored in dns-library form so
// it's directly usable when wiring TSIG verification later.
Algorithm string
// Secret is the base64-decoded raw bytes of the shared secret.
// Decoding at parse time means a malformed secret fails Corefile
// load (loud), not mid-query (silent).
Secret []byte
}
// supportedTSIGAlgorithms maps user-facing algorithm names (as written
// in Corefile) to the canonical names recognised by miekg/dns. Only
// SHA-family entries are included — HMAC-MD5 is deprecated and not
// supported here on principle.
var supportedTSIGAlgorithms = map[string]string{
"hmac-sha1": dns.HmacSHA1,
"hmac-sha224": dns.HmacSHA224,
"hmac-sha256": dns.HmacSHA256,
"hmac-sha384": dns.HmacSHA384,
"hmac-sha512": dns.HmacSHA512,
}
// parseTSIGAlgorithm validates and canonicalises the algorithm name
// from a `tsig-key` directive. Returns the miekg/dns canonical form
// (which has a trailing dot per DNS-name convention).
func parseTSIGAlgorithm(s string) (string, error) {
canon, ok := supportedTSIGAlgorithms[strings.ToLower(s)]
if !ok {
return "", fmt.Errorf("unsupported TSIG algorithm %q (supported: hmac-sha1, hmac-sha224, hmac-sha256, hmac-sha384, hmac-sha512)", s)
}
return canon, nil
}
// decodeTSIGSecret validates the base64-encoded TSIG secret from the
// Corefile and returns the raw bytes. Rejects empty secrets and any
// secret shorter than 8 bytes (well under the HMAC algorithm's block
// size for any supported algorithm — anything that short is almost
// certainly a typo, not deliberate).
func decodeTSIGSecret(s string) ([]byte, error) {
raw, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, fmt.Errorf("invalid base64 in TSIG secret: %w", err)
}
if len(raw) < 8 {
return nil, fmt.Errorf("TSIG secret too short (%d bytes after base64-decode; need >= 8)", len(raw))
}
return raw, nil
}
// canonicalKeyName normalises a TSIG key name to lowercase + trailing
// dot. miekg/dns expects the dot-terminated FQDN form internally, so
// we coerce here once at parse-time rather than every lookup.
func canonicalKeyName(name string) string {
name = strings.ToLower(name)
if !strings.HasSuffix(name, ".") {
name += "."
}
return name
}