Skip to content

Commit

Permalink
shared/trust: Add testing coverage for HMAC utilities
Browse files Browse the repository at this point in the history
Signed-off-by: Julian Pelizäus <[email protected]>
  • Loading branch information
roosterfish committed Aug 28, 2024
1 parent 5c7dc6b commit f3ebe6b
Showing 1 changed file with 272 additions and 0 deletions.
272 changes: 272 additions & 0 deletions shared/trust/hmac_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
package trust

import (
"bytes"
"encoding/hex"
"errors"
"io"
"net/http"
"testing"

"github.com/stretchr/testify/require"
)

// errorCloser implements the Reader and Closer interfaces and allows to
// simulate errors during read and close operations.
type errorReaderCloser struct {
readErr error
closeErr error
}

// Read simulates a read from errorReaderCloser.
// Return io.EOF to exit out any read operations.
func (e errorReaderCloser) Read(p []byte) (n int, err error) {
return 0, e.readErr
}

// Close simulates a close of errorReaderCloser.
func (e errorReaderCloser) Close() error {
return e.closeErr
}

func TestCreateHMAC(t *testing.T) {
tests := []struct {
name string
conf HMACConf
argonSalt string
payload any
expectedHeader string
expectedErr error
}{
{
name: "Create HMAC from simple payload",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
payload: map[string]string{"hello": "world"},
expectedHeader: "LXD1.0 4022ad4878aff5a3bbd815aec63cce26cb5e8abd4df69589312cd0dee25fd717",
},
{
name: "Create HMAC from simple payload using argon2 as KDF",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
argonSalt: "caffee",
payload: map[string]string{"hello": "world"},
expectedHeader: "LXD1.0 caffee:b4b19532928620a1d54e7d1c58e4baaa916a8e0023ed8a08b2b05038d6da189a",
},
{
name: "Reject creating HMAC from invalid payload",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
payload: make(chan bool),
expectedErr: errors.New("Failed to marshal payload: json: unsupported type: chan bool"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hmac := NewHMAC(tt.conf)
if tt.argonSalt != "" {
argonFormat, err := NewDefaultHMACFormatArgon()
require.NoError(t, err)

// Use a fixed salt
salt := tt.argonSalt
saltBytes, err := hex.DecodeString(salt)
require.NoError(t, err)

argonFormat.Salt = saltBytes
hmac.WithFormat(argonFormat)
}

header, err := hmac.AuthorizationHeader(tt.payload)
if tt.expectedErr != nil {
require.Equal(t, tt.expectedErr.Error(), err.Error())
} else {
require.NoError(t, err)
require.Equal(t, tt.expectedHeader, header)
}
})
}
}

func TestValidateHMAC(t *testing.T) {
tests := []struct {
name string
conf HMACConf
argonSalt string
request *http.Request
expectedErr error
}{
{
name: "Validate HMAC from request header",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
request: &http.Request{
Header: http.Header{
"Authorization": []string{"LXD1.0 4022ad4878aff5a3bbd815aec63cce26cb5e8abd4df69589312cd0dee25fd717"},
},
Body: io.NopCloser(bytes.NewBufferString(`{"hello":"world"}`)),
},
},
{
name: "Validate non-matching HMAC from request header",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
request: &http.Request{
Header: http.Header{
"Authorization": []string{"LXD1.0 4022ad4878aff5a3bbd815aec63cce26cb5e8abd4df69589312cd0dee25fd717"},
},
Body: io.NopCloser(bytes.NewBufferString(`{"hello":"world","modified":"body"}`)),
},
expectedErr: errors.New("Invalid HMAC"),
},
{
name: "Validate HMAC from request header using argon2 as KDF",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
argonSalt: "caffee",
request: &http.Request{
Header: http.Header{
"Authorization": []string{"LXD1.0 caffee:b4b19532928620a1d54e7d1c58e4baaa916a8e0023ed8a08b2b05038d6da189a"},
},
Body: io.NopCloser(bytes.NewBufferString(`{"hello":"world"}`)),
},
},
{
name: "Validate non-matching HMAC from request header using argon2 as KDF",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
argonSalt: "caffee",
request: &http.Request{
Header: http.Header{
"Authorization": []string{"LXD1.0 caffee:b4b19532928620a1d54e7d1c58e4baaa916a8e0023ed8a08b2b05038d6da189a"},
},
Body: io.NopCloser(bytes.NewBufferString(`{"hello":"world","modified":"body"}`)),
},
expectedErr: errors.New("Invalid HMAC"),
},
{
name: "Reject header missing the version",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
request: &http.Request{
Header: http.Header{
"Authorization": []string{"invalid"},
},
},
expectedErr: errors.New("Failed to parse Authorization header: Version or HMAC is missing"),
},
{
name: "Reject header missing the version using argon2 as KDF",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
argonSalt: "caffee",
request: &http.Request{
Header: http.Header{
"Authorization": []string{"invalid"},
},
},
expectedErr: errors.New("Failed to parse Authorization header: Version or HMAC and salt combination is missing"),
},
{
name: "Reject header missing the HMAC and salt combination using argon2 as KDF",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
argonSalt: "caffee",
request: &http.Request{
Header: http.Header{
"Authorization": []string{"LXD1.0 caffee"},
},
},
expectedErr: errors.New("Failed to parse Authorization header: Salt or HMAC is missing"),
},
{
name: "Reject header with a non hex salt using argon2 as KDF",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
argonSalt: "caffee",
request: &http.Request{
Header: http.Header{
"Authorization": []string{"LXD1.0 nonhex:abc"},
},
},
expectedErr: errors.New("Failed to parse Authorization header: Failed to decode the salt: encoding/hex: invalid byte: U+006E 'n'"),
},
{
name: "Reject header with a non hex HMAC using argon2 as KDF",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
argonSalt: "caffee",
request: &http.Request{
Header: http.Header{
"Authorization": []string{"LXD1.0 caffee:nonhex"},
},
},
expectedErr: errors.New("Failed to parse Authorization header: Failed to decode the HMAC: encoding/hex: invalid byte: U+006E 'n'"),
},
{
name: "Reject request with missing Authorization header",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
request: &http.Request{},
expectedErr: errors.New("Authorization header is missing"),
},
{
name: "Reject request with non-matching HMAC version",
conf: NewDefaultHMACConf("foo", "LXD2.0"),
request: &http.Request{
Header: http.Header{
"Authorization": []string{"LXD1.0 4022ad4878aff5a3bbd815aec63cce26cb5e8abd4df69589312cd0dee25fd717"},
},
},
expectedErr: errors.New(`Authorization header uses version "LXD1.0" but expected "LXD2.0"`),
},
{
name: "Reject request with a non hex HMAC",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
request: &http.Request{
Header: http.Header{
"Authorization": []string{"LXD1.0 nonhexcharacters"},
},
},
expectedErr: errors.New("Failed to parse Authorization header: Failed to decode the HMAC: encoding/hex: invalid byte: U+006E 'n'"),
},
{
name: "Reject request whose body cannot be read",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
request: &http.Request{
Header: http.Header{
"Authorization": []string{"LXD1.0 4022ad4878aff5a3bbd815aec63cce26cb5e8abd4df69589312cd0dee25fd717"},
},
// Use reader that errors out immediately.
Body: errorReaderCloser{readErr: errors.New("Fail always")},
},
expectedErr: errors.New("Failed to write request body: Failed to read request body: Fail always"),
},
{
name: "Reject request whose body cannot be closed",
conf: NewDefaultHMACConf("foo", "LXD1.0"),
request: &http.Request{
Header: http.Header{
"Authorization": []string{"LXD1.0 4022ad4878aff5a3bbd815aec63cce26cb5e8abd4df69589312cd0dee25fd717"},
},
// Use reader that errors out immediately.
// Return EOF from the reader to trigger a Close.
Body: errorReaderCloser{readErr: io.EOF, closeErr: errors.New("Fail always")},
},
expectedErr: errors.New("Failed to write request body: Failed to close the request body: Fail always"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hmac := NewHMAC(tt.conf)
if tt.argonSalt != "" {
argonFormat, err := NewDefaultHMACFormatArgon()
require.NoError(t, err)

// Use a fixed salt
salt := tt.argonSalt
saltBytes, err := hex.DecodeString(salt)
require.NoError(t, err)

argonFormat.Salt = saltBytes
hmac.WithFormat(argonFormat)
}

err := hmac.ValidateAuthorizationHeader(tt.request)
if tt.expectedErr != nil {
require.Equal(t, tt.expectedErr.Error(), err.Error())
} else {
require.NoError(t, err)
}
})
}
}

0 comments on commit f3ebe6b

Please sign in to comment.