Backing Up Key Shares
Every share generated and stored inside the Vertex can be exported and backed up for safekeeping. For added flexibility, at startup the Vertex can be configured to either export the shares "raw" or alternatively to encrypt them using a configured key.
Encryption Scheme
The Vertex uses a hybrid encryption scheme combining RSA-OAEP-SHA256 for asymmetric key encapsulation and ChaCha20-Poly1305 for symmetric data encryption and authentication.
How the Hybrid Encryption Works
- Key Encapsulation: A random 32-byte Content Encryption Key (CEK) is generated and encrypted using RSA-OAEP-SHA256 with the provided public key (required to be at least 3072 bits long)
- Data Encryption: The actual key share data is encrypted using ChaCha20-Poly1305 with the CEK.
- Result: The final encrypted blob includes the encrypted CEK, nonce (IV), ciphertext, and authentication tag.
For more details on how to implement decryption of these blobs in your own systems, see the Decrypting Backup Shares section below.
Configuring Backup Settings for the Vertex
When deploying the Vertex, you can configure it to encrypt exported shares and decrypt imported shares using the following command-line arguments (configure directly in the terraform):
--backup-public-key: PEM-encoded RSA public key for encrypting exported shares.--backup-public-key-scheme: Encryption scheme (currently only supports"RSA_OAEP_SHA256_CHACHA20_POLY1305").--backup-public-key-format: The format the public key is provided in (defaults toplain-text)--import-private-key: PEM-encoded RSA private key for decrypting imported shares.--import-private-key-scheme: Decryption scheme (currently only supports"RSA_OAEP_SHA256_CHACHA20_POLY1305").--import-private-key-format: The format the private key is provided in (defaults toplain-text)
Note that encryption and decryption capabilities are configured separately:
- If you configure only a backup public key, you can export encrypted shares but cannot import encrypted shares.
- If you configure only an import private key, you can import encrypted shares but cannot export encrypted shares.
- You can configure both to enable both functionalities.
- If neither is configured, shares will be exported and imported in plaintext.
Configure Backup Settings in Terraform
When deploying the Vertex using the provided Terraform module for Azure and GCP, you can configure the Vertex with public/private keys via the following Terraform variables:
- Azure
- GCP
- AWS
Both private and public keys should be stored in PEM format in an Azure Key Vault as a Secret.
If you want to enable encryption of the exported shares, you need to provide a public key using the following Terraform variables:
server_share_backup_public_encryption_key_pem_secret_name- the name of the secret holding the public key for the server share backup.server_share_backup_public_encryption_key_pem_secret_vault_name- the name of the key vault where the public key secret exists in.server_share_backup_public_encryption_key_pem_secret_rg_name- the name of the resource group that the key vault is in.server_share_backup_public_encryption_key_scheme- the name of the encryption scheme.
If you want to enable decryption of imported shares, you need to provide a private key using the following Terraform variables:
server_share_backup_private_decryption_key_pem_secret_name- the name of the secret holding the private key for the server share backup.server_share_backup_private_decryption_key_pem_secret_vault_name- the name of the key vault where the private key secret exists in.server_share_backup_private_decryption_key_pem_secret_rg_name- the name of the resource group that the key vault is in.server_share_backup_private_decryption_key_scheme- the name of the decryption scheme.
Both private and public keys should be stored in PEM format in a GCP Secret Manager.
If you want to enable encryption of the exported shares, you need to provide a public key using the following Terraform variable:
server_share_backup_public_encryption_key_secret_name- the name of the secret holding the public key for the server share backup.server_share_backup_public_encryption_key_secret_scheme- the name of the encryption scheme.
If you want to enable decryption of imported shares, you need to provide a private key using the following Terraform variable:
server_share_backup_private_decryption_key_secret_name- the name of the secret holding the private key for the server share backup.server_share_backup_private_decryption_key_secret_scheme- the name of the decryption scheme.
Both private and public keys should be stored in PEM format in an AWS Secrets Manager.
If you want to enable encryption of the exported shares, you need to provide a public key using the following Terraform variable:
server_share_backup_public_encryption_key_secret_name- the name of the secret holding the public key for the server share backup.server_share_backup_public_encryption_key_secret_scheme- the name of the encryption scheme.
If you want to enable decryption of imported shares, you need to provide a private key using the following Terraform variable:
server_share_backup_private_decryption_key_secret_name- the name of the secret holding the private key for the server share backup.server_share_backup_private_decryption_key_secret_scheme- the name of the decryption scheme.
Exporting a Key Share
Exporting a key share can be done using the export-key-share endpoint:
curl -L -X POST 'https://<YOUR_VERTEX>/ecdsa/backup/export-key-share' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: <USER_API_KEY>' \
--data-raw '{
"encrypted": <BOOL>,
"key_id": "<KEY_ID>"
}'
This operation takes either a key_id or a key_name and exports its associated key share. The encrypted parameter determines whether the export is encrypted or not:
- If you've configured a backup public key, you must set
encrypted: true. - If you haven't configured a backup public key, you must set
encrypted: false.
The response includes the key share data in hexadecimal format:
{
"key_share": "04a8b6c7d..."
}
For more information about generating keys, refer to the Generating Keys section.
Importing a Key Share
Importing a key share can be done using the import-key endpoint:
curl -L -X POST 'https://<YOUR_VERTEX>/ecdsa/backup/import-key-share' \
-H 'Content-Type: application/json' \
-H 'Authorization: <USER_API_KEY>' \
-H 'Accept: application/json' \
--data-raw '{
"encrypted": <BOOL>,
"key_share": "<EXPORTED_KEY_SHARE_HEX>",
"key_name": "<OPTIONAL_CUSTOM_NAME>"
}'
This operation takes a key share (in raw or encrypted form) and imports it into the Vertex. The parameters are:
encrypted: Whether the key share is encrypted (must betrueif you're importing an encrypted share).key_share: The hexadecimal representation of the key share.key_name(optional): A custom name for the imported key.
This operation will create a new key_id for the imported share. If you want your user to start using the imported share, make sure you are using this new keyid. This operation _does not delete the original share.
Importing a Key Share for Recovery
If you set up a backup mechanism through which you have access to an exported share and want to use it for recovery purposes, you can import the share using the same process as above. The following snippet assumes the following scenario:
- A client and a server, each holding a share.
- Every time a client generates a share, it also saves an encrypted exported backup of the server's share (using the
export-keyendpoint). - The potential disaster: the server and all of its data (shares included) are completely lost. A new server is instantiated, configured with the same encryption key as before, and communicating under the same domain.
Given this scenario, the client code below will seamlessly recover the share from the exported backup and continue to sign messages as before:
async function main(): Promise<EcdsaSignature> {
// Given we've already generated a key and have our exported share as `exported_share`.
try {
// Attempt to sign the message with the original key.
// `sign` is a function that signs a message using the given `key_id` and throws a native `Response` object in case of an error.
const signature = await sign({ key_id: user_key_id, message: msg });
return signature;
} catch (error: unknown) {
if (error instanceof Response) {
const error_body = await error.json();
// `key_id_not_exists` is a custom error type that indicates the key share is not found.
if (error_body.err_type === "key_id_not_exists") {
// We will import the exported share and sign the message with the newly created key share.
const import_result = await import_key_share(exported_share);
const signature = await sign({
key_id: import_result.key_id,
message: msg,
});
return signature;
}
} else {
throw error;
}
}
}
In the example above we attempt to sign a message with the original key share. If the key share is not found, we import the exported share and sign the message with the newly created key share.
Note the special error the Vertex will throw when the key share is not found - key_id_not_exists. In this case we can attempt to recover our exported share and reuse it.
Decrypting Backup Shares
If you need to decrypt backup shares outside of the Vertex system (for example, for disaster recovery or auditing), you can implement the decryption process using standard cryptographic libraries. The Vertex uses a hybrid encryption scheme combining RSA-OAEP-SHA256 and ChaCha20-Poly1305.
Key Generation
To generate an RSA key pair suitable for use with the backup system:
- OpenSSL
- Python
- TypeScript
# Generate a private key (minimum 3072 bits recommended)
openssl genrsa -out private_key.pem 3072
# Export public key in PEM format (to configure in Vertex)
openssl rsa -in private_key.pem -pubout -out public_key.pem
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
# Generate private key (minimum 3072 bits recommended)
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=3072
)
# Get public key
public_key = private_key.public_key()
# Export as PEM format (for configuration)
public_key_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
print(f"Public Key (PEM format):\n{public_key_pem.decode('utf-8')}")
import * as crypto from "crypto";
// Generate key pair (minimum 3072 bits recommended)
function generateRSAKeyPair(keySizeBits: number = 3072): {
privateKey: crypto.KeyObject;
publicKey: crypto.KeyObject;
} {
if (keySizeBits < 3072) {
throw new Error("RSA key size must be at least 3072 bits");
}
return crypto.generateKeyPairSync("rsa", {
modulusLength: keySizeBits,
publicExponent: 65537,
});
}
// Generate the key pair
const { privateKey, publicKey } = generateRSAKeyPair(3072);
// Export public key in PEM format (for configuration)
const publicKeyPem = publicKey.export({
type: "spki",
format: "pem",
}) as string;
console.log(`Public Key (PEM format):\n${publicKeyPem}`);
Decrypting a Backup
To decrypt a backup share exported from the Vertex:
- Python
- TypeScript
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
import binascii
import math
def decrypt_backup(hex_input, private_key):
# Calculate RSA key size in bytes (using ceiling division)
rsa_modulus_size_bytes = (private_key.key_size + 7) // 8
# Convert hex to bytes
input_bytes = binascii.unhexlify(hex_input)
# Ensure input is long enough
assert len(input_bytes) >= rsa_modulus_size_bytes + 12 + 16, "Input too short"
# Parse components
encrypted_cek = input_bytes[:rsa_modulus_size_bytes]
nonce = input_bytes[rsa_modulus_size_bytes:rsa_modulus_size_bytes + 12]
ciphertext = input_bytes[rsa_modulus_size_bytes + 12:-16]
auth_tag = input_bytes[-16:]
# Decrypt the CEK with RSA-OAEP
decrypted_cek = private_key.decrypt(
encrypted_cek,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
assert len(decrypted_cek) == 32, "Decrypted CEK must be 32 bytes"
# Decrypt the data with ChaCha20-Poly1305
chacha = ChaCha20Poly1305(decrypted_cek)
plaintext = chacha.decrypt(nonce, ciphertext + auth_tag, None) # No AAD
return plaintext
import * as crypto from "crypto";
function decryptBackup(hexInput: string, privateKey: crypto.KeyObject): Buffer {
// Convert hex to buffer
const inputBuffer = Buffer.from(hexInput, "hex");
// Calculate RSA key size in bytes (using ceiling division)
const rsaModulusSizeBytes = Math.ceil(
privateKey.asymmetricKeyDetails.modulusLength! / 8
);
// Check minimum length
const minLength = rsaModulusSizeBytes + 12 + 16; // RSA block + nonce + tag
if (inputBuffer.length < minLength) {
throw new Error(`Input too short: expected at least ${minLength} bytes`);
}
// Parse components
const encryptedCek = inputBuffer.subarray(0, rsaModulusSizeBytes);
const nonce = inputBuffer.subarray(
rsaModulusSizeBytes,
rsaModulusSizeBytes + 12
);
const ciphertextEnd = inputBuffer.length - 16;
const ciphertext = inputBuffer.subarray(
rsaModulusSizeBytes + 12,
ciphertextEnd
);
const authTag = inputBuffer.subarray(ciphertextEnd);
// Decrypt the CEK with RSA-OAEP
const decryptedCek = crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
},
encryptedCek
);
if (decryptedCek.length !== 32) {
throw new Error(
`Decrypted CEK has invalid length: ${decryptedCek.length} (expected 32)`
);
}
// Decrypt the data with ChaCha20-Poly1305
const decipher = crypto.createDecipheriv(
"chacha20-poly1305",
decryptedCek,
nonce,
{
authTagLength: 16,
}
);
decipher.setAuthTag(authTag);
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
}