Repository: miekg/dns
Pull request #1201 (2020-12-16 11:26:10)

Add GSS-TSIG support
Here's another go at adding GSS-TSIG support.

Inspired by how crypto/ssh handles this I've defined a simple interface for an external GSSAPI implementation and added this to the `Client` and `Conn` structs. I've added two new internal `tsigGenerateGSSAPI()` and `tsigVerifyGSSAPI()` functions that are passed this interface and then refactored the original exported `TsigGenerate()` and `TsigVerify()` functions to use these, passing `nil` for the GSSAPI interface, so existing code using those will behave the same as before.

I've left the new functions unexported and I haven't touched the `Server` or `Transfer` structs yet. I've not added any tests either but that should be easy enough if you approve.

tmthrgd: This idea looks reasonable enough (though I won’t review the actual implementation just yet), it appears entirely backwards compatible. Can you show an example of how the interface might be implemented?
bodgit: To begin with you need to establish a GSSAPI context with the target server.

You're basically doing what is documented in [RFC 2743](https://tools.ietf.org/html/rfc2743#page-42) with `GSS_Init_sec_context()`. You first call it passing `GSS_C_NO_CONTEXT` for the input_context_handle with a NULL input_token. The result of this is an output_context_handle and an output_token the latter of which needs to be sent to the target server.

To do this you send a TKEY request with the target server, the TKEY name is generated with something like this:
```golang
dns.Fqdn(fmt.Sprintf("%d.sig-%s", rng.Int31(), host)) // this is what nsupdate uses
```
and the request is created something like this:
```golang
msg := &dns.Msg{
	MsgHdr: dns.MsgHdr{
		RecursionDesired: false,
	},
	Question: make([]dns.Question, 1),
	Extra:    make([]dns.RR, 1),
}

msg.Question[0] = dns.Question{
	Name:   keyname,
	Qtype:  dns.TypeTKEY,
	Qclass: dns.ClassANY,
}

msg.Id = dns.Id()

msg.Extra[0] = &dns.TKEY{
	Hdr: dns.RR_Header{
		Name:   keyname,
		Rrtype: dns.TypeTKEY,
		Class:  dns.ClassANY,
		Ttl:    0,
	},
	Algorithm:  "gss-tsig.",
	Mode:       3,
	Inception:  inception,
	Expiration: expiration,
	KeySize:    uint16(len(output_token)),
	Key:        hex.EncodeToString(output_token),
}
```
That is sent to the server which responds (hopefully) with a TKEY answer. The key payload is now the input_token which is then used to call `GSS_Init_sec_context()` again and hopefully you get `GSS_S_COMPLETE`. If you get `GSS_S_CONTINUE_NEEDED` you repeat the exchange.

At this point you have a negotiated context which you should store using the TKEY name you created. That TKEY name is the TSIG secret name however there's no secret to speak of, the context is the secret here.

So an implementation of the interface would look something like this:
```
type context struct {
	client *client.Client
	key    types.EncryptionKey
}

type Example struct {
	ctx map[string]context 
}

...

func (e *Example) Generate(msg []byte, tsig *dns.TSIG) ([]byte, error) {
	if dns.CanonicalName(tsig.Algorithm) != dns.GSS {
		return nil, dns.ErrKeyAlg
	}

	ctx, ok := e.ctx[tsig.Hdr.Name] // this is where you retrieve your negotiated context
	if !ok {
		return nil, dns.ErrSecret
	}

	token := gssapi.MICToken{
		Flags:     gssapi.MICTokenFlagAcceptorSubkey,
		SndSeqNum: 0,
		Payload:   msg,
	}

	if err := token.SetChecksum(ctx.key, keyusage.GSSAPI_INITIATOR_SIGN); err != nil {
		return nil, err
	}

	b, err := token.Marshal()
	if err != nil {
		return nil, err
	}

	return b, nil
}

func (e *Example) Verify(stripped []byte, tsig *dns.TSIG) error {
	if dns.CanonicalName(tsig.Algorithm) != dns.GSS {
		return dns.ErrKeyAlg
	}

	ctx, ok := e.ctx[tsig.Hdr.Name] // retrieve the negotiated context
	if !ok {
		return dns.ErrSecret
	}

	mac, err := hex.DecodeString(tsig.MAC)
	if err != nil {
		return err
	}

	var token gssapi.MICToken
	err = token.Unmarshal(mac, true)
	if err != nil {
		return err
	}
	token.Payload = stripped

	// This is the actual verification bit
	_, err = token.Verify(ctx.key, keyusage.GSSAPI_ACCEPTOR_SIGN)
	if err != nil {
		return err
	}

	return nil
}
```
That example is based on using github.com/jcmturner/gokrb5 for the GSSAPI implementation, on Windows you would use github.com/alexbrainman/sspi instead which has a similar API but uses SSPI.

