diff --git a/rfc2865/chap.go b/rfc2865/chap.go new file mode 100644 index 00000000..5be7d193 --- /dev/null +++ b/rfc2865/chap.go @@ -0,0 +1,57 @@ +package rfc2865 + +import ( + "crypto/md5" + "crypto/subtle" + + "layeh.com/radius" +) + +// CHAPMatch reports whether the given CHAP-Password value corresponds to +// password and challenge. +// +// chapPassword must be the raw 17-octet CHAP-Password attribute value: a +// 1-octet CHAP identifier followed by the 16-octet MD5 response, as described +// in RFC 1994 and RFC 2865, Section 2.2. challenge is the CHAP challenge, +// i.e. the CHAP-Challenge attribute value, or the request Authenticator when +// the request does not carry a CHAP-Challenge attribute. +// +// The comparison of the expected and received responses is done in constant +// time. +func CHAPMatch(password, chapPassword, challenge []byte) bool { + if len(chapPassword) != 17 { + return false + } + + h := md5.New() + h.Write(chapPassword[:1]) // CHAP identifier + h.Write(password) + h.Write(challenge) + + var sum [md5.Size]byte + return subtle.ConstantTimeCompare(h.Sum(sum[:0]), chapPassword[1:]) == 1 +} + +// CHAPVerify reports whether password is the correct password for the CHAP +// authentication carried in p. +// +// It combines the CHAP-Password attribute with the CHAP-Challenge attribute. If +// p does not contain a CHAP-Challenge attribute, the packet's Authenticator is +// used as the challenge, as described in RFC 2865, Section 2.2. +// +// CHAPVerify returns false if p does not contain a validly sized CHAP-Password +// attribute. It only handles standard CHAP (MD5); MS-CHAP and MS-CHAPv2 use +// different attributes and algorithms. +func CHAPVerify(p *radius.Packet, password []byte) bool { + chapPassword := CHAPPassword_Get(p) + if len(chapPassword) == 0 { + return false + } + + challenge := CHAPChallenge_Get(p) + if len(challenge) == 0 { + challenge = p.Authenticator[:] + } + + return CHAPMatch(password, chapPassword, challenge) +} diff --git a/rfc2865/chap_test.go b/rfc2865/chap_test.go new file mode 100644 index 00000000..28b23424 --- /dev/null +++ b/rfc2865/chap_test.go @@ -0,0 +1,85 @@ +package rfc2865 + +import ( + "crypto/md5" + "testing" + + "layeh.com/radius" +) + +func makeCHAPPassword(ident byte, password, challenge []byte) []byte { + h := md5.New() + h.Write([]byte{ident}) + h.Write(password) + h.Write(challenge) + + out := make([]byte, 17) + out[0] = ident + copy(out[1:], h.Sum(nil)) + return out +} + +func TestCHAPMatch(t *testing.T) { + password := []byte("hello") + challenge := []byte("0123456789abcdef") + chapPassword := makeCHAPPassword(0x42, password, challenge) + + if !CHAPMatch(password, chapPassword, challenge) { + t.Fatal("CHAPMatch returned false for a valid password") + } + if CHAPMatch([]byte("wrong"), chapPassword, challenge) { + t.Fatal("CHAPMatch returned true for an invalid password") + } + if CHAPMatch(password, chapPassword, []byte("different-chal")) { + t.Fatal("CHAPMatch returned true for a mismatched challenge") + } + if CHAPMatch(password, chapPassword[:16], challenge) { + t.Fatal("CHAPMatch returned true for a malformed CHAP-Password") + } +} + +func TestCHAPVerify_withCHAPChallenge(t *testing.T) { + secret := []byte("secret") + password := []byte("testing123") + challenge := []byte("a-random-challenge") + + p := radius.New(radius.CodeAccessRequest, secret) + if err := CHAPChallenge_Add(p, challenge); err != nil { + t.Fatal(err) + } + if err := CHAPPassword_Add(p, makeCHAPPassword(0x01, password, challenge)); err != nil { + t.Fatal(err) + } + + if !CHAPVerify(p, password) { + t.Fatal("CHAPVerify returned false for a valid password") + } + if CHAPVerify(p, []byte("nope")) { + t.Fatal("CHAPVerify returned true for an invalid password") + } +} + +func TestCHAPVerify_authenticatorAsChallenge(t *testing.T) { + secret := []byte("secret") + password := []byte("testing123") + + p := radius.New(radius.CodeAccessRequest, secret) + // No CHAP-Challenge attribute: the Authenticator is the challenge. + if err := CHAPPassword_Add(p, makeCHAPPassword(0x07, password, p.Authenticator[:])); err != nil { + t.Fatal(err) + } + + if !CHAPVerify(p, password) { + t.Fatal("CHAPVerify returned false for a valid password") + } + if CHAPVerify(p, []byte("nope")) { + t.Fatal("CHAPVerify returned true for an invalid password") + } +} + +func TestCHAPVerify_noCHAPPassword(t *testing.T) { + p := radius.New(radius.CodeAccessRequest, []byte("secret")) + if CHAPVerify(p, []byte("anything")) { + t.Fatal("CHAPVerify returned true for a packet without a CHAP-Password") + } +}