Skip to content

Commit 31586e5

Browse files
committed
Symmetric auth enhancements
Added AES-256-CMAC support. Auth keys may now be specified as ASCII or HEX using the "ASCII:" and "HEX:" prefixes. When running unit tests, the user may override the default ntp server host with the NTP_HOST environment variable.
1 parent b536f10 commit 31586e5

File tree

3 files changed

+101
-56
lines changed

3 files changed

+101
-56
lines changed

auth.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@ import (
1717
)
1818

1919
// AuthType specifies the cryptographic hash algorithm used to generate a
20-
// symmetric key authentication digest (or CMAC) for an NTP message. Although
21-
// in theory many algorithms are supported by well-known NTP servers, in
22-
// practice only MD5 and SHA1 are commonly used. Please note that MD5 and SHA1
23-
// are no longer considered secure; they appear here solely for compatibility
24-
// with existing NTP server implementations.
20+
// symmetric key authentication digest (or CMAC) for an NTP message. Please
21+
// note that MD5 and SHA1 are no longer considered secure; they appear here
22+
// solely for compatibility with existing NTP server implementations.
2523
type AuthType int
2624

2725
const (
@@ -31,6 +29,7 @@ const (
3129
AuthSHA256 // SHA-2 digest (256 bits)
3230
AuthSHA512 // SHA-2 digest (512 bits)
3331
AuthAES128 // AES-128-CMAC
32+
AuthAES256 // AES-256-CMAC
3433
)
3534

3635
// AuthOptions contains fields used to configure symmetric key authentication
@@ -40,9 +39,11 @@ type AuthOptions struct {
4039
// authentication digest or CMAC.
4140
Type AuthType
4241

43-
// The cryptographic key used by the client to perform the authentication.
44-
// If the key string is longer than 20 characters, then it is assumed to
45-
// be hex-encoded. Otherwise it is assumed to be ASCII-encoded.
42+
// The cryptographic key used by the client to perform authentication. The
43+
// key may be hex-encoded or ascii-encoded. To use a hex-encoded key,
44+
// prefix it by "HEX:". To use an ascii-encoded key, prefix it by
45+
// "ASCII:". For example, "HEX:6931564b4a5a5045766c55356b30656c7666316c"
46+
// or "ASCII:cvuZyN4C8HX8hNcAWDWp".
4647
Key string
4748

4849
// The identifier used by the NTP server to identify which key to use
@@ -62,6 +63,7 @@ var algorithms = []struct {
6263
{4, 32, 20, calcDigest_SHA256}, // AuthSHA256
6364
{4, 32, 20, calcDigest_SHA512}, // AuthSHA512
6465
{16, 16, 16, calcCMAC_AES}, // AuthAES128
66+
{32, 32, 16, calcCMAC_AES}, // AuthAES256
6567
}
6668

6769
func calcDigest_MD5(payload, key []byte) []byte {
@@ -153,20 +155,31 @@ func xor(dst, src []byte) {
153155
binary.BigEndian.PutUint64(dst[8:16], d1)
154156
}
155157

156-
func decodeAuthKey(opt AuthOptions) ([]byte, error) {
158+
func decodeAuthKey(opt AuthOptions) (key []byte, err error) {
157159
if opt.Type == AuthNone {
158160
return nil, nil
159161
}
160162

161-
var key []byte
162-
if len(opt.Key) > 20 {
163-
var err error
164-
key, err = hex.DecodeString(opt.Key)
163+
var keyIn string
164+
var isHex bool
165+
switch {
166+
case len(opt.Key) >= 4 && opt.Key[:4] == "HEX:":
167+
isHex, keyIn = true, opt.Key[4:]
168+
case len(opt.Key) >= 6 && opt.Key[:6] == "ASCII:":
169+
isHex, keyIn = false, opt.Key[6:]
170+
case len(opt.Key) > 20:
171+
isHex, keyIn = true, opt.Key
172+
default:
173+
isHex, keyIn = false, opt.Key
174+
}
175+
176+
if isHex {
177+
key, err = hex.DecodeString(keyIn)
165178
if err != nil {
166179
return nil, ErrInvalidAuthKey
167180
}
168181
} else {
169-
key = []byte(opt.Key)
182+
key = []byte(keyIn)
170183
}
171184

172185
a := algorithms[opt.Type]

auth_test.go

Lines changed: 62 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package ntp
77
import (
88
"bytes"
99
"encoding/hex"
10+
"errors"
1011
"os"
1112
"strings"
1213
"testing"
@@ -18,24 +19,23 @@ func TestOnlineAuthenticatedQuery(t *testing.T) {
1819
// server to be running and configured with known symmetric authentication
1920
// keys.
2021
//
21-
// To run this test, you must execute go test with "-args test_auth". For
22+
// To run this test, you must execute it with "-args test_auth". For
2223
// example:
2324
//
2425
// go test -v -run TestOnlineAuthenticatedQuery -args test_auth
2526
//
26-
// You must also run a localhost NTP server configured with the following
27-
// trusted symmetric keys:
27+
// You must also run a local NTP server configured with the following
28+
// trusted symmetric keys (shown in chrony.keys format):
2829
//
29-
// ID TYPE KEY
30-
// -- ---- ---
31-
// 1 MD5 cvuZyN4C8HX8hNcAWDWp
32-
// 2 SHA1 6931564b4a5a5045766c55356b30656c7666316c
33-
// 3 SHA256 7133736e777057764256777739706a5533326164
34-
// 4 SHA512 597675555446585868494d447543425971526e74
35-
// 5 AES128CMAC 68663033736f77706568707164304049
30+
// 1 MD5 ASCII:cvuZyN4C8HX8hNcAWDWp
31+
// 2 SHA1 HEX:6931564b4a5a5045766c55356b30656c7666316c
32+
// 3 SHA256 HEX:7133736e777057764256777739706a5533326164
33+
// 4 SHA512 HEX:597675555446585868494d447543425971526e74
34+
// 5 AES128 HEX:68663033736f77706568707164304049
35+
// 6 AES256 HEX:47cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a
3636

3737
skip := true
38-
for _, arg := range os.Args[1:] {
38+
for _, arg := range os.Args {
3939
if arg == "test_auth" {
4040
skip = false
4141
}
@@ -45,6 +45,8 @@ func TestOnlineAuthenticatedQuery(t *testing.T) {
4545
return
4646
}
4747

48+
var errAuthFail = errors.New("timeout")
49+
4850
cases := []struct {
4951
Type AuthType
5052
Key string
@@ -53,59 +55,78 @@ func TestOnlineAuthenticatedQuery(t *testing.T) {
5355
}{
5456
// KeyID 1 (MD5)
5557
{AuthMD5, "cvuZyN4C8HX8hNcAWDWp", 1, nil},
58+
{AuthMD5, "ASCII:cvuZyN4C8HX8hNcAWDWp", 1, nil},
5659
{AuthMD5, "6376755a794e344338485838684e634157445770", 1, nil},
60+
{AuthMD5, "HEX:6376755a794e344338485838684e634157445770", 1, nil},
5761
{AuthMD5, "", 1, ErrInvalidAuthKey},
58-
{AuthMD5, "6376755a794e344338485838684e63415744577", 1, ErrInvalidAuthKey},
59-
{AuthMD5, "6376755a794e344338485838684e63415744577g", 1, ErrInvalidAuthKey},
60-
{AuthMD5, "XvuZyN4C8HX8hNcAWDWp", 1, ErrAuthFailed},
61-
{AuthMD5, "cvuZyN4C8HX8hNcAWDWp", 2, ErrAuthFailed},
62-
{AuthSHA1, "cvuZyN4C8HX8hNcAWDWp", 1, ErrAuthFailed},
62+
{AuthMD5, "HEX:6376755a794e344338485838684e63415744577", 1, ErrInvalidAuthKey},
63+
{AuthMD5, "HEX:6376755a794e344338485838684e63415744577g", 1, ErrInvalidAuthKey},
64+
{AuthMD5, "ASCII:XvuZyN4C8HX8hNcAWDWp", 1, errAuthFail},
65+
{AuthMD5, "ASCII:cvuZyN4C8HX8hNcAWDWp", 2, errAuthFail},
66+
{AuthSHA1, "ASCII:cvuZyN4C8HX8hNcAWDWp", 1, errAuthFail},
6367

6468
// KeyID 2 (SHA1)
65-
{AuthSHA1, "6931564b4a5a5045766c55356b30656c7666316c", 2, nil},
66-
{AuthSHA1, "i1VKJZPEvlU5k0elvf1l", 2, nil},
69+
{AuthSHA1, "HEX:6931564b4a5a5045766c55356b30656c7666316c", 2, nil},
70+
{AuthSHA1, "HEX:6931564b4a5a5045766c55356b30656c7666316c", 2, nil},
71+
{AuthSHA1, "ASCII:i1VKJZPEvlU5k0elvf1l", 2, nil},
72+
{AuthSHA1, "ASCII:i1VKJZPEvlU5k0elvf1l", 2, nil},
6773
{AuthSHA1, "", 2, ErrInvalidAuthKey},
68-
{AuthSHA1, "0031564b4a5a5045766c55356b30656c7666316c", 2, ErrAuthFailed},
69-
{AuthSHA1, "6931564b4a5a5045766c55356b30656c7666316c", 1, ErrAuthFailed},
70-
{AuthMD5, "6931564b4a5a5045766c55356b30656c7666316c", 2, ErrAuthFailed},
74+
{AuthSHA1, "HEX:0031564b4a5a5045766c55356b30656c7666316c", 2, errAuthFail},
75+
{AuthSHA1, "HEX:6931564b4a5a5045766c55356b30656c7666316c", 1, errAuthFail},
76+
{AuthMD5, "HEX:6931564b4a5a5045766c55356b30656c7666316c", 2, errAuthFail},
7177

7278
// KeyID 3 (SHA256)
73-
{AuthSHA256, "7133736e777057764256777739706a5533326164", 3, nil},
74-
{AuthSHA256, "q3snwpWvBVww9pjU32ad", 3, nil},
79+
{AuthSHA256, "HEX:7133736e777057764256777739706a5533326164", 3, nil},
80+
{AuthSHA256, "ASCII:q3snwpWvBVww9pjU32ad", 3, nil},
7581
{AuthSHA256, "", 3, ErrInvalidAuthKey},
76-
{AuthSHA256, "0033736e777057764256777739706a5533326164", 3, ErrAuthFailed},
77-
{AuthSHA256, "7133736e777057764256777739706a5533326164", 2, ErrAuthFailed},
78-
{AuthSHA1, "7133736e777057764256777739706a5533326164", 3, ErrAuthFailed},
82+
{AuthSHA256, "HEX:0033736e777057764256777739706a5533326164", 3, errAuthFail},
83+
{AuthSHA256, "HEX:7133736e777057764256777739706a5533326164", 2, errAuthFail},
84+
{AuthSHA1, "HEX:7133736e777057764256777739706a5533326164", 3, errAuthFail},
7985

80-
// KeyID 4 (SHA512)
81-
{AuthSHA512, "597675555446585868494d447543425971526e74", 4, nil},
82-
{AuthSHA512, "YvuUTFXXhIMDuCBYqRnt", 4, nil},
86+
// // KeyID 4 (SHA512)
87+
{AuthSHA512, "HEX:597675555446585868494d447543425971526e74", 4, nil},
88+
{AuthSHA512, "ASCII:YvuUTFXXhIMDuCBYqRnt", 4, nil},
8389
{AuthSHA512, "", 4, ErrInvalidAuthKey},
84-
{AuthSHA512, "007675555446585868494d447543425971526e74", 4, ErrAuthFailed},
85-
{AuthSHA512, "597675555446585868494d447543425971526e74", 3, ErrAuthFailed},
86-
{AuthSHA256, "597675555446585868494d447543425971526e74", 4, ErrAuthFailed},
90+
{AuthSHA512, "HEX:007675555446585868494d447543425971526e74", 4, errAuthFail},
91+
{AuthSHA512, "HEX:597675555446585868494d447543425971526e74", 3, errAuthFail},
92+
{AuthSHA256, "HEX:597675555446585868494d447543425971526e74", 4, errAuthFail},
8793

8894
// KeyID 5 (AES128)
89-
{AuthAES128, "68663033736f77706568707164304049", 5, nil},
90-
{AuthAES128, "68663033736f77706568707164304049fefefefe", 5, nil},
91-
{AuthAES128, "hf03sowpehpqd0@I", 5, nil},
95+
{AuthAES128, "HEX:68663033736f77706568707164304049", 5, nil},
96+
{AuthAES128, "HEX:68663033736f77706568707164304049fefefefe", 5, nil},
97+
{AuthAES128, "ASCII:hf03sowpehpqd0@I", 5, nil},
9298
{AuthAES128, "", 5, ErrInvalidAuthKey},
93-
{AuthAES128, "00663033736f77706568707164304049", 5, ErrAuthFailed},
94-
{AuthAES128, "68663033736f77706568707164304049", 4, ErrAuthFailed},
95-
{AuthMD5, "68663033736f77706568707164304049", 5, ErrAuthFailed},
99+
{AuthAES128, "HEX:00663033736f77706568707164304049", 5, errAuthFail},
100+
{AuthAES128, "HEX:68663033736f77706568707164304049", 4, errAuthFail},
101+
{AuthMD5, "HEX:68663033736f77706568707164304049", 5, errAuthFail},
102+
103+
// KeyID 6 (AES256)
104+
{AuthAES256, "HEX:47cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a", 6, nil},
105+
{AuthAES256, "", 6, ErrInvalidAuthKey},
106+
{AuthAES256, "HEX:00cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a", 6, errAuthFail},
107+
{AuthAES256, "HEX:47cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a", 5, errAuthFail},
108+
{AuthMD5, "HEX:47cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a", 6, errAuthFail},
96109
}
97110

98-
host := "localhost"
99111
for i, c := range cases {
100112
opt := QueryOptions{
101-
Timeout: 1 * time.Second,
113+
Timeout: 250 * time.Millisecond,
102114
Auth: AuthOptions{c.Type, c.Key, c.KeyID},
103115
}
104116
r, err := QueryWithOptions(host, opt)
117+
if c.ExpectedErr == errAuthFail {
118+
// With old NTP servers, failed authentication leads to Crypto-NAK
119+
// (ErrAuthFailed). With modern NTP servers, it leads to an I/O
120+
// timeout error.
121+
if err != ErrAuthFailed && !strings.Contains(err.Error(), "timeout") {
122+
t.Errorf("case %d: expected error [%v], got error [%v]\n", i, c.ExpectedErr, err)
123+
}
124+
continue
125+
}
105126
if c.ExpectedErr != nil && c.ExpectedErr == err {
106127
continue
107128
}
108-
if isNil(t, host, err) {
129+
if err == nil {
109130
err = r.Validate()
110131
if err != c.ExpectedErr {
111132
t.Errorf("case %d: expected error [%v], got error [%v]\n", i, c.ExpectedErr, err)

ntp_test.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,30 @@ package ntp
77
import (
88
"errors"
99
"net"
10+
"os"
1011
"strings"
1112
"testing"
1213
"time"
1314

1415
"github.com/stretchr/testify/assert"
1516
)
1617

18+
// The NTP server to use for online unit tests. May be overridden by the
19+
// NTP_HOST environment variable.
20+
var host string = "0.beevik-ntp.pool.ntp.org"
21+
1722
const (
18-
host = "0.beevik-ntp.pool.ntp.org"
1923
refID = 0xc0a80001
2024
timeFormat = "Mon Jan _2 2006 15:04:05.00000000 (MST)"
2125
)
2226

27+
func init() {
28+
h := os.Getenv("NTP_HOST")
29+
if h != "" {
30+
host = h
31+
}
32+
}
33+
2334
func isNil(t *testing.T, host string, err error) bool {
2435
switch {
2536
case err == nil:

0 commit comments

Comments
 (0)