You would then use it something like:
```golang
host := "ns.example.com"

e := new(Example)
keyname := e.NegotiateTKEYRecord(host) // this does the TKEY exchange as documented above

client := new(dns.Client)
client.Net = "tcp"
client.GSSAPI = e
client.TsigSecret = map[string]string{keyname: ""} // doesn't matter what the secret is set to

// Use the DNS client as normal
msg := new(dns.Msg)
msg.SetUpdate(dns.Fqdn("example.com"))

insert, err := dns.NewRR("test.example.com. 300 A 192.0.2.1")
if err != nil {
        panic(err)
}
msg.Insert([]dns.RR{insert})

msg.SetTsig(keyname, dns.GSS, 300, time.Now().Unix())

rr, _, err := client.Exchange(msg, net.JoinHostPort(host, "53"))
if err != nil {
        panic(err)
}

if rr.Rcode != dns.RcodeSuccess {
        fmt.Printf("DNS error: %s (%d)\n", dns.RcodeToString[rr.Rcode], rr.Rcode)
}
```
There's probably a bit more housekeeping but that's essentially it.
bodgit: The server side would be similar however you would have to instead respond to any TKEY requests by calling `GSS_Accept_sec_context()` as per the RFC but once you've done that the TSIG generate and verify code would be similar, perhaps just swapping the key usage so you would generate the response signatures using `keyusage.GSSAPI_ACCEPTOR_SIGN` and verify using `keyusage.GSSAPI_INITIATOR_SIGN` continuing the gokrb5 example.
tmthrgd: That doesn’t seem too unreasonable and likely useable in practice. Is GSS-TSIG the only TSIG-extension-type-thing out there or are there others?
bodgit: I've not seen any other mechanisms other than GSS-TSIG, apart from the standard HMAC methods. Of course GSS itself stands for Generic Security Service so it is potentially a transport for any number of authentication methods although realistically it is just Kerberos.

Patch:
diff --git a/client.go b/client.go
index f580205e..aa2c49d3 100644
--- a/client.go
+++ b/client.go
@@ -23,6 +23,7 @@ type Conn struct {
 	net.Conn                         // a net.Conn holding the connection
 	UDPSize        uint16            // minimum receive buffer for UDP messages
 	TsigSecret     map[string]string // secret(s) for Tsig map[<zonename>]<base64 secret>, zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2)
+	TsigProvider   TsigProvider      // An implementation of the TsigProvider interface. If defined it replaces TsigSecret and is used for all TSIG operations.
 	tsigRequestMAC string
 }
 
@@ -40,6 +41,7 @@ type Client struct {
 	ReadTimeout    time.Duration     // net.Conn.SetReadTimeout value for connections, defaults to 2 seconds - overridden by Timeout when that value is non-zero
 	WriteTimeout   time.Duration     // net.Conn.SetWriteTimeout value for connections, defaults to 2 seconds - overridden by Timeout when that value is non-zero
 	TsigSecret     map[string]string // secret(s) for Tsig map[<zonename>]<base64 secret>, zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2)
+	TsigProvider   TsigProvider      // An implementation of the TsigProvider interface. If defined it replaces TsigSecret and is used for all TSIG operations.
 	SingleInflight bool              // if true suppress multiple outstanding queries for the same Qname, Qtype and Qclass
 	group          singleflight
 }
