1package dns
2
3import (
4	"encoding/binary"
5	"encoding/hex"
6	"errors"
7	"fmt"
8	"strings"
9	"testing"
10	"time"
11)
12
13func newTsig(algo string) *Msg {
14	m := new(Msg)
15	m.SetQuestion("example.org.", TypeA)
16	m.SetTsig("example.", algo, 300, time.Now().Unix())
17	return m
18}
19
20func TestTsig(t *testing.T) {
21	m := newTsig(HmacSHA256)
22	buf, _, err := TsigGenerate(m, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
23	if err != nil {
24		t.Fatal(err)
25	}
26	err = TsigVerify(buf, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
27	if err != nil {
28		t.Fatal(err)
29	}
30
31	// TSIG accounts for ID substitution. This means if the message ID is
32	// changed by a forwarder, we should still be able to verify the TSIG.
33	m = newTsig(HmacSHA256)
34	buf, _, err = TsigGenerate(m, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
35	if err != nil {
36		t.Fatal(err)
37	}
38
39	binary.BigEndian.PutUint16(buf[0:2], 42)
40	err = TsigVerify(buf, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
41	if err != nil {
42		t.Fatal(err)
43	}
44}
45
46func TestTsigCase(t *testing.T) {
47	m := newTsig(strings.ToUpper(HmacSHA256))
48	buf, _, err := TsigGenerate(m, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
49	if err != nil {
50		t.Fatal(err)
51	}
52	err = TsigVerify(buf, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
53	if err != nil {
54		t.Fatal(err)
55	}
56}
57
58const (
59	// A template wire-format DNS message (in hex form) containing a TSIG RR.
60	// Its time signed field will be filled by tests.
61	wireMsg = "c60028000001000000010001076578616d706c6503636f6d00000600010161c00c0001000100000e100004c0000201077465" +
62		"73746b65790000fa00ff00000000003d0b686d61632d73686132353600" +
63		"%012x" + // placeholder for the "time signed" field
64		"012c00208cf23e0081d915478a182edcea7ff48ad102948e6c7ef8e887536957d1fa5616c60000000000"
65	// A secret (in base64 format) with which the TSIG in wireMsg will be validated
66	testSecret = "NoTCJU+DMqFWywaPyxSijrDEA/eC3nK0xi3AMEZuPVk="
67	// the 'time signed' field value that would make the TSIG RR valid with testSecret
68	timeSigned uint64 = 1594855491
69)
70
71func TestTsigErrors(t *testing.T) {
72	// Helper shortcut to build wire-format test message.
73	// TsigVerify can modify the slice, so we need to create a new one for each test case below.
74	buildMsgData := func(tm uint64) []byte {
75		msgData, err := hex.DecodeString(fmt.Sprintf(wireMsg, tm))
76		if err != nil {
77			t.Fatal(err)
78		}
79		return msgData
80	}
81
82	// the signature is valid but 'time signed' is too far from the "current time".
83	if err := tsigVerify(buildMsgData(timeSigned), tsigHMACProvider(testSecret), "", false, timeSigned+301); err != ErrTime {
84		t.Fatalf("expected an error '%v' but got '%v'", ErrTime, err)
85	}
86	if err := tsigVerify(buildMsgData(timeSigned), tsigHMACProvider(testSecret), "", false, timeSigned-301); err != ErrTime {
87		t.Fatalf("expected an error '%v' but got '%v'", ErrTime, err)
88	}
89
90	// the signature is invalid and 'time signed' is too far.
91	// the signature should be checked first, so we should see ErrSig.
92	if err := tsigVerify(buildMsgData(timeSigned+301), tsigHMACProvider(testSecret), "", false, timeSigned); err != ErrSig {
93		t.Fatalf("expected an error '%v' but got '%v'", ErrSig, err)
94	}
95
96	// tweak the algorithm name in the wire data, resulting in the "unknown algorithm" error.
97	msgData := buildMsgData(timeSigned)
98	copy(msgData[67:], "bogus")
99	if err := tsigVerify(msgData, tsigHMACProvider(testSecret), "", false, timeSigned); err != ErrKeyAlg {
100		t.Fatalf("expected an error '%v' but got '%v'", ErrKeyAlg, err)
101	}
102
103	// call TsigVerify with a message that doesn't contain a TSIG
104	msgData, tsig, err := stripTsig(buildMsgData(timeSigned))
105	if err != nil {
106		t.Fatal(err)
107	}
108	if err := tsigVerify(msgData, tsigHMACProvider(testSecret), "", false, timeSigned); err != ErrNoSig {
109		t.Fatalf("expected an error '%v' but got '%v'", ErrNoSig, err)
110	}
111
112	// replace the test TSIG with a bogus one with large "other data", which would cause overflow in TsigVerify.
113	// The overflow should be caught without disruption.
114	tsig.OtherData = strings.Repeat("00", 4096)
115	tsig.OtherLen = uint16(len(tsig.OtherData) / 2)
116	msg := new(Msg)
117	if err = msg.Unpack(msgData); err != nil {
118		t.Fatal(err)
119	}
120	msg.Extra = append(msg.Extra, tsig)
121	if msgData, err = msg.Pack(); err != nil {
122		t.Fatal(err)
123	}
124	err = tsigVerify(msgData, tsigHMACProvider(testSecret), "", false, timeSigned)
125	if err == nil || !strings.Contains(err.Error(), "overflow") {
126		t.Errorf("expected error to contain %q, but got %v", "overflow", err)
127	}
128}
129
130// This test exercises some more corner cases for TsigGenerate.
131func TestTsigGenerate(t *testing.T) {
132	// This is a template TSIG to be used for signing.
133	tsig := TSIG{
134		Hdr:        RR_Header{Name: "testkey.", Rrtype: TypeTSIG, Class: ClassANY, Ttl: 0},
135		Algorithm:  HmacSHA256,
136		TimeSigned: timeSigned,
137		Fudge:      300,
138		OrigId:     42,
139		Error:      RcodeBadTime, // use a non-0 value to make sure it's indeed used
140	}
141
142	tests := []struct {
143		desc        string // test description
144		requestMAC  string // request MAC to be passed to TsigGenerate (arbitrary choice)
145		otherData   string // other data specified in the TSIG (arbitrary choice)
146		expectedMAC string // pre-computed expected (correct) MAC in hex form
147	}{
148		{"with request MAC", "3684c225", "",
149			"c110e3f62694755c10761dc8717462431ee34340b7c9d1eee09449150757c5b1"},
150		{"no request MAC", "", "",
151			"385449a425c6d52b9bf2c65c0726eefa0ad8084cdaf488f24547e686605b9610"},
152		{"with other data", "3684c225", "666f6f",
153			"15b91571ca80b3b410a77e2b44f8cc4f35ace22b26020138439dd94803e23b5d"},
154	}
155	for _, tc := range tests {
156		tc := tc
157		t.Run(tc.desc, func(t *testing.T) {
158			// Build TSIG for signing from the template
159			testTSIG := tsig
160			testTSIG.OtherLen = uint16(len(tc.otherData) / 2)
161			testTSIG.OtherData = tc.otherData
162			req := &Msg{
163				MsgHdr:   MsgHdr{Opcode: OpcodeUpdate},
164				Question: []Question{Question{Name: "example.com.", Qtype: TypeSOA, Qclass: ClassINET}},
165				Extra:    []RR{&testTSIG},
166			}
167
168			// Call generate, and check the returned MAC against the expected value
169			msgData, mac, err := TsigGenerate(req, testSecret, tc.requestMAC, false)
170			if err != nil {
171				t.Error(err)
172			}
173			if mac != tc.expectedMAC {
174				t.Fatalf("MAC doesn't match: expected '%s', but got '%s'", tc.expectedMAC, mac)
175			}
176
177			// Retrieve the TSIG to be sent out, confirm the MAC in it
178			_, outTSIG, err := stripTsig(msgData)
179			if err != nil {
180				t.Error(err)
181			}
182			if outTSIG.MAC != tc.expectedMAC {
183				t.Fatalf("MAC doesn't match: expected '%s', but got '%s'", tc.expectedMAC, outTSIG.MAC)
184			}
185			// Confirm other fields of MAC.
186			// RDLENGTH should be valid as stripTsig succeeded, so we exclude it from comparison
187			outTSIG.MACSize = 0
188			outTSIG.MAC = ""
189			testTSIG.Hdr.Rdlength = outTSIG.Hdr.Rdlength
190			if *outTSIG != testTSIG {
191				t.Fatalf("TSIG RR doesn't match: expected '%v', but got '%v'", *outTSIG, testTSIG)
192			}
193		})
194	}
195}
196
197func TestTSIGHMAC224And384(t *testing.T) {
198	tests := []struct {
199		algorithm   string // TSIG algorithm, also used as test description
200		secret      string // (arbitrarily chosen) secret suitable for the algorithm in base64 format
201		expectedMAC string // pre-computed expected (correct) MAC in hex form
202	}{
203		{HmacSHA224, "hVEkQuAqnTmBuRrT9KF1Udr91gOMGWPw9LaTtw==",
204			"d6daf9ea189e48bc38f9aed63d6cc4140cdfa38a7a333ee2eefdbd31",
205		},
206		{HmacSHA384, "Qjer2TL2lAdpq9w6Gjs98/ClCQx/L3vtgVHCmrZ8l/oKEPjqUUMFO18gMCRwd5H4",
207			"89a48936d29187870c325cbdba5ad71609bd038d0459d6010c844d659c570e881d3650e4fe7310be53ebe5178d0d1001",
208		},
209	}
210	for _, tc := range tests {
211		tc := tc
212		t.Run(tc.algorithm, func(t *testing.T) {
213			// Build a DNS message with TSIG for the test scenario
214			tsig := TSIG{
215				Hdr:        RR_Header{Name: "testkey.", Rrtype: TypeTSIG, Class: ClassANY, Ttl: 0},
216				Algorithm:  tc.algorithm,
217				TimeSigned: timeSigned,
218				Fudge:      300,
219				OrigId:     42,
220			}
221			req := &Msg{
222				MsgHdr:   MsgHdr{Opcode: OpcodeUpdate},
223				Question: []Question{Question{Name: "example.com.", Qtype: TypeSOA, Qclass: ClassINET}},
224				Extra:    []RR{&tsig},
225			}
226
227			// Confirm both Generate and Verify recognize the algorithm and handle it correctly
228			msgData, mac, err := TsigGenerate(req, tc.secret, "", false)
229			if err != nil {
230				t.Error(err)
231			}
232			if mac != tc.expectedMAC {
233				t.Fatalf("MAC doesn't match: expected '%s' but got '%s'", tc.expectedMAC, mac)
234			}
235			if err = tsigVerify(msgData, tsigHMACProvider(tc.secret), "", false, timeSigned); err != nil {
236				t.Error(err)
237			}
238		})
239	}
240}
241
242const testGoodKeyName = "goodkey."
243
244var (
245	testErrBadKey = errors.New("this is an intentional error")
246	testGoodMAC   = []byte{0, 1, 2, 3}
247)
248
249// testProvider always generates the same MAC and only accepts the one signature
250type testProvider struct {
251	GenerateAllKeys bool
252}
253
254func (provider *testProvider) Generate(_ []byte, t *TSIG) ([]byte, error) {
255	if t.Hdr.Name == testGoodKeyName || provider.GenerateAllKeys {
256		return testGoodMAC, nil
257	}
258	return nil, testErrBadKey
259}
260
261func (*testProvider) Verify(_ []byte, t *TSIG) error {
262	if t.Hdr.Name == testGoodKeyName {
263		return nil
264	}
265	return testErrBadKey
266}
267
268func TestTsigGenerateProvider(t *testing.T) {
269	tables := []struct {
270		keyname string
271		mac     []byte
272		err     error
273	}{
274		{
275			testGoodKeyName,
276			testGoodMAC,
277			nil,
278		},
279		{
280			"badkey.",
281			nil,
282			testErrBadKey,
283		},
284	}
285
286	for _, table := range tables {
287		t.Run(table.keyname, func(t *testing.T) {
288			tsig := TSIG{
289				Hdr:        RR_Header{Name: table.keyname, Rrtype: TypeTSIG, Class: ClassANY, Ttl: 0},
290				Algorithm:  HmacSHA1,
291				TimeSigned: timeSigned,
292				Fudge:      300,
293				OrigId:     42,
294			}
295			req := &Msg{
296				MsgHdr:   MsgHdr{Opcode: OpcodeUpdate},
297				Question: []Question{Question{Name: "example.com.", Qtype: TypeSOA, Qclass: ClassINET}},
298				Extra:    []RR{&tsig},
299			}
300
301			_, mac, err := tsigGenerateProvider(req, new(testProvider), "", false)
302			if err != table.err {
303				t.Fatalf("error doesn't match: expected '%s' but got '%s'", table.err, err)
304			}
305			expectedMAC := hex.EncodeToString(table.mac)
306			if mac != expectedMAC {
307				t.Fatalf("MAC doesn't match: expected '%s' but got '%s'", table.mac, expectedMAC)
308			}
309		})
310	}
311}
312
313func TestTsigVerifyProvider(t *testing.T) {
314	tables := []struct {
315		keyname string
316		err     error
317	}{
318		{
319			testGoodKeyName,
320			nil,
321		},
322		{
323			"badkey.",
324			testErrBadKey,
325		},
326	}
327
328	for _, table := range tables {
329		t.Run(table.keyname, func(t *testing.T) {
330			tsig := TSIG{
331				Hdr:        RR_Header{Name: table.keyname, Rrtype: TypeTSIG, Class: ClassANY, Ttl: 0},
332				Algorithm:  HmacSHA1,
333				TimeSigned: timeSigned,
334				Fudge:      300,
335				OrigId:     42,
336			}
337			req := &Msg{
338				MsgHdr:   MsgHdr{Opcode: OpcodeUpdate},
339				Question: []Question{Question{Name: "example.com.", Qtype: TypeSOA, Qclass: ClassINET}},
340				Extra:    []RR{&tsig},
341			}
342
343			provider := &testProvider{true}
344			msgData, _, err := tsigGenerateProvider(req, provider, "", false)
345			if err != nil {
346				t.Error(err)
347			}
348			if err = tsigVerify(msgData, provider, "", false, timeSigned); err != table.err {
349				t.Fatalf("error doesn't match: expected '%s' but got '%s'", table.err, err)
350			}
351		})
352	}
353}
354