package rfc2136 import ( "fmt" "strings" "time" "github.com/miekg/dns" ) // tsigResponseFudge is the time tolerance (seconds) embedded in // responses we sign. RFC 8945 §10 suggests 300s; we mirror that. const tsigResponseFudge = 300 // signResponseIfSigned attaches a TSIG record to resp using the // request's key name and algorithm. This causes the downstream // dns.ResponseWriter to compute and serialize the MAC at WriteMsg // time (using the secret from the server's TsigSecret map, which // setup.go populated). Per RFC 8945 §5.4.2, the response to a // TSIG-signed message MUST itself be signed if the server knows the // key — otherwise the client cannot authenticate the answer and // rejects it with "expected a TSIG or SIG(0)" (BIND nsupdate's exact // complaint). // // If the request was not TSIG-signed, this is a no-op. If the key is // not in the server's TsigSecret map (e.g. unknown key), miekg/dns // will skip signing at write time and the response goes back // unsigned — that's the correct shape for "I don't have your key." func signResponseIfSigned(resp, req *dns.Msg) { tsig := req.IsTsig() if tsig == nil { return } resp.SetTsig(tsig.Hdr.Name, tsig.Algorithm, tsigResponseFudge, time.Now().Unix()) } // checkTSIG verifies that the incoming UPDATE message is properly signed // with a TSIG key we know about. The actual signature math has already // been done by the underlying dns.Server (because setup.go registered // our keys in dnsserver.Config.TsigSecret); this function just inspects // the result and the key identity. // // Behavior matrix: // // No TSIG keys configured → updates are unauthenticated. Caller may // still allow if it deems the network safe; // we conservatively reject (REFUSED) since // the practical use case (Caddy) always // signs. // TSIG keys configured but message has no TSIG → reject. // TSIG present, key name not in our map → reject. // TSIG present, signature failed at dns.Server → reject (TsigStatus()). // All good → nil. func (p *RFC2136) checkTSIG(w dns.ResponseWriter, r *dns.Msg) error { tsig := r.IsTsig() if len(p.TSIGKeys) == 0 { return fmt.Errorf("no TSIG keys configured; refusing all UPDATEs as a safety default") } if tsig == nil { return fmt.Errorf("TSIG required but not present") } keyName := strings.ToLower(tsig.Hdr.Name) if !strings.HasSuffix(keyName, ".") { keyName += "." } key, known := p.TSIGKeys[keyName] if !known { return fmt.Errorf("unknown TSIG key %q", keyName) } // Algorithm pinning: the incoming TSIG can in theory use any // algorithm miekg/dns supports, but we only honour the one declared // in Corefile. Rejecting algorithm-downgrade attempts is a small // but important hardening — without this, an attacker who somehow // got a key could downgrade to HMAC-MD5 (which we don't even // configure but miekg/dns understands). if !strings.EqualFold(tsig.Algorithm, key.Algorithm) { return fmt.Errorf("TSIG algorithm mismatch: incoming=%s expected=%s", tsig.Algorithm, key.Algorithm) } // The underlying dns.Server verifies the TSIG MAC for us when it // has the secret in its TsigSecret map (which setup.go wires up). // A nil from TsigStatus means verification succeeded; any non-nil // error means the signature was invalid, the time was outside the // fudge window, or some other auth failure. // // H6 — Replay window: the TSIG `fudge` field (RFC 8945 §5.2) is the // allowed clock skew between client and server. miekg/dns enforces // this in TsigVerify (rejecting with ErrTime if the request's time // is too far from local time). We rely on miekg/dns's default // tolerance (currently 300s); we do not configure it ourselves. // If miekg/dns's default ever changes, our replay-window behavior // changes with it — operators should monitor upstream release notes. // A future enhancement is to make the fudge configurable via the // Corefile (e.g., `tsig-fudge 300`). if status := w.TsigStatus(); status != nil { return fmt.Errorf("TSIG verification failed for key %q: %w", keyName, status) } return nil }