@@ -175,7 +177,7 @@ func (c *Client) exchange(m *Msg, co *Conn) (r *Msg, rtt time.Duration, err erro
 		co.UDPSize = c.UDPSize
 	}
 
-	co.TsigSecret = c.TsigSecret
+	co.TsigSecret, co.TsigProvider = c.TsigSecret, c.TsigProvider
 	t := time.Now()
 	// write with the appropriate write timeout
 	co.SetWriteDeadline(t.Add(c.getTimeoutForRequest(c.writeTimeout())))
@@ -222,11 +224,15 @@ func (co *Conn) ReadMsg() (*Msg, error) {
 		return m, err
 	}
 	if t := m.IsTsig(); t != nil {
-		if _, ok := co.TsigSecret[t.Hdr.Name]; !ok {
-			return m, ErrSecret
+		if co.TsigProvider != nil {
+			err = tsigVerifyProvider(p, co.TsigProvider, co.tsigRequestMAC, false)
+		} else {
+			if _, ok := co.TsigSecret[t.Hdr.Name]; !ok {
+				return m, ErrSecret
+			}
+			// Need to work on the original message p, as that was used to calculate the tsig.
+			err = TsigVerify(p, co.TsigSecret[t.Hdr.Name], co.tsigRequestMAC, false)
 		}
-		// Need to work on the original message p, as that was used to calculate the tsig.
-		err = TsigVerify(p, co.TsigSecret[t.Hdr.Name], co.tsigRequestMAC, false)
 	}
 	return m, err
 }
