forked from canonical/lxd
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
shared/trust: Add testing coverage for HMAC utilities
Signed-off-by: Julian Pelizäus <[email protected]>
- Loading branch information
1 parent
5c7dc6b
commit f3ebe6b
Showing
1 changed file
with
272 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |