From f3ebe6b80a66c4d6070ebbdd647b6e080fec3506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Wed, 28 Aug 2024 11:53:14 +0200 Subject: [PATCH] shared/trust: Add testing coverage for HMAC utilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus --- shared/trust/hmac_test.go | 272 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 shared/trust/hmac_test.go diff --git a/shared/trust/hmac_test.go b/shared/trust/hmac_test.go new file mode 100644 index 000000000000..f3c8a9462435 --- /dev/null +++ b/shared/trust/hmac_test.go @@ -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) + } + }) + } +}