@@ -304,10 +310,14 @@ func (co *Conn) WriteMsg(m *Msg) (err error) {
 	var out []byte
 	if t := m.IsTsig(); t != nil {
 		mac := ""
-		if _, ok := co.TsigSecret[t.Hdr.Name]; !ok {
-			return ErrSecret
+		if co.TsigProvider != nil {
+			out, mac, err = tsigGenerateProvider(m, co.TsigProvider, co.tsigRequestMAC, false)
+		} else {
+			if _, ok := co.TsigSecret[t.Hdr.Name]; !ok {
+				return ErrSecret
+			}
+			out, mac, err = TsigGenerate(m, co.TsigSecret[t.Hdr.Name], co.tsigRequestMAC, false)
 		}
-		out, mac, err = TsigGenerate(m, co.TsigSecret[t.Hdr.Name], co.tsigRequestMAC, false)
 		// Set for the next read, although only used in zone transfers
 		co.tsigRequestMAC = mac
 	} else {
diff --git a/doc.go b/doc.go
index 6861de77..f7629ec3 100644
--- a/doc.go
+++ b/doc.go
@@ -194,6 +194,30 @@ request an AXFR for miek.nl. with TSIG key named "axfr." and secret
 You can now read the records from the transfer as they come in. Each envelope
 is checked with TSIG. If something is not correct an error is returned.
 
+A custom TSIG implementation can be used. This requires additional code to
+perform any session establishment and signature generation/verification. The
+client must be configured with an implementation of the TsigProvider interface:
+
+	type Provider struct{}
+
+	func (*Provider) Generate(msg []byte, tsig *dns.TSIG) ([]byte, error) {
+		// Use tsig.Hdr.Name and tsig.Algorithm in your code to
+		// generate the MAC using msg as the payload.
+	}
+
+	func (*Provider) Verify(msg []byte, tsig *dns.TSIG) error {
+		// Use tsig.Hdr.Name and tsig.Algorithm in your code to verify
+		// that msg matches the value in tsig.MAC.
+	}
+
+	c := new(dns.Client)
+	c.TsigProvider = new(Provider)
+	m := new(dns.Msg)
+	m.SetQuestion("miek.nl.", dns.TypeMX)
+	m.SetTsig(keyname, dns.HmacSHA1, 300, time.Now().Unix())
+	...
+	// TSIG RR is calculated by calling your Generate method
+
 Basic use pattern validating and replying to a message that has TSIG set.
 
 	server := &dns.Server{Addr: ":53", Net: "udp"}
diff --git a/svcb.go b/svcb.go
index f44dc67d..1373fe21 100644
--- a/svcb.go
+++ b/svcb.go
@@ -204,7 +204,7 @@ type SVCB struct {
 	Hdr      RR_Header
 	Priority uint16
 	Target   string         `dns:"domain-name"`
-	Value    []SVCBKeyValue `dns:"pairs"` // Value must be empty if Priority is non-zero.
+	Value    []SVCBKeyValue `dns:"pairs"` // Value must be empty if Priority is zero.
 }
 
 // HTTPS RR. Everything valid for SVCB applies to HTTPS as well.
diff --git a/tsig.go b/tsig.go
index 59904dd6..5b52e1c4 100644
--- a/tsig.go
+++ b/tsig.go
@@ -24,6 +24,56 @@ const (
 	HmacMD5 = "hmac-md5.sig-alg.reg.int." // Deprecated: HmacMD5 is no longer supported.
 )
 
+// TsigProvider provides the API to plug-in a custom TSIG implementation.
+type TsigProvider interface {
+	// Generate is passed the DNS message to be signed and the partial TSIG RR. It returns the signature and nil, otherwise an error.
+	Generate(msg []byte, t *TSIG) ([]byte, error)
+	// Verify is passed the DNS message to be verified and the TSIG RR. If the signature is valid it will return nil, otherwise an error.
+	Verify(msg []byte, t *TSIG) error
+}
+
+type tsigHMACProvider string
+
+func (key tsigHMACProvider) Generate(msg []byte, t *TSIG) ([]byte, error) {
+	// If we barf here, the caller is to blame
+	rawsecret, err := fromBase64([]byte(key))
+	if err != nil {
+		return nil, err
+	}
+	var h hash.Hash
+	switch CanonicalName(t.Algorithm) {
+	case HmacSHA1:
+		h = hmac.New(sha1.New, rawsecret)
+	case HmacSHA224:
+		h = hmac.New(sha256.New224, rawsecret)
+	case HmacSHA256:
+		h = hmac.New(sha256.New, rawsecret)
+	case HmacSHA384:
+		h = hmac.New(sha512.New384, rawsecret)
+	case HmacSHA512:
+		h = hmac.New(sha512.New, rawsecret)
+	default:
+		return nil, ErrKeyAlg
+	}
+	h.Write(msg)
+	return h.Sum(nil), nil
+}
+
+func (key tsigHMACProvider) Verify(msg []byte, t *TSIG) error {
+	b, err := key.Generate(msg, t)
+	if err != nil {
+		return err
+	}
+	mac, err := hex.DecodeString(t.MAC)
+	if err != nil {
+		return err
+	}
+	if !hmac.Equal(b, mac) {
+		return ErrSig
+	}
+	return nil
+}
+
 // TSIG is the RR the holds the transaction signature of a message.
 // See RFC 2845 and RFC 4635.
 type TSIG struct {
@@ -98,14 +148,13 @@ type timerWireFmt struct {
 // timersOnly is false.
 // If something goes wrong an error is returned, otherwise it is nil.
 func TsigGenerate(m *Msg, secret, requestMAC string, timersOnly bool) ([]byte, string, error) {
+	return tsigGenerateProvider(m, tsigHMACProvider(secret), requestMAC, timersOnly)
+}
+
+func tsigGenerateProvider(m *Msg, provider TsigProvider, requestMAC string, timersOnly bool) ([]byte, string, error) {
 	if m.IsTsig() == nil {
 		panic("dns: TSIG not last RR in additional")
 	}
-	// If we barf here, the caller is to blame
-	rawsecret, err := fromBase64([]byte(secret))
-	if err != nil {
-		return nil, "", err
-	}
 
 	rr := m.Extra[len(m.Extra)-1].(*TSIG)
 	m.Extra = m.Extra[0 : len(m.Extra)-1] // kill the TSIG from the msg
@@ -119,25 +168,13 @@ func TsigGenerate(m *Msg, secret, requestMAC string, timersOnly bool) ([]byte, s
 	}
 
 	t := new(TSIG)
-	var h hash.Hash
-	switch CanonicalName(rr.Algorithm) {
-	case HmacSHA1:
-		h = hmac.New(sha1.New, rawsecret)
-	case HmacSHA224:
-		h = hmac.New(sha256.New224, rawsecret)
-	case HmacSHA256:
-		h = hmac.New(sha256.New, rawsecret)
-	case HmacSHA384:
-		h = hmac.New(sha512.New384, rawsecret)
-	case HmacSHA512:
-		h = hmac.New(sha512.New, rawsecret)
-	default:
-		return nil, "", ErrKeyAlg
-	}
-	h.Write(buf)
 	// Copy all TSIG fields except MAC and its size, which are filled using the computed digest.
 	*t = *rr
-	t.MAC = hex.EncodeToString(h.Sum(nil))
+	mac, err := provider.Generate(buf, rr)
+	if err != nil {
+		return nil, "", err
+	}
+	t.MAC = hex.EncodeToString(mac)
 	t.MACSize = uint16(len(t.MAC) / 2) // Size is half!
 
 	tbuf := make([]byte, Len(t))
@@ -156,49 +193,28 @@ func TsigGenerate(m *Msg, secret, requestMAC string, timersOnly bool) ([]byte, s
 // If the signature does not validate err contains the
 // error, otherwise it is nil.
 func TsigVerify(msg []byte, secret, requestMAC string, timersOnly bool) error {
-	return tsigVerify(msg, secret, requestMAC, timersOnly, uint64(time.Now().Unix()))
+	return tsigVerify(msg, tsigHMACProvider(secret), requestMAC, timersOnly, uint64(time.Now().Unix()))
+}
+
+func tsigVerifyProvider(msg []byte, provider TsigProvider, requestMAC string, timersOnly bool) error {
+	return tsigVerify(msg, provider, requestMAC, timersOnly, uint64(time.Now().Unix()))
 }
 
 // actual implementation of TsigVerify, taking the current time ('now') as a parameter for the convenience of tests.
-func tsigVerify(msg []byte, secret, requestMAC string, timersOnly bool, now uint64) error {
-	rawsecret, err := fromBase64([]byte(secret))
-	if err != nil {
-		return err
-	}
+func tsigVerify(msg []byte, provider TsigProvider, requestMAC string, timersOnly bool, now uint64) error {
 	// Strip the TSIG from the incoming msg
 	stripped, tsig, err := stripTsig(msg)
 	if err != nil {
 		return err
 	}
 
-	msgMAC, err := hex.DecodeString(tsig.MAC)
-	if err != nil {
-		return err
-	}
-
 	buf, err := tsigBuffer(stripped, tsig, requestMAC, timersOnly)
 	if err != nil {
 		return err
 	}
 
-	var h hash.Hash
-	switch CanonicalName(tsig.Algorithm) {
-	case HmacSHA1:
-		h = hmac.New(sha1.New, rawsecret)
-	case HmacSHA224:
-		h = hmac.New(sha256.New224, rawsecret)
-	case HmacSHA256:
-		h = hmac.New(sha256.New, rawsecret)
-	case HmacSHA384:
-		h = hmac.New(sha512.New384, rawsecret)
-	case HmacSHA512:
-		h = hmac.New(sha512.New, rawsecret)
-	default:
-		return ErrKeyAlg
-	}
-	h.Write(buf)
-	if !hmac.Equal(h.Sum(nil), msgMAC) {
-		return ErrSig
+	if err := provider.Verify(buf, tsig); err != nil {
+		return err
 	}
 
 	// Fudge factor works both ways. A message can arrive before it was signed because


Test patch:
diff --git a/tsig_test.go b/tsig_test.go
index 81bb1796..e3f06566 100644
--- a/tsig_test.go
+++ b/tsig_test.go
@@ -3,6 +3,7 @@ package dns
 import (
 	"encoding/binary"
 	"encoding/hex"
+	"errors"
 	"fmt"
 	"strings"
 	"testing"
@@ -79,23 +80,23 @@ func TestTsigErrors(t *testing.T) {
 	}
 
 	// the signature is valid but 'time signed' is too far from the "current time".
-	if err := tsigVerify(buildMsgData(timeSigned), testSecret, "", false, timeSigned+301); err != ErrTime {
+	if err := tsigVerify(buildMsgData(timeSigned), tsigHMACProvider(testSecret), "", false, timeSigned+301); err != ErrTime {
 		t.Fatalf("expected an error '%v' but got '%v'", ErrTime, err)
 	}
-	if err := tsigVerify(buildMsgData(timeSigned), testSecret, "", false, timeSigned-301); err != ErrTime {
+	if err := tsigVerify(buildMsgData(timeSigned), tsigHMACProvider(testSecret), "", false, timeSigned-301); err != ErrTime {
 		t.Fatalf("expected an error '%v' but got '%v'", ErrTime, err)
 	}
 
 	// the signature is invalid and 'time signed' is too far.
 	// the signature should be checked first, so we should see ErrSig.
-	if err := tsigVerify(buildMsgData(timeSigned+301), testSecret, "", false, timeSigned); err != ErrSig {
+	if err := tsigVerify(buildMsgData(timeSigned+301), tsigHMACProvider(testSecret), "", false, timeSigned); err != ErrSig {
 		t.Fatalf("expected an error '%v' but got '%v'", ErrSig, err)
 	}
 
 	// tweak the algorithm name in the wire data, resulting in the "unknown algorithm" error.
 	msgData := buildMsgData(timeSigned)
 	copy(msgData[67:], "bogus")
-	if err := tsigVerify(msgData, testSecret, "", false, timeSigned); err != ErrKeyAlg {
+	if err := tsigVerify(msgData, tsigHMACProvider(testSecret), "", false, timeSigned); err != ErrKeyAlg {
 		t.Fatalf("expected an error '%v' but got '%v'", ErrKeyAlg, err)
 	}
 
@@ -104,7 +105,7 @@ func TestTsigErrors(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if err := tsigVerify(msgData, testSecret, "", false, timeSigned); err != ErrNoSig {
+	if err := tsigVerify(msgData, tsigHMACProvider(testSecret), "", false, timeSigned); err != ErrNoSig {
 		t.Fatalf("expected an error '%v' but got '%v'", ErrNoSig, err)
 	}
 
@@ -120,7 +121,7 @@ func TestTsigErrors(t *testing.T) {
 	if msgData, err = msg.Pack(); err != nil {
 		t.Fatal(err)
 	}
-	err = tsigVerify(msgData, testSecret, "", false, timeSigned)
+	err = tsigVerify(msgData, tsigHMACProvider(testSecret), "", false, timeSigned)
 	if err == nil || !strings.Contains(err.Error(), "overflow") {
 		t.Errorf("expected error to contain %q, but got %v", "overflow", err)
 	}
@@ -231,9 +232,122 @@ func TestTSIGHMAC224And384(t *testing.T) {
 			if mac != tc.expectedMAC {
 				t.Fatalf("MAC doesn't match: expected '%s' but got '%s'", tc.expectedMAC, mac)
 			}
-			if err = tsigVerify(msgData, tc.secret, "", false, timeSigned); err != nil {
+			if err = tsigVerify(msgData, tsigHMACProvider(tc.secret), "", false, timeSigned); err != nil {
 				t.Error(err)
 			}
 		})
 	}
 }
+
+const testGoodKeyName = "goodkey."
+
+var (
+	testErrBadKey = errors.New("this is an intentional error")
+	testGoodMAC   = []byte{0, 1, 2, 3}
+)
+
+// testProvider always generates the same MAC and only accepts the one signature
+type testProvider struct {
+	GenerateAllKeys bool
+}
+
+func (provider *testProvider) Generate(_ []byte, t *TSIG) ([]byte, error) {
+	if t.Hdr.Name == testGoodKeyName || provider.GenerateAllKeys {
+		return testGoodMAC, nil
+	}
+	return nil, testErrBadKey
+}
+
+func (*testProvider) Verify(_ []byte, t *TSIG) error {
+	if t.Hdr.Name == testGoodKeyName {
+		return nil
+	}
+	return testErrBadKey
+}
+
+func TestTsigGenerateProvider(t *testing.T) {
+	tables := []struct {
+		keyname string
+		mac     []byte
+		err     error
+	}{
+		{
+			testGoodKeyName,
+			testGoodMAC,
+			nil,
+		},
+		{
+			"badkey.",
+			nil,
+			testErrBadKey,
+		},
+	}
+
+	for _, table := range tables {
+		t.Run(table.keyname, func(t *testing.T) {
+			tsig := TSIG{
+				Hdr:        RR_Header{Name: table.keyname, Rrtype: TypeTSIG, Class: ClassANY, Ttl: 0},
+				Algorithm:  HmacSHA1,
+				TimeSigned: timeSigned,
+				Fudge:      300,
+				OrigId:     42,
+			}
+			req := &Msg{
+				MsgHdr:   MsgHdr{Opcode: OpcodeUpdate},
+				Question: []Question{Question{Name: "example.com.", Qtype: TypeSOA, Qclass: ClassINET}},
+				Extra:    []RR{&tsig},
+			}
+
+			_, mac, err := tsigGenerateProvider(req, new(testProvider), "", false)
+			if err != table.err {
+				t.Fatalf("error doesn't match: expected '%s' but got '%s'", table.err, err)
+			}
+			expectedMAC := hex.EncodeToString(table.mac)
+			if mac != expectedMAC {
+				t.Fatalf("MAC doesn't match: expected '%s' but got '%s'", table.mac, expectedMAC)
+			}
+		})
+	}
+}
+
+func TestTsigVerifyProvider(t *testing.T) {
+	tables := []struct {
+		keyname string
+		err     error
+	}{
+		{
+			testGoodKeyName,
+			nil,
+		},
+		{
+			"badkey.",
+			testErrBadKey,
+		},
+	}
+
+	for _, table := range tables {
+		t.Run(table.keyname, func(t *testing.T) {
+			tsig := TSIG{
+				Hdr:        RR_Header{Name: table.keyname, Rrtype: TypeTSIG, Class: ClassANY, Ttl: 0},
+				Algorithm:  HmacSHA1,
+				TimeSigned: timeSigned,
+				Fudge:      300,
+				OrigId:     42,
+			}
+			req := &Msg{
+				MsgHdr:   MsgHdr{Opcode: OpcodeUpdate},
+				Question: []Question{Question{Name: "example.com.", Qtype: TypeSOA, Qclass: ClassINET}},
+				Extra:    []RR{&tsig},
+			}
+
+			provider := &testProvider{true}
+			msgData, _, err := tsigGenerateProvider(req, provider, "", false)
+			if err != nil {
+				t.Error(err)
+			}
+			if err = tsigVerify(msgData, provider, "", false, timeSigned); err != table.err {
+				t.Fatalf("error doesn't match: expected '%s' but got '%s'", table.err, err)
+			}
+		})
+	}
+}