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.
Signed-off-by: Julian Pelizäus <[email protected]>
- Loading branch information
1 parent
34727be
commit 2098a29
Showing
1 changed file
with
251 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,251 @@ | ||
package trust | ||
|
||
import ( | ||
"bytes" | ||
"crypto/hmac" | ||
"crypto/rand" | ||
"crypto/sha256" | ||
"encoding/hex" | ||
"encoding/json" | ||
"fmt" | ||
"hash" | ||
"io" | ||
"net/http" | ||
"strings" | ||
|
||
"golang.org/x/crypto/argon2" | ||
|
||
"github.com/canonical/lxd/shared/api" | ||
) | ||
|
||
// HMACVersion indicates the version used for the authorization header format. | ||
type HMACVersion string | ||
|
||
// HMACConf represents the HMAC configuration. | ||
type HMACConf struct { | ||
HashFunc func() hash.Hash | ||
Passphrase []byte | ||
Version HMACVersion | ||
} | ||
|
||
// HMACFormat represents arbitrary formats to diplay and parse the actual HMAC. | ||
// Implementations like argon extend the format with an additional salt. | ||
type HMACFormat interface { | ||
Passphrase(passphrase []byte) []byte | ||
Header(version HMACVersion, hmac []byte) string | ||
ParseHeader(header string) (HMACVersion, []byte, error) | ||
} | ||
|
||
// HMAC represents the the tooling for creating and validating HMACs. | ||
type HMAC struct { | ||
conf HMACConf | ||
format HMACFormat | ||
} | ||
|
||
// HMACFormatDefault represents the default format. | ||
type HMACFormatDefault struct{} | ||
|
||
// HMACFormatArgon represents the argon2 format. | ||
type HMACFormatArgon struct { | ||
Salt []byte | ||
Time uint32 | ||
Memory uint32 | ||
Threads uint8 | ||
KeyLength uint32 | ||
} | ||
|
||
// NewDefaultHMACConf returns the default configuration for HMAC. | ||
func NewDefaultHMACConf(passphrase string, version HMACVersion) HMACConf { | ||
return HMACConf{ | ||
Passphrase: []byte(passphrase), | ||
HashFunc: sha256.New, | ||
Version: version, | ||
} | ||
} | ||
|
||
// Passphrase accepts the outer HMAC passphrase and returns the same without modification. | ||
func (h *HMACFormatDefault) Passphrase(passphrase []byte) []byte { | ||
return passphrase | ||
} | ||
|
||
// Header returns the actual HMAC alongside together with the used version. | ||
func (h *HMACFormatDefault) Header(version HMACVersion, hmac []byte) string { | ||
return fmt.Sprintf("%s %s", version, hex.EncodeToString(hmac)) | ||
} | ||
|
||
// ParseHeader extracts the actual version and HMAC from the header. | ||
func (h *HMACFormatDefault) ParseHeader(header string) (HMACVersion, []byte, error) { | ||
authHeaderSplit := strings.Split(header, " ") | ||
if len(authHeaderSplit) != 2 { | ||
return "", nil, fmt.Errorf("Authorization header is missing the version: %q", header) | ||
} | ||
|
||
hmac, err := hex.DecodeString(authHeaderSplit[1]) | ||
if err != nil { | ||
return "", nil, fmt.Errorf("Failed to decode the HMAC: %w", err) | ||
} | ||
|
||
return HMACVersion(authHeaderSplit[0]), hmac, nil | ||
} | ||
|
||
// NewDefaultHMACFormatArgon returns the default format for argon2. | ||
// Recommended defaults according to https://www.rfc-editor.org/rfc/rfc9106#section-4-6.2. | ||
// We use the second recommended option to not require a system having 2 GiB of memory. | ||
func NewDefaultHMACFormatArgon() (*HMACFormatArgon, error) { | ||
// 128 bit salt. | ||
salt := make([]byte, 16) | ||
_, err := rand.Read(salt) | ||
if err != nil { | ||
return nil, fmt.Errorf("Failed to create salt: %w", err) | ||
} | ||
|
||
return &HMACFormatArgon{ | ||
Salt: salt, | ||
// 3 iterations. | ||
Time: 3, | ||
// 64 MiB memory. | ||
Memory: 64 * 1024, | ||
// 4 lanes. | ||
Threads: 4, | ||
// 256 bit tag size. | ||
KeyLength: 32, | ||
}, nil | ||
} | ||
|
||
// Passphrase accepts the outer HMAC passphrase and runs it through the argon key derivation function. | ||
func (h *HMACFormatArgon) Passphrase(passphrase []byte) []byte { | ||
return argon2.IDKey(passphrase, h.Salt, h.Time, h.Memory, h.Threads, h.KeyLength) | ||
} | ||
|
||
// Header returns the actual HMAC alongside it's salt together with the used version. | ||
func (h *HMACFormatArgon) Header(version HMACVersion, hmac []byte) string { | ||
return fmt.Sprintf("%s %s:%s", version, hex.EncodeToString(h.Salt), hex.EncodeToString(hmac)) | ||
} | ||
|
||
// ParseHeader extracts the actual version, HMAC and it's salt from the header. | ||
func (h *HMACFormatArgon) ParseHeader(header string) (HMACVersion, []byte, error) { | ||
authHeaderSplit := strings.Split(header, " ") | ||
if len(authHeaderSplit) != 2 { | ||
return "", nil, fmt.Errorf("Authorization header is missing the version: %q", header) | ||
} | ||
|
||
authHeaderDetails := strings.Split(authHeaderSplit[1], ":") | ||
if len(authHeaderDetails) != 2 { | ||
return "", nil, fmt.Errorf("Authorization header is missing name and/or hash: %q", header) | ||
} | ||
|
||
salt, err := hex.DecodeString(authHeaderDetails[0]) | ||
if err != nil { | ||
return "", nil, fmt.Errorf("Failed to decode the name: %w", err) | ||
} | ||
|
||
h.Salt = salt | ||
|
||
hmac, err := hex.DecodeString(authHeaderDetails[1]) | ||
if err != nil { | ||
return "", nil, fmt.Errorf("Failed to decode the HMAC: %w", err) | ||
} | ||
|
||
return HMACVersion(authHeaderSplit[0]), hmac, nil | ||
} | ||
|
||
// NewHMAC returns a new instance of HMAC. | ||
func NewHMAC(conf HMACConf) *HMAC { | ||
return &HMAC{ | ||
conf: conf, | ||
format: &HMACFormatDefault{}, | ||
} | ||
} | ||
|
||
// WithFormat configures the HMAC instance to use the specified format. | ||
// The format can be used to extend the way of displaying and parsing the resulting header. | ||
func (h *HMAC) WithFormat(format HMACFormat) *HMAC { | ||
h.format = format | ||
return h | ||
} | ||
|
||
// WriteBytes creates a new HMAC hash using the given bytes. | ||
func (h *HMAC) WriteBytes(b []byte) ([]byte, error) { | ||
mac := hmac.New(h.conf.HashFunc, h.format.Passphrase(h.conf.Passphrase)) | ||
_, err := mac.Write(b) | ||
if err != nil { | ||
return nil, fmt.Errorf("Failed to create HMAC: %w", err) | ||
} | ||
|
||
return mac.Sum(nil), nil | ||
} | ||
|
||
// WriteJSON creates a new HMAC hash using the given struct. | ||
func (h *HMAC) WriteJSON(v any) ([]byte, error) { | ||
payload, err := json.Marshal(v) | ||
if err != nil { | ||
return nil, fmt.Errorf("Failed to marshal payload: %w", err) | ||
} | ||
|
||
return h.WriteBytes(payload) | ||
} | ||
|
||
// WriteRequest creates a new HMAC hash using the given request. | ||
// It will extract the requests body. | ||
func (h *HMAC) WriteRequest(r *http.Request) ([]byte, error) { | ||
body, err := io.ReadAll(r.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("Failed to read request body: %w", err) | ||
} | ||
|
||
defer func() { | ||
// Reset the request body for the actual handler. | ||
r.Body = io.NopCloser(bytes.NewBuffer(body)) | ||
}() | ||
|
||
err = r.Body.Close() | ||
if err != nil { | ||
return nil, fmt.Errorf("Failed to close the request body: %w", err) | ||
} | ||
|
||
return h.WriteBytes(body) | ||
} | ||
|
||
// AuthorizationHeader returns the authorization header for the given HMAC format. | ||
func (h *HMAC) AuthorizationHeader(v any) (string, error) { | ||
hmacBytes, err := h.WriteJSON(v) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
return h.format.Header(h.conf.Version, hmacBytes), nil | ||
} | ||
|
||
// equal returns true in case hmac1 is identical to hmac2. | ||
func (h *HMAC) equal(hmac1 []byte, hmac2 []byte) bool { | ||
return hmac.Equal(hmac1, hmac2) | ||
} | ||
|
||
// ValidateAuthorizationHeader extracts the HMAC from the Authorization header and | ||
// validates if it matches the HMAC computed over the requests body. | ||
func (h *HMAC) ValidateAuthorizationHeader(r *http.Request) error { | ||
authHeader := r.Header.Get("Authorization") | ||
if authHeader == "" { | ||
return api.StatusErrorf(http.StatusBadRequest, "Authorization header is missing") | ||
} | ||
|
||
version, hmacHeader, err := h.format.ParseHeader(authHeader) | ||
if err != nil { | ||
return api.StatusErrorf(http.StatusBadRequest, "Failed to parse Authorization header: %w", err) | ||
} | ||
|
||
if version != h.conf.Version { | ||
return api.StatusErrorf(http.StatusBadRequest, "Authorization header uses version %q but expected %q", version, h.conf.Version) | ||
} | ||
|
||
hmacBody, err := h.WriteRequest(r) | ||
if err != nil { | ||
return api.StatusErrorf(http.StatusInternalServerError, "Failed to write request body: %w", err) | ||
} | ||
|
||
if !h.equal(hmacHeader, hmacBody) { | ||
return api.StatusErrorf(http.StatusForbidden, "Invalid HMAC") | ||
} | ||
|
||
return nil | ||
} |