Secure Unwrap of Keys
Normal unwrap that run as specified in the PCKS #11 standard requires that things are done in a certain way. With regard to the Builder Vault PKCS #11 module this results in the key needs to be decrypted in the PKCS #11 module and then split up into shares and imported into the MPC nodes. This is less than ideal as the the key will be in clear in a single location.
To improve on this behaviour, the PKCS #11 module supports a Vendor Specified unwrap flow, that allows leveraging the normal export/import functionality of Builder Vault to import keys into the MPC nodes without being decrypted in the PKCS #11 module.
Using Secure Unwrap
Secure unwrap requires some steps to set up and use. The general steps needed to perform a Secure Unwrap:
- Export wrapping keys
- Create wrapped key shares
- Make a secure unwrap in the PKCS #11 module
Export wrapping keys
The first step is to get the wrapping keys that need to protect the keys when they are transported to the PKCS #11 module. These are RSA public keys, each of which are bound to one specific MPC node.
To get the wrapping keys using the PKCS #11 interface, a Vendor Defined key generation mechanism (CKM_BLOCKDAEMON_WRAPPING_KEYS_GEN) have been added. This key generation mechanism takes no arguments and creates a Vendor Defined object (CKK_BLOCKDAEMON_WRAPPING_KEYS). This key object contains the following attributes along the generic key attributes (the number in parenthesis are the PKCS #11 defined common attribute footnotes):
| Attribute | Data Type | Meaning |
|---|---|---|
| CKA_UNWRAP (8) | CK_BBOOL | CK_TRUE if key supports unwrapping (i.e., can be used to unwrap other keys) (9) |
| CKA_UNWRAP_TEMPLATE | CK_ATTRIBUTE_PTR | For wrapping keys. The attribute template to apply to any keys unwrapped using this wrapping key. Any user supplied template is applied after this template as if the object has already been created. The number of attributes in the array is the ulValueLen component of the attribute divided by the size of CK_ATTRIBUTE. |
| CKA_BLOCKDAEMON_JSON_WRAPPING_KEYS (4) | RFC2279 string | Contains the JSON encoding of the wrapping keys of the MPC nodes. |
When the wrapping keys object has been generated, the wrapping keys need to be extracted from the attribute on the generated object. An example showing the key generation and extraction of the attribute can be seen here:
// Create the key
CK_BBOOL boolTrue = CK_TRUE;
CK_ATTRIBUTE generateTemplate[] = {
{CKA_TOKEN, &boolTrue, sizeof(boolTrue)},
{CKA_UNWRAP, &boolTrue, sizeof(boolTrue)},
};
CK_ULONG generateCount = sizeof(generateTemplate) / sizeof(CK_ATTRIBUTE);
CK_MECHANISM mech = {
CKM_BLOCKDAEMON_WRAPPING_KEYS_GEN,
NULL_PTR,
0
};
CK_OBJECT_HANDLE wrappingKeysHandle;
CK_RV rv = f->C_GenerateKey(session, &mech, generateTemplate, generateCount, &wrappingKeysHandle);
if (rv != CKR_OK) {
return rv
}
// Extract the wrapping keys from the generated key object
CK_BYTE wrappingKeys[3000];
CK_ATTRIBUTE getTemplate[] = {
{CKA_BLOCKDAEMON_JSON_WRAPPING_KEYS, wrappingKeys, sizeof(wrappingKeys)-1},
};
CK_ULONG getCount = sizeof(getTemplate) / sizeof(CK_ATTRIBUTE);
rv = f->C_GetAttributeValue(session, wrappingKeysHandle, getTemplate, getCount);
if (rv != CKR_OK) {
return rv
}
// The following line is optional, but will add a zero termination to the string.
// This is also the reason for the -1 in the template above as this ensures there is room.
wrappingKeys[getTemplate[0].ulValueLen] = '\0';The byte array in wrappingKeys can be parsed with the following Golang code:
// The input for this code (keyshare can be for other player IDs depending on the config):
wrappingKeys := // JSON exported from the PKCS #11 module
var wrappingKeyMap map[int][]byte
err := json.Unmarshal([]byte(wrappingKeysJSON), &wrappingKeyMap)
if err != nil { panic(err) }The format of the JSON that this parses is:
{
"0":"BASE64Encoding==",
"1":"BASE64Encoding==",
"2":"BASE64Encoding=="
}
NoteAll wrapping keys will be served by the PKCS #11 module if exporting them using the PKCS #11 module. An attacker could potentially replace them here to get access to all the key shares.
If this is a concern, then the wrapping keys can also be exported directly from the MPC nodes by using the SDKs. The exported wrapping keys can either be used directly, or to check if the values exported using the PKCS #11 are the same.
Create Wrapped Key Shares
There are several ways to create the wrapped key shares. They can be exported from a non-PKCS #11 Builder Vault, potentially the same MPC nodes that the PKCS #11 module is running on.
They can also be created elsewhere and split up and encrypted using the utility methods in tsmutils.
There are examples of making wrapped shares in the Key Import and Export section. These are more thorough, but only covers ECDSA and EdDSA, so here are some example for AES and RSA.
The exported shares are collected in a JSON structure that is used for the unwrap. The format of the unwrap JSON is:
{
"wrapped-key-shares":{
"0":"BASE64Encoding==",
"1":"BASE64Encoding==",
"2":"BASE64Encoding=="
},
"check-value":"BASE64Encoding=="
}The check-value is dependent on what type of key is being unwrapped:
- RSA: The public key as an DER encoded Subject Public Key Info (SPKI). This is also referred to as PKIX in Golang.
- AES: The KCV of the key. Note that this is the KCV that Builder Vault uses for export, which is an encryption of a block of 1s. This is opposed to the one that PKCS #11 uses, which is an encryption of 0s.
The JSON can be encoded with the following code, given the shares and the check value:
// Variables with input:
wrappedKeyShares := // Map of wrapped key shares for the different players
checkValue := // The check value as defined for this key type
type WrappedKey struct {
WrappedKeyShares map[int][]byte `json:"wrapped-key-shares"`
CheckValue []byte `json:"check-value"`
}
var jsonStruct WrappedKey
jsonStruct.CheckValue = checkValue
jsonStruct.WrappedKeyShares = make(map[int][]byte)
jsonStruct.WrappedKeyShares[0] = wrappedKeyShares[0]
jsonStruct.WrappedKeyShares[1] = wrappedKeyShares[1]
jsonStruct.WrappedKeyShares[2] = wrappedKeyShares[2]
wrappedKeysJSON, err := json.Marshal(jsonStruct)
if err != nil { panic(err) }The following sub sections will provide code that will create the wrapped key shares. The key shares will be stored in the wrappedKeyShares map and the generated check value set in checkValue.
RSA
Export from Builder Vault
To export from Builder Vault, the following code needs to be run simultaneous against each MPC node:
// Variables with input:
client := // The Builder Vault SDK client
sessionConfig := // Session config for this operation
keyID := // KeyID of the key to export
playerID := // The ID of the player of this client as a string
wrappingKeyMap := // The parsed JSON Wrapping key map
wrappedKeyShares := // The map that contains the wrapped keyshares (output)
wrappedKeyShare, err := client.RSA().ExportKey(context.Background(), sessionConfig, keyID,
wrappingKeyMap[playerID])
if err != nil { panic(err) }
wrappedKeyShares[playerID] = wrappedKeyShare.WrappedKeyShare
checkValue := wrappedKeyShare.PKIXPublicKeyCreating Shares Externally
To build the values from a Golang rsa.PrivateKey (crypto/rsa):
// Variables with input:
privateKey := // The *rsa.Private key to make encrypted shares for
players := // An []int array of players that the key should be split bewteen, e.g. []int{0,1,2}
publicKey := privateKey.Public()
keyShares, err := tsmutils.RSASecretShare(2, players, privateKey)
if err != nil { panic(err) }
wrappedKeyShares := make(map[int][]byte, 3)
for k, v := range keyShares {
wrappingKey, err := x509.ParsePKIXPublicKey(wrappingKeyMap[k])
if err != nil { panic(err) }
rsaWrappingKey, ok := wrappingKey.(*rsa.PublicKey)
if !ok { panic("not an RSA key") }
wrappedKeyShares[k], err = tsmutils.EnvelopeWrap(rsaWrappingKey, v)
if err != nil { panic(err) }
}
checkValue, err := x509.MarshalPKIXPublicKey(publicKey)
if err != nil { panic(err) }AES
Export from Builder Vault
When an AES key is exported, the checksum (checkValue) is included in the exported values. This requires that all nodes start the following code simultaneous:
// Variables with input:
client := // The Builder Vault SDK client
sessionConfig := // Session config for this operation
keyID := // KeyID of the key to export
playerID := // The ID of the player of this client as a string
wrappingKeyMap := // The parsed JSON Wrapping key map
wrappedKeyShares := // The map that contains the wrapped keyshares (output)
wrappedKeyShare, err := client.AES().ExportKey(context.Background(), sessionConfig, keyID,
wrappingKeyMap[playerID])
if err != nil { panic(err) }
wrappedKeyShares[playerID] = wrappedKeyShare.WrappedKeyShare
checkValue := wrappedKeyShare.ChecksumCreating Shares Externally
To build the values from a Golang byte array of an AES key:
// Variables with input:
aesKey := // The []byte key to make encrypted shares for
players := // An []int array of players that the key should be split bewteen, e.g. []int{0,1,2}
keyShares, err := tsmutils.AESSecretShare(players, aesKey)
if err != nil { panic(err) }
wrappedKeyShares := make(map[int][]byte, 3)
for k, v := range sharing.Shares {
wrappingKey, err := x509.ParsePKIXPublicKey(wrappingKeyMap[k])
if err != nil { panic(err) }
rsaWrappingKey, ok := wrappingKey.(*rsa.PublicKey)
if !ok { panic("not an RSA key") }
wrappedKeyShares[k], err = tsmutils.Wrap(rsaWrappingKey, v)
if err != nil { panic(err) }
}
checkValue, err := sharing.Checksum
if err != nil { panic(err) }Using Secure Unwrap
With all the previous pieces in place, there should be a wrappedKeysJSON that contains the wrapped shares and a check-value. With this we can do the secure unwrap in the PKCS #11 module using the vendor defined mechanism CKM_BLOCKDAEMON_MPC_UNWRAP. The secure unwrap can be activated with the following code:
// Variables with input:
wrappedKeysJSON = // Insert the json generated with the wrapped key shares and check value
wrappingKeysHandle = // The handle for the key generated in the first step, or recreated before this
CK_MECHANISM unwrapMechanism = {
CKM_BLOCKDAEMON_MPC_UNWRAP,
NULL_PTR,
0,
};
// This is an example of AES, for RSA an RSA specific template should be used.
// Also other attributes can be set like e.g. CKA_LABEL.
CK_OBJECT_CLASS clazz = CKO_SECRET_KEY;
CK_KEY_TYPE type = CKK_AES;
CK_ULONG keyLength = 32;
CK_ATTRIBUTE unwrapTemplate[] = {
{CKA_CLASS, &clazz, sizeof(clazz)},
{CKA_KEY_TYPE, &type, sizeof(type)},
{CKA_TOKEN, &boolTrue, sizeof(boolTrue)},
{CKA_VALUE_LEN, &keyLength, sizeof(keyLength)},
{CKA_ENCRYPT, &boolTrue, sizeof(boolTrue)}
};
// The -1 after sizeof(wrappedKeysJSON) is needed if the parameter is given as a string,
// otherwise it should be removed.
CK_OBJECT_HANDLE unwrappedKeyHandle;
rv = f->C_UnwrapKey(session, &unwrapMechanism, wrappingKeysHandle, (unsigned char*) wrappedKeysJSON,
sizeof(wrappedKeysJSON)-1, unwrapTemplate, 5, &unwrappedKeyHandle);
if (rv != CKR_OK) {
return rv
}Note that only token objects can be created with the secure unwrap as it unwrap into the MPC nodes directly.
Updated 10 days ago
