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 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:

  1. Export wrapping keys
  2. Create wrapped key shares
  3. 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:

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.
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.
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):
wavingKeys := // 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=="
}

Note

All 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.

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:

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.PKIXPublicKey

Creating 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:

wrappedKeyShare, err := client.AES().ExportKey(context.Background(), sessionConfig, keyID, wrappingKeyMap[playerID]) if err != nil { panic(err) }

wrappedKeyShares[playerID] = wrappedKeyShare.WrappedKeyShare checkValue := wrappedKeyShare.Checksum


#### Creating Shares Externally

To build the values from a Golang byte array of an AES key:

```go
// 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) }

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,
};
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)},
};
CK_OBJECT_HANDLE unwrappedKeyHandle;
rv = f->C_UnwrapKey(session, &unwrapMechanism, wrappingKeysHandle, (unsigned char*) wrappedKeysJSON,
                    sizeof(wrappingKeysJSON)-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.