Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions rfc2869/message_authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package rfc2869

import (
"crypto/hmac"
"crypto/md5"
"encoding/binary"
"errors"

"layeh.com/radius"
)

const messageAuthenticatorValueLength = 16

// ErrNoMessageAuthenticator is returned when a packet does not contain a
// Message-Authenticator attribute.
var ErrNoMessageAuthenticator = errors.New("rfc2869: no Message-Authenticator attribute")

// ErrInvalidMessageAuthenticator is returned by ValidateMessageAuthenticator
// when a packet's Message-Authenticator attribute does not match the expected
// HMAC-MD5 value.
var ErrInvalidMessageAuthenticator = errors.New("rfc2869: invalid Message-Authenticator")

// AddMessageAuthenticator adds a zeroed, correctly sized Message-Authenticator
// attribute to p, replacing any existing one.
//
// The placeholder attribute must be present before the packet is encoded so
// that SignMessageAuthenticator can fill in the computed value over the final
// wire bytes.
func AddMessageAuthenticator(p *radius.Packet) {
var zero [messageAuthenticatorValueLength]byte
_ = MessageAuthenticator_Set(p, zero[:])
}

// SignMessageAuthenticator computes the Message-Authenticator (RFC 3579,
// Section 3.2) for the encoded RADIUS packet contained in wire and writes it
// into the packet's Message-Authenticator attribute.
//
// wire must be a fully encoded RADIUS packet that already contains a 16-octet
// Message-Authenticator attribute (use AddMessageAuthenticator before encoding).
// secret is the shared secret used as the HMAC-MD5 key.
//
// The HMAC is computed over the entire packet with the Message-Authenticator
// value treated as zero, using the Authenticator field currently present in
// wire. This is exactly the required computation for Access-Request and
// Status-Server packets. For response packets, set the Authenticator field to
// the corresponding request's Authenticator before signing and compute the
// Response Authenticator afterwards.
func SignMessageAuthenticator(wire, secret []byte) error {
offset, err := messageAuthenticatorValueOffset(wire)
if err != nil {
return err
}
sum := computeMessageAuthenticator(wire, secret, offset)
copy(wire[offset:offset+messageAuthenticatorValueLength], sum)
return nil
}

// ValidateMessageAuthenticator verifies the Message-Authenticator attribute
// (RFC 3579, Section 3.2) of the encoded RADIUS packet contained in wire,
// using secret as the HMAC-MD5 key.
//
// It returns nil if the attribute is present and valid,
// ErrNoMessageAuthenticator if the packet has no Message-Authenticator
// attribute, ErrInvalidMessageAuthenticator if the attribute does not match,
// or another error if wire is malformed.
//
// The Authenticator field present in wire is used as the HMAC input, which is
// correct for Access-Request and Status-Server packets. To validate a response
// packet, set the Authenticator field to the request's Authenticator first.
func ValidateMessageAuthenticator(wire, secret []byte) error {
offset, err := messageAuthenticatorValueOffset(wire)
if err != nil {
return err
}
sum := computeMessageAuthenticator(wire, secret, offset)
if !hmac.Equal(sum, wire[offset:offset+messageAuthenticatorValueLength]) {
return ErrInvalidMessageAuthenticator
}
return nil
}

// messageAuthenticatorValueOffset returns the offset within wire of the 16-octet
// Message-Authenticator value.
func messageAuthenticatorValueOffset(wire []byte) (int, error) {
if len(wire) < 20 {
return 0, errors.New("rfc2869: packet not at least 20 bytes long")
}
length := int(binary.BigEndian.Uint16(wire[2:4]))
if length < 20 || length > len(wire) {
return 0, errors.New("rfc2869: invalid packet length")
}

for i := 20; i < length; {
if i+2 > length {
return 0, errors.New("rfc2869: invalid attribute")
}
attrLength := int(wire[i+1])
if attrLength < 2 || i+attrLength > length {
return 0, errors.New("rfc2869: invalid attribute length")
}
if radius.Type(wire[i]) == MessageAuthenticator_Type {
if attrLength != 2+messageAuthenticatorValueLength {
return 0, errors.New("rfc2869: invalid Message-Authenticator length")
}
return i + 2, nil
}
i += attrLength
}

return 0, ErrNoMessageAuthenticator
}

// computeMessageAuthenticator returns the HMAC-MD5 over wire (up to its declared
// length) with the 16 octets at valueOffset treated as zero.
func computeMessageAuthenticator(wire, secret []byte, valueOffset int) []byte {
length := int(binary.BigEndian.Uint16(wire[2:4]))

mac := hmac.New(md5.New, secret)
mac.Write(wire[:valueOffset])
var zero [messageAuthenticatorValueLength]byte
mac.Write(zero[:])
mac.Write(wire[valueOffset+messageAuthenticatorValueLength : length])
return mac.Sum(nil)
}
106 changes: 106 additions & 0 deletions rfc2869/message_authenticator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package rfc2869

import (
"crypto/hmac"
"crypto/md5"
"encoding/binary"
"errors"
"testing"

"layeh.com/radius"
)

func encodeWithMessageAuthenticator(t *testing.T, code radius.Code, secret []byte) []byte {
t.Helper()

p := radius.New(code, secret)
p.Add(radius.Type(1), radius.Attribute("user")) // User-Name
AddMessageAuthenticator(p)
p.Add(radius.Type(4), radius.Attribute{192, 0, 2, 1}) // NAS-IP-Address

wire, err := p.Encode()
if err != nil {
t.Fatalf("encode: %s", err)
}
return wire
}

func TestMessageAuthenticator_SignAndValidate(t *testing.T) {
secret := []byte("s3cr3t")

for _, code := range []radius.Code{radius.CodeAccessRequest, radius.CodeStatusServer} {
wire := encodeWithMessageAuthenticator(t, code, secret)

if err := SignMessageAuthenticator(wire, secret); err != nil {
t.Fatalf("code %v: sign: %s", code, err)
}

if err := ValidateMessageAuthenticator(wire, secret); err != nil {
t.Fatalf("code %v: validate after sign: %s", code, err)
}

// Independently recompute the HMAC over the packet with the
// Message-Authenticator value zeroed and confirm it matches what Sign
// wrote.
offset, err := messageAuthenticatorValueOffset(wire)
if err != nil {
t.Fatalf("code %v: offset: %s", code, err)
}
length := int(binary.BigEndian.Uint16(wire[2:4]))
check := make([]byte, length)
copy(check, wire[:length])
for i := offset; i < offset+messageAuthenticatorValueLength; i++ {
check[i] = 0
}
mac := hmac.New(md5.New, secret)
mac.Write(check)
if !hmac.Equal(mac.Sum(nil), wire[offset:offset+messageAuthenticatorValueLength]) {
t.Fatalf("code %v: signed value does not match independent HMAC", code)
}
}
}

func TestMessageAuthenticator_ValidateTampered(t *testing.T) {
secret := []byte("s3cr3t")
wire := encodeWithMessageAuthenticator(t, radius.CodeAccessRequest, secret)
if err := SignMessageAuthenticator(wire, secret); err != nil {
t.Fatal(err)
}

// Flip a bit in the packet body (the User-Name value).
tampered := make([]byte, len(wire))
copy(tampered, wire)
tampered[22] ^= 0x01
if err := ValidateMessageAuthenticator(tampered, secret); !errors.Is(err, ErrInvalidMessageAuthenticator) {
t.Fatalf("got err %v; expected ErrInvalidMessageAuthenticator", err)
}

// A different secret must not validate.
if err := ValidateMessageAuthenticator(wire, []byte("other")); !errors.Is(err, ErrInvalidMessageAuthenticator) {
t.Fatalf("got err %v; expected ErrInvalidMessageAuthenticator", err)
}
}

func TestMessageAuthenticator_Missing(t *testing.T) {
secret := []byte("s3cr3t")

p := radius.New(radius.CodeAccessRequest, secret)
p.Add(radius.Type(1), radius.Attribute("user"))
wire, err := p.Encode()
if err != nil {
t.Fatal(err)
}

if err := ValidateMessageAuthenticator(wire, secret); !errors.Is(err, ErrNoMessageAuthenticator) {
t.Fatalf("validate got err %v; expected ErrNoMessageAuthenticator", err)
}
if err := SignMessageAuthenticator(wire, secret); !errors.Is(err, ErrNoMessageAuthenticator) {
t.Fatalf("sign got err %v; expected ErrNoMessageAuthenticator", err)
}
}

func TestMessageAuthenticator_Malformed(t *testing.T) {
if err := ValidateMessageAuthenticator([]byte{0x01, 0x02}, []byte("x")); err == nil {
t.Fatal("expected error for short packet")
}
}