Shared: Add HMAC and cert utils
This PR adds additional HMAC and cert utils to the shared package.
The HMAC implementation follows the practices from the explicit trust establishment spec. It's imported as a general utility in LXD to be used by MicroCloud during trust establishment.
In order to make the use of a KDF (in this case argon2) optional, a format interface is added to allow plugging this functionality in on demand.
To create a HMAC header, the caller will run:
// Specific HMAC version to allow indicating protocol changes in the future.
const HMACMicroCloud10 trust.HMACVersion = "MicroCloud-1.0"
// Custom format that extends HMAC with a KDF (in this case argon2).
argonFormat, err := trust.NewDefaultHMACFormatArgon()
if err != nil {
return err
}
hmac := trust.NewHMAC(trust.NewDefaultHMACConf(<passphrase>, HMACMicroCloud10)).WithFormat(argonFormat)
// Create the "Authorization" header in the format `Authorization: <version> <salt>:<HMAC>`
header, err := hmac.AuthorizationHeader(<message body to be sent>)
if err != nil {
return err
}
On the server side the validation happens as follows:
argonFormat, err := trust.NewDefaultHMACFormatArgon()
if err != nil {
return response.InternalError(fmt.Errorf("Failed to setup argon: %w", err))
}
requestHMAC := trust.NewHMAC(trust.NewDefaultHMACConf(<passphrase>, HMACMicroCloud10)).WithFormat(argonFormat)
// Validate the HMAC based on the given *http.Request r
err = requestHMAC.ValidateAuthorizationHeader(r)
if err != nil {
return response.SmartError(err)
}
I am still not sure if we want to track the version within the format. Thinking about possible scenarios here.
This PR adds additional HMAC and cert utils to the
sharedpackage.The HMAC implementation follows the practices from the explicit trust establishment spec. It's imported as a general utility in LXD to be used by MicroCloud during trust establishment.
In order to make the use of a KDF (in this case argon2) optional, a format interface is added to allow plugging this functionality in on demand.
To create a HMAC header, the caller will run:
// Specific HMAC version to allow indicating protocol changes in the future. const HMACMicroCloud10 trust.HMACVersion = "MicroCloud-1.0" // Custom format that extends HMAC with a KDF (in this case argon2). argonFormat, err := trust.NewDefaultHMACFormatArgon() if err != nil { return err } hmac := trust.NewHMAC(trust.NewDefaultHMACConf(<passphrase>, HMACMicroCloud10)).WithFormat(argonFormat) // Create the "Authorization" header in the format `Authorization: <version> <salt>:<HMAC>` header, err := hmac.AuthorizationHeader(<message body to be sent>) if err != nil { return err }On the server side the validation happens as follows:
argonFormat, err := trust.NewDefaultHMACFormatArgon() if err != nil { return response.InternalError(fmt.Errorf("Failed to setup argon: %w", err)) } requestHMAC := trust.NewHMAC(trust.NewDefaultHMACConf(<passphrase>, HMACMicroCloud10)).WithFormat(argonFormat) // Validate the HMAC based on the given *http.Request r err = requestHMAC.ValidateAuthorizationHeader(r) if err != nil { return response.SmartError(err) }
Is the expectation that this will be used in an http middleware?
I'm a bit concerned how this is tightly coupled to the http package (although I do recall as discussing the balance of making it flexible and easy to test vs making it opinionated for the microcloud trust scenario).
Again, im wondering a bytes.Buffer would help to decouple this from the http package, and make setting/getting the Authorization header an excercise for the caller?
Again, im wondering a bytes.Buffer would help to decouple this from the http package, and make setting/getting the Authorization header an excercise for the caller?
Let me give you the reason why I went this route.
The client who needs to send the signed request has some type of request body that will be used to create the HMAC.
In our scenarios this is usually some struct which will then get marshaled into JSON and sent in the request.
To make it easy to generate an HMAC from various body/input types, I have added the WriteBytes, WriteJSON and WriteRequest methods which ultimately allow you to provide input for the HMAC based on different scenarios.
WriteBytes and WriteJSON (which uses WriteBytes under the hood) are there mostly for the client who sends the request.
At this point you have the contents you want to send in the body ready as a specific struct.
On the other side if you look at the receiver (the one that has to validate the HMAC signed request), the information about the used data type within the request body is unknown so we cannot imply anything and have take the raw bytes from the requests body.
Therefore I made it simple by adding a WriteRequest which also uses the WriteBytes under the hood but doesn't imply anything on the incoming request.
Is the expectation that this will be used in an http middleware?
Yes. As reading from the requests body within some middleware (where this validation code is intended to be used) has to be done with care, the bodies reader has to be duplicated to allow reading the body without affecting any downstream handlers.
This together with the fact that the HMAC has to be provided as part of the Authorization header made me thinking of including all the extraction and body copy logic as part of another helper.
This led to the fact of accepting a *http.Request in ValidateAuthorizationHeader. It not only helps in preventing to duplicate the header retrieval code but it also doesn't require the caller to duplicate the request body by himself.
It also has the benefit that after you have initialized your instance of HMAC you only have to call one method to either generate or validate the header.
At the end it's opinionated in a way that it only allows to validate the HMAC in case it's embedded as part of a requests header. But as this is the only use case right now for MicroClouds authentication middleware, I think we can easily add additional validators in the future if necessary.
Please could we have some tests that are usage examples for this package?
Added. I have split the tests into HMAC creation and validation to demonstrate both usage scenarios.