- Overview
- Tutorials
- How Tos
- Download
- Install
- Configure
- Secure
- TLS API Configuration
- Configure API Authentication and Authorization with JWT
- Configure API Limits
- Set Resource Limits
- Crypto key management
- Restrict key usage
- Namespace Key Management
- Key management service (KMS) configuration
- Optimize
- Observe
- Operate
- Initializing node identity manually
- Canton Console
- Synchronizer connections
- High Availability Usage
- Manage Daml packages and archives
- Participant Node pruning
- Party Management
- Party replication
- Decentralized party overview
- Setup an External Party
- Ledger API User Management
- Node Traffic Management
- Identity Management
- Upgrade
- Decommission
- Recover
- Troubleshoot
- Explanations
- Reference
Migrate to external key storage with a KMS by exporting the key material¶
A running Participant will have a set of asymmetric long-term keys that it uses to perform cryptographic operations, and to create its namespace. Migrating a live Participant to use a KMS, instead of storing its private keys locally, requires a series of steps: (1) move those keys to the KMS, (2) create a new Participant and configure it to use the KMS with the imported keys, followed by (3) copying the data from the old Participant to the new. This migration is only possible if the private key material is accessible. You must ensure that the private keys you are going to import have not been compromised.
Note
For a decentralized synchronizer, it is better to onboard a new node with new keys rather than migrating existing nodes.
Export Keys¶
You must first export all cryptographic keys, for example, to files:
@ val keyIds = participant1.keys.public.list().map(_.id)
keyIds : Seq[Fingerprint] = Vector(12201ff69b1d..., 1220dbd7853f..., 1220c987884f..., 1220d3c3df93...)
@ val exportedKeys = keyIds.map(keyId => participant1.keys.secret.download(keyId, password = None))
exportedKeys : Seq[com.google.protobuf.ByteString] = Vector(
<ByteString@6da4d925 size=137 contents="\n\204\001\n\201\001\022\177\nD12201ff69b1d24edbf0ee2028a304ea702ee8...">,
<ByteString@57a23d28 size=139 contents="\n\206\001\n\203\001\022\200\001\nD1220dbd7853fa3bfbb897b1107dc160f08ad...">,
<ByteString@5fbcd395 size=139 contents="\n\206\001\n\203\001\022\200\001\nD1220c987884f0e6fbf4700e113ec1d7a3f8c...">,
<ByteString@7f1d9043 size=238 contents="\n\351\001\022\346\001\022\343\001\nD1220d3c3df9375deddf4b1d4fbee45a18d22...">
)
@ val parsedKeys = exportedKeys.map { keyPair =>
CryptoKeyPair.fromTrustedByteString(keyPair).fold(
err => throw new RuntimeException(s"Failed to parse key: $err"),
cryptoKey => cryptoKey
)
}
parsedKeys : Seq[CryptoKeyPair[PublicKey, PrivateKey]] = Vector(
SigningKeyPair(
publicKey = SigningPublicKey(
id = 12201ff69b1d...,
format = DER-encoded X.509 SubjectPublicKeyInfo,
keySpec = EC-Curve25519,
usage = namespace
),
privateKey = SigningPrivateKey(
id = 12201ff69b1d...,
format = DER-encoded PKCS #8 PrivateKeyInfo,
key = <ByteString@7fa8f87a size=48 contents="0.\002\001\0000\005\006\003+ep\004\"\004 \344u\377#\215\r\354\366\211\305Y\375\235\277\371\004bF\021\036\240;\263X\214\312\037?U~\344C">,
keySpec = EC-Curve25519,
usage = Set(namespace)
)
),
SigningKeyPair(
publicKey = SigningPublicKey(
id = 1220dbd7853f...,
format = DER-encoded X.509 SubjectPublicKeyInfo,
keySpec = EC-Curve25519,
usage = Set(sequencer-auth, proof-of-ownership)
),
privateKey = SigningPrivateKey(
id = 1220dbd7853f...,
format = DER-encoded PKCS #8 PrivateKeyInfo,
key = <ByteString@7e234b01 size=48 contents="0.\002\001\0000\005\006\003+ep\004\"\004 ?3\035\"n\237\357`\370\017*\230\223\326\236b\027(\360\037\264\351J\335d)\270o\355\353\345\336">,
keySpec = EC-Curve25519,
usage = Set(sequencer-auth, proof-of-ownership)
)
),
SigningKeyPair(
publicKey = SigningPublicKey(
id = 1220c987884f...,
format = DER-encoded X.509 SubjectPublicKeyInfo,
keySpec = EC-Curve25519,
usage = Set(signing, proof-of-ownership)
),
privateKey = SigningPrivateKey(
id = 1220c987884f...,
format = DER-encoded PKCS #8 PrivateKeyInfo,
key = <ByteString@31da5ee6 size=48 contents="0.\002\001\0000\005\006\003+ep\004\"\004 \366\270>G\003T\a\204I)\267\004\032\325sb\322\305\026\211\025A\207\231\204\225\311\216\327\203\375\310">,
keySpec = EC-Curve25519,
usage = Set(signing, proof-of-ownership)
)
),
EncryptionKeyPair(
publicKey = EncryptionPublicKey(
id = 1220d3c3df93...,
format = DER-encoded X.509 SubjectPublicKeyInfo,
keySpec = EC-P256
),
privateKey = EncryptionPrivateKey(
id = 1220d3c3df93...,
format = DER-encoded PKCS #8 PrivateKeyInfo,
key = <ByteString@1caed242 size=150 contents="0\201\223\002\001\0000\023\006\a*\206H\316=\002\001\006\b*\206H\316=\003\001\a\004y0w\002\001\001\004 \321\307\376\344\206.b\200f4\270...">,
keySpec = EC-P256
)
)
)
@ val privateKeys = parsedKeys.map { parsedKey => parsedKey.privateKey match {
case EncryptionPrivateKey(id, _, key, _) => (id, key)
case SigningPrivateKey(id, _, key, _, _) => (id, key)
}
}
privateKeys : Seq[(Fingerprint, com.google.protobuf.ByteString)] = Vector(
(
12201ff69b1d...,
<ByteString@7fa8f87a size=48 contents="0.\002\001\0000\005\006\003+ep\004\"\004 \344u\377#\215\r\354\366\211\305Y\375\235\277\371\004bF\021\036\240;\263X\214\312\037?U~\344C">
),
(
1220dbd7853f...,
<ByteString@7e234b01 size=48 contents="0.\002\001\0000\005\006\003+ep\004\"\004 ?3\035\"n\237\357`\370\017*\230\223\326\236b\027(\360\037\264\351J\335d)\270o\355\353\345\336">
),
(
1220c987884f...,
<ByteString@31da5ee6 size=48 contents="0.\002\001\0000\005\006\003+ep\004\"\004 \366\270>G\003T\a\204I)\267\004\032\325sb\322\305\026\211\025A\207\231\204\225\311\216\327\203\375\310">
),
(
1220d3c3df93...,
<ByteString@1caed242 size=150 contents="0\201\223\002\001\0000\023\006\a*\206H\316=\002\001\006\b*\206H\316=\003\001\a\004y0w\002\001\001\004 \321\307\376\344\206.b\200f4\270...">
)
)
@ import java.nio.file.{Files, Paths}
@ privateKeys.foreach{ case (keyId, privateKey) => Files.write(Paths.get(s"key_${keyId.unwrap}"), privateKey.toByteArray)}
When exporting keys, make sure to copy the output from `keyIds` into a file, as this information is needed again during import. Note that in this case, setting a password on the exported key cannot be used, because the key is immediately parsed and must be imported into the KMS, and both AWS and GCP KMS do not support password-based encryption.
Securing Keys with a Wrapping Key¶
Before importing these keys into a KMS, you should protect them for transport by encrypting them with a wrapping key provided by the KMS into which you wish to import the keys. Both AWS and GCP provide a way to securely import user-supplied cryptographic keys by encrypting them with a wrapping key:
AWS KMS <https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys-encrypt-key-material.html>
GCP KMS <https://docs.cloud.google.com/kms/docs/key-wrapping>
Import Keys to a KMS¶
Importing keys into a KMS must be done manually through the available APIs. Please ensure that your keys have the expected format and are supported by the KMS.
Currently, the two KMSs supported by Canton (apart from any custom KMS implementations) — AWS KMS and GCP KMS — support
all key schemes used by Canton, except for the ``ECIES`` encryption key scheme. Since ECIES encryption keys are
not supported, a new key (e.g., RSA) must be used. This process is explained in more detail
in Using new keys.
Below are instructions for importing keys into:
AWS KMS <https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys.html>
GCP KMS <https://docs.cloud.google.com/kms/docs/key-import>
Take note of the KMS key identifier that was assigned for each imported key so that you can use it later.
Create a new Participant with KMS-enabled¶
After all keys have been successfully imported into the KMS, you must create and start a new Participant. When registering the keys with the KMS, it is important to use all the information from the key export, including metadata, usage flags, and any associated identifiers, to ensure the keys are correctly recognized and functional. The section Enable external key storage with a KMS explains how to configure a Participant to use a KMS for storing and operating on private keys, while the section Run with manually-generated keys explains how to start a Participant with pre-existing KMS keys.
Replicate data¶
Finally, you must transfer all parties and active contracts from the old Participant to the new one. Since the identities of the two Participants are the same, you do not need to modify any of the data.
First, recreate all parties from the old Participant on the new Participant:
val parties = participantOld.parties.list().map(_.party)
parties.foreach { party =>
participantNew.topology.party_to_participant_mappings
.propose(
party = party,
newParticipants = Seq(participantNew.id -> ParticipantPermission.Submission),
store = daId,
)
}
// Disconnect from new KMS-compatible synchronizer to prepare migration of parties and contracts
participantNew.synchronizers.disconnect(newKmsSynchronizerAlias)
Next, transfer the active contracts of all parties from the old Participant to the new one. You can then reconnect to the Synchronizer:
val parties = participantOld.parties.list().map(_.party)
// Make sure synchronizer and the old participant are quiet before exporting ACS
participantOld.synchronizers.disconnect(oldSynchronizerAlias)
oldSynchronizerMediator.stop()
oldSynchronizerSequencer.stop()
File.usingTemporaryFile("participantOld-acs", suffix = ".txt") { acsFile =>
val acsFileName = acsFile.toString
val ledgerEnd = NonNegativeLong.tryCreate(participantOld.ledger_api.state.end())
// Export from old participant
participantOld.repair.export_acs(
parties = parties.toSet,
exportFilePath = acsFileName,
ledgerOffset = ledgerEnd,
contractSynchronizerRenames = Map(oldSynchronizerId.logical -> newKmsSynchronizerId.logical),
)
// Import to new participant
participantNew.repair.import_acs(acsFileName)
}
// Kill/stop the old participant
participantOld.stop()
// Connect the new participant to the new synchronizer
participantNew.synchronizers.reconnect(newSynchronizerAlias)
The result is a new Participant with the same namespace, whose private keys are stored and managed by a KMS, connected to the same Synchronizer as before.
Warning
AWS and GCP KMS do not allow the retrieval of private keys. Therefore, if you want to be able to revert to not using a KMS or to change KMS providers, you must persist your keys in a secure location elsewhere (e.g., an encrypted offline backup stored under strict access controls), in particular your root namespace key. There is currently no automatic revert process. You must follow a similar procedure: obtain the keys, manually initialize a new node, transfer data.
Using new keys¶
Creating new keys directly in the KMS is not possible because the keys must first be announced on the network. Therefore, the only way to start using a new KMS-backed participant with fresh keys is either to create them before migrating the node to the KMS or to rotate the keys after the migration is complete using:
val newSigningKeyParticipant = participant1.keys.secret
.rotate_kms_node_key(
keyFingerprint,
newKmsKeyId,
"kms_key_rotated",
)
You cannot use or rotate the namespace root key, because this key is responsible for identifying a Participant and creating its namespace.
If you want to use new keys, you must manually generate or rotate them using a scheme that Canton supports. The tables here provide information about the currently supported crypto schemes in Canton. Currently, the only crypto scheme that a KMS-backed Participant does not support is encryption with ECIES-HMAC-SHA256-AES128-CBC, and instead it uses RSA-OAEP-SHA256.
For example, since ECIES keys cannot be imported into one of our supported KMS providers, the operator of the original non-KMS Participant must create a new RSA-2048 encryption key and announce it as the Participant’s new encryption key. Only after this step can you migrate to a KMS-enabled Participant.
The migration process then proceeds in the same way, except that the new target Participant running with a KMS must ensure it is configured with the correct schemes, in line with the new key that was created. For example, if you want encryption keys to use RSA-2048, you must add the following configuration:
canton.participants.participant1.crypto.encryption.algorithms.default = rsa-oaep-sha-256
canton.participants.participant1.crypto.encryption.keys.default = rsa-2048