HMAC
In addition to ECDSA signatures, Schnorr signatures, and RSA operations, the Builder Vault also supports HMAC-SHA256 and HMAC-SHA512 operations.
A Note on Performance and Parameters
Computing HMAC in a threshold wallet requires the use of so-called general purpose MPC. Builder Vault uses one of the most efficient general purpose MPC protocols, but even so, the computation of HMAC still requires a significant amount of bandwidth between the MPC nodes. For this reason, it is not suited for use cases demanding high-throughput or for computing HMAC of long messages.
Builder Vault currently only supports HMAC keys shared among three MPC nodes and with a security threshold of one. Contact the Builder Vault support team for more information.
Key Generation
You can generate a new HMAC key among a set of MPC nodes as follows:
keyID, err := client.HMAC().GenerateKey(ctx, sessionConfig, threshold, keyLength, desiredKeyID)
As usual, the sessionConfig
defines the set of MPC nodes among which the resulting AES key should be secret shared and a unique session ID. The threshold
is the security threshold for the key. The keyLength
specifies the byte length of the key to generate. The key length must be between 1 and 256. Finally, desiredKeyID
can be used to give the key a specific ID. If desiredKeyID
is set to the empty string, the MPC nodes will agree on a random key ID for the new key.
To start the MPC key generation, the above method must be called on the SDKs of all the MPC nodes specified in the session configuration. The MPC session will only succeed if they agree on the session configuration, as well as the threshold, keyLength, and desired key ID.
If the MPC operation succeeds, the key ID will be returned to each SDK.
When to Use the Key
If an honest MPC node succeeds with the key generation MPC session, the protocol ensures that the MPC node will hold a correct key share. But the protocol does guarantee that all honest players will succeed. In the worst case, only a few honest players will succeed, too few to actually perform a decryption or signature. Therefore you should make sure that all MPC nodes successfully completed the key generation MPC session before using the new key.
Message Authentication
Given an HMAC key in the Builder Vault with some key ID, you can compute the HMAC-SHA256 or HMAC-SHA512 digests of a byte array data
as follows:
partialResult, err = client.HMAC().HMACSHA512(ctx, sessionConfig, keyID, data)
This will start an MPC session among the MPC nodes defined in the session configuration. If they all agree on the key ID, and the data to be authenticated, the MPC session will succeed and each SDK will receive a partial result. To obtain the final HMAC digest, the partial results must be collected and combined as follows:
digest, err := tsm.HMACFinalize([][]byte{ partialResult1, partialResult2, partialResult3})
A complete example is given below, showing how to generate a 128-bit HMAC . The key is then used to compute a HMAC-SHA256 digest of a message.
The example assumes a running instance of the Builder Vault with three MPC nodes that are reachable at the addresses http://localhost:8500, http://localhost:8501, http://localhost:8502. You can for example follow the tutorial here to set up a Builder Vault instance locally like this, or you can use the hosted sandbox Builder Vault provided by Blockdaemon, as described here.
package main
import (
"context"
"encoding/hex"
"fmt"
"gitlab.com/Blockdaemon/go-tsm-sdkv2/v65/tsm"
"golang.org/x/sync/errgroup"
)
func main() {
// Create a client for each of the nodes
configs := []*tsm.Configuration{
tsm.Configuration{URL: "http://localhost:8500"}.WithAPIKeyAuthentication("apikey0"),
tsm.Configuration{URL: "http://localhost:8501"}.WithAPIKeyAuthentication("apikey1"),
tsm.Configuration{URL: "http://localhost:8502"}.WithAPIKeyAuthentication("apikey2"),
}
clients := make([]*tsm.Client, len(configs))
for i, config := range configs {
var err error
if clients[i], err = tsm.NewClient(config); err != nil {
panic(err)
}
}
// Generate an HMAC key
keyLength := 32 // The key should be a 256-bit key
threshold := 1 // The security threshold for this key
keyGenPlayers := []int{0, 1, 2} // The key should be secret shared among all three MPC nodes
fmt.Println("Generating key using players", keyGenPlayers)
keyGenSessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), keyGenPlayers, nil)
ctx := context.Background()
keyIDs := make([]string, len(clients))
var eg errgroup.Group
for i, client := range clients {
client, i := client, i
eg.Go(func() error {
var err error
keyIDs[i], err = client.HMAC().GenerateKey(ctx, keyGenSessionConfig, threshold, keyLength, "")
return err
})
}
// Validate key ID; make sure none of the nodes aborted during keygen, before you use the key.
if err := eg.Wait(); err != nil {
panic(err)
}
for i := 1; i < len(keyIDs); i++ {
if keyIDs[0] != keyIDs[i] {
panic("key IDs do not match")
}
}
keyID := keyIDs[0]
fmt.Println("Generated key with ID:", keyID)
// Use the key to compute an HMAC message digest
data := []byte("some data to be input to hmac")
hmacSessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), keyGenPlayers, nil)
partialHMACResults := make([][]byte, len(clients))
for i, client := range clients {
client, i := client, i
eg.Go(func() error {
var err error
partialHMACResults[i], err = client.HMAC().HMACSHA256(ctx, hmacSessionConfig, keyID, data)
return err
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
digest, err := tsm.HMACFinalize(partialHMACResults)
if err != nil {
panic(err)
}
fmt.Println("HMAC-SHA256:", hex.EncodeToString(digest))
}
Running this example should result in output similar to this:
Generating key using players [0 1 2]
Generated key with ID: lzjROvftXyYsdPEO2xGJPF8MDMxd
HMAC-SHA256: 7383f56cd98e5a7d8cf066c56e2cf3e481576655ee91e730afbfd7be905df65d
Key Import and Export
HMAC keys can be imported and exported in the same way as AES keys. The only difference is the checksum used to ensure the integrity of the HMAC key sharing to import or export is the first three bytes of the value SHA256("BuilderVault HMAC Key" || key)
, where key
is the HMAC key.
The following is a complete example showing how to import and export an HMAC key with the Builder Vault.
package main
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"fmt"
"gitlab.com/Blockdaemon/go-tsm-sdkv2/v65/tsm"
"gitlab.com/Blockdaemon/go-tsm-sdkv2/v65/tsm/tsmutils"
"golang.org/x/sync/errgroup"
"sync"
)
func main() {
// A 256-bit HMAC key to be imported into Builder Vault
hmacKey, err := hex.DecodeString("e7e34471f8366f1946a5a97dfab895cf4a4561ff9f6665578809a500da9adb7d")
if err != nil {
panic(err)
}
// Split the HMAC key into a secret sharing
players := []int{0, 1, 2} // The key should be secret shared among the MPC three nodes with these player IDs
sharing, err := tsmutils.HMACSecretShare(players, hmacKey)
if err != nil {
panic(err)
}
shares := sharing.Shares // This is the actual shares
checksum := sharing.Checksum // This is the first three bytes of SHA256("BuilderVault HMAC Key" || key)
fmt.Println("Key :", hex.EncodeToString(hmacKey))
fmt.Println("Key checksum :", hex.EncodeToString(checksum))
// Create a client for each of the nodes
configs := []*tsm.Configuration{
tsm.Configuration{URL: "http://localhost:8500"}.WithAPIKeyAuthentication("apikey0"),
tsm.Configuration{URL: "http://localhost:8501"}.WithAPIKeyAuthentication("apikey1"),
tsm.Configuration{URL: "http://localhost:8502"}.WithAPIKeyAuthentication("apikey2"),
}
clients := make([]*tsm.Client, len(configs))
for i, config := range configs {
var err error
if clients[i], err = tsm.NewClient(config); err != nil {
panic(err)
}
}
// Wrap each key share with the wrapping key of the MPC node that should receive the share
ctx := context.Background()
wrappedKeyShares := make([][]byte, len(players))
for i, client := range clients {
// Get the wrapping key from the MPC node
wrappingKey, err := client.WrappingKey().WrappingKey(ctx)
if err != nil {
panic(err)
}
publicKey, err := x509.ParsePKIXPublicKey(wrappingKey)
if err != nil {
panic(err)
}
publicRSAKey, ok := publicKey.(*rsa.PublicKey)
if !ok {
panic("error parsing wrapping key: not an rsa key")
}
// Encrypt the key share using the wrapping key
keyShare := shares[players[i]]
wrappedKeyShares[i], err = tsmutils.Wrap(publicRSAKey, keyShare)
if err != nil {
panic(err)
}
}
// Import the wrapped key shares into the Builder Vault
threshold := 1 // The security threshold that we want for the resulting key sharing
importSessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
keyIDs := map[int]string{}
var eg errgroup.Group
var lock sync.Mutex
for playerID, client := range clients {
playerID, client := playerID, client
eg.Go(func() error {
res, err := client.HMAC().ImportKeyShares(ctx, importSessionConfig, threshold, wrappedKeyShares[playerID], checksum, "")
if err != nil {
return err
}
lock.Lock()
keyIDs[playerID] = res
lock.Unlock()
return nil
})
}
// Validate key ID; make sure none of the nodes aborted during keygen, before you use the key.
if err := eg.Wait(); err != nil {
panic(err)
}
for i := 1; i < len(keyIDs); i++ {
if keyIDs[0] != keyIDs[i] {
panic("key IDs do not match")
}
}
keyID := keyIDs[0]
fmt.Println("Key ID of imported key:", keyID)
// The key can now be used to compute HMAC-SHA256 or HMAC-SHA512
// We can export the AES key from the Builder Vault as follows
// Key shares are wrapped before being returned to the SDKs.
// In a real application, you would probably use different wrapping keys for each MPC node. For example, you would
// use the wrapping key obtained from another instance of the MPC node with the same player ID.
// For the sake of simplicity, we use the same wrapping key for all MPC nodes in this example.
// In any case, the wrapping key must be whitelisted and key export must be enabled in the MPC node configuration,
// before the export can take place.
privateWrappingKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
publicWrappingKey, err := x509.MarshalPKIXPublicKey(&privateWrappingKey.PublicKey)
if err != nil {
panic(err)
}
exportSessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
exportedWrappedKeyShares := make(map[int]*tsm.HMACWrappedKeyShare, len(clients))
for playerID, client := range clients {
playerID, client := playerID, client
eg.Go(func() error {
res, err := client.HMAC().ExportKeyShares(ctx, exportSessionConfig, keyID, publicWrappingKey)
if err != nil {
return err
}
lock.Lock()
exportedWrappedKeyShares[playerID] = res
lock.Unlock()
return nil
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
for i := 1; i < len(keyIDs); i++ {
if !bytes.Equal(exportedWrappedKeyShares[0].Checksum, exportedWrappedKeyShares[i].Checksum) {
panic("checksums do not match")
}
}
exportedKeyChecksum := exportedWrappedKeyShares[0].Checksum
fmt.Println("Exported key checksum: ", hex.EncodeToString(exportedKeyChecksum))
// Given all the key shares and the private wrapping key, you can recover the HMAC key from the shares as follows.
// But for maximum security, you can also just import the wrapped key shares and checksum into another
// Builder Vault instance. This will migrate the key between the Builder Vault instances, without any single point
// of trust.
// Unwrap the wrapped key shares
exportedKeyShares := make(map[int][]byte, 0)
for playerID, wrappedKeyShare := range exportedWrappedKeyShares {
keyShare, err := tsmutils.Unwrap(privateWrappingKey, wrappedKeyShare.WrappedKeyShare)
if err != nil {
panic(err)
}
exportedKeyShares[playerID] = keyShare
}
// Then recover the HMAC key from the key shares
exportedKey, err := tsmutils.HMACRecombine(exportedKeyShares, checksum)
if err != nil {
panic(err)
}
fmt.Println("HMAC key recovered from exported key shares:", hex.EncodeToString(exportedKey))
// Sanity check: Exported key should equal original key
if !bytes.Equal(hmacKey, exportedKey) {
panic("hmac key mismatch")
}
}
Running this should produce output like this:
Key : e7e34471f8366f1946a5a97dfab895cf4a4561ff9f6665578809a500da9adb7d
Key checksum : f3c7cd
Key ID of imported key: 8567OsnjEx61bdtfM0Yxj7rSbsaH
Exported key checksum: f3c7cd
HMAC key recovered from exported key shares: e7e34471f8366f1946a5a97dfab895cf4a4561ff9f6665578809a500da9adb7d
Updated 27 days ago