Note

This page is a work in progress. It may contain incomplete or incorrect information.

Namespace Key Management

Online Root Namespace Key

By default, the Canton node creates a root namespace key during the initialization of the node. Operators can manually create an intermediate key on the node to authorize topology transactions. Only the intermediate key can be changed. Therefore, it is important to keep the root key secure.

If a Canton node is configured to use a KMS system, then the root key can be administratively isolated, using the KMS Administration controls to remove node access to the root key. As the root key is only required when authorizing a new intermediate key or revoking an existing authorization, it is sufficient to just selectively allow the node to access the key only whenever such operations are performed.

Only the root key can be isolated. All the other keys are essential for Canton to operate correctly. Optionally, the intermediate key can also be isolated but will have to be turned on and off as needed during administrative operations such as uploading DARs or adding parties.

Offline Root Namespace Key

A more secure way to manage the root namespace key is to use an offline root namespace key. In this case, the root namespace key is stored in a secure, possibly air gapped location, inaccessible from the Canton node.

Where the root key is stored depends on your organization’s security requirements. For the remainder of this guide, assume that there are two sites, the online site of the node with its intermediate key and the offline site of the root key. The offline root key is used to sign a delegation to the node’s intermediate key, which is then used to authorize topology transactions.

The offline root key procedure is supported by a set of utility scripts, which can be found in the Canton scripts directory (scripts/offline-root-key).

Channel for Key Exchange

There must be a channel or method which allows to exchange the public intermediate key and the signed intermediate namespace delegations between the online and offline sites. The channel must be trusted in terms of authenticity, but not confidentiality, as no secret information is exchanged. This means you need to ensure that the data is not tampered with during transport, but the data itself does not need to be encrypted.

This can be done using multiple methods, depending on whether the sites are air-gapped or connected through a network. Possible examples are: secure file transfer, QR codes, physical storage medium.

Assuming that such a trusted channel exists, the following steps are required to set up the offline root namespace key:

1. Configure Node To Expect External Root Key

Before the first start-up, the Canton node must be configured not to initialize automatically. This is done by setting

Manual init config
     participant1.init.identity.type = manual

The node can then be started with this configuration. It starts the Admin API, but halts the startup process and wait for the initialization of the node identity together with the necessary topology transactions.

2. Export Public Key of Node

Assuming you have access to the remote console of the node, create a new signing key to use as the intermediate key:

@ val key = participant1.keys.secret.generate_signing_key(name = "NamespaceDelegation", usage = com.digitalasset.canton.crypto.SigningKeyUsage.NamespaceOnly)
key : SigningPublicKey = SigningPublicKey(
  id = 122001e9b2c9...,
  format = DER-encoded X.509 SubjectPublicKeyInfo,
  keySpec = EC-Curve25519,
  usage = namespace
)
@ val intermediateKeyPath = better.files.File.newTemporaryFile(prefix = "intermediate-key.pub").pathAsString
intermediateKeyPath : String = "/tmp/intermediate-key.pub6141875231608017386"
@ participant1.keys.public.download_to(key.id, intermediateKeyPath)

This creates a file with the public key of the intermediate key.

The supported key specifications are listed in the follow protobuf definition:

Signing key specifications
 enum SigningKeySpec {
   SIGNING_KEY_SPEC_UNSPECIFIED = 0;

   // Elliptic Curve Key from Curve25519
   // as defined in http://ed25519.cr.yp.to/
   SIGNING_KEY_SPEC_EC_CURVE25519 = 1;

   // Elliptic Curve Key from the NIST P-256 curve (aka secp256r1)
   // as defined in https://doi.org/10.6028/NIST.FIPS.186-4
   SIGNING_KEY_SPEC_EC_P256 = 2;

   // Elliptic Curve Key from the NIST P-384 curve (aka secp384r1)
   // as defined in https://doi.org/10.6028/NIST.FIPS.186-4
   SIGNING_KEY_SPEC_EC_P384 = 3;

   // Elliptic Curve Key from SECG P256k1 curve (aka secp256k1)
   // commonly used in bitcoin and ethereum
   // as defined in https://www.secg.org/sec2-v2.pdf
   SIGNING_KEY_SPEC_EC_SECP256K1 = 4;
 }

The synchronizer the participant node intends to connect to might restrict further the list of supported key specifications. To obtain this information from the synchronizer directly, run the following command on the synchronizer Public API.

grpcurl -d '{}' <sequencer_endpoint> com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/GetSynchronizerParameters

If a console is not accessible, you can use either a bootstrap script or grpccurl against the Admin API to invoke the commands.

3. Share Public Key of Node with Offline Site

Next, the intermediate public key must be transported to the offline site as described above. Ensure that the public key is not tampered with during transport.

4. Generate Root Key and The Root Certificate

Using OpenSSL

Ensure that the necessary scripts are available on the secure site. These scripts are included in the Canton release packages at scripts/offline-root-key. An example demonstrating usage of those scripts using openssl to generate keys and sign certificates is available at examples/10-offline-root-namespace-init. Run the next set of commands from the examples/10-offline-root-namespace-init directory.

Start by initializing variables used in the snippets below

Script variables
 # Points to the location of the offline root key scripts under the scripts/offline-root-key directory in the release artifact
 SCRIPTS_ROOT="$(dirname "$0")/../../scripts/offline-root-key"
 PRIVATE_KEY="$OUTPUT_DIR/root_private_key.der"
 PUBLIC_KEY="$OUTPUT_DIR/root_public_key.der"
 ROOT_NAMESPACE_PREFIX="$OUTPUT_DIR/root_namespace"
 INTERMEDIATE_NAMESPACE_PREFIX="$OUTPUT_DIR/intermediate_namespace"
mkdir -p tmp/certs
OUTPUT_DIR="tmp/certs"
CANTON_NAMESPACE_DELEGATION_PUB_KEY=<Path to the intermediate key downloaded from Canton. ``intermediateKeyPath`` in this example>

As well as setting the path to the protobuf image containing the required protobuf definitions to generate certificates.

Protobuf paths
 CURRENT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null && pwd)
 export BUF_PROTO_IMAGE="$CURRENT_DIR/../../scripts/offline-root-key/root_namespace_buf_image.bin"
 source "$CURRENT_DIR/../../scripts/offline-root-key/utils.sh"

Then, generate the root key in the secure environment and extract the public key:

Generate key pair with openssl
 openssl ecparam -name prime256v1 -genkey -noout -outform DER -out "$PRIVATE_KEY"
 openssl ec -inform der -in "$PRIVATE_KEY" -pubout -outform der -out "$PUBLIC_KEY" 2> /dev/null

Then, create the self-signed root namespace delegation, which is effectively a self-signed certificate used as the trust anchor of the given namespace:

Prepare root cert
 "$SCRIPTS_ROOT/prepare-certs.sh" --root-delegation --root-pub-key "$PUBLIC_KEY" --target-pub-key "$PUBLIC_KEY" --output "$ROOT_NAMESPACE_PREFIX"

Note that the root public key must be in the x509 SPKI DER format. This generates two files, root-delegation.prep and root-delegation.hash. .prep files contain unsigned topology transactions serialized to bytes. If you really want to be sure what you are signing, inspect the prepare-certs.sh script to see how it generates the topology transaction and how it computes the hash. Next, the hash needs to be signed.

If you are using openssl, the following command can be used to sign the hash:

Sign root cert
 openssl pkeyutl -rawin -inkey "$PRIVATE_KEY" -keyform DER -sign < "$ROOT_NAMESPACE_PREFIX.hash" > "$ROOT_NAMESPACE_PREFIX.signature"

Finally, assemble the signature and the prepared transaction:

Assemble root cert
 "$SCRIPTS_ROOT/assemble-cert.sh" --prepared-transaction "$ROOT_NAMESPACE_PREFIX.prep" --signature "$ROOT_NAMESPACE_PREFIX.signature" --signature-algorithm ecdsa256 --output "$ROOT_NAMESPACE_PREFIX.cert"

This creates a so-called signed topology transaction.

Using GCP KMS

If you are using GCP KMS, you can use KMS CLI (https://cloud.google.com/kms/docs/create-validate-signatures) with the following commands to generate the key:

gcloud kms keyrings create key-ring --location location (APPROXIMATE)

And the following command can be used to generate the signature:

gcloud kms asymmetric-sign \
    --version key-version \
    --key <root-key> \
    --keyring key-ring \
    --location location \
    --digest-algorithm digest-algorithm \
    --input-file root-delegation.prep \
    --signature-file root-delegation.signature

5. Create the Intermediate Certificate

If the root key and the self-signed root delegation are available, you can create the intermediate certificate. The steps are very similar to the root certificate, but the target is the public key of the intermediate key, and the --intermediate-delegation flag is used instead of --root-delegation.

Prepare delegation cert
 "$SCRIPTS_ROOT/prepare-certs.sh" --intermediate-delegation --root-pub-key "$PUBLIC_KEY" --canton-target-pub-key "$CANTON_NAMESPACE_DELEGATION_PUB_KEY" --output "$INTERMEDIATE_NAMESPACE_PREFIX"

Verify that the generated topology transaction (printed to stdout) is correct and refers to the correct keys. Once verified, the generated hash needs to be signed:

Sign delegation cert
 openssl pkeyutl -rawin -inkey "$PRIVATE_KEY" -keyform DER -sign < "$INTERMEDIATE_NAMESPACE_PREFIX.hash" > "$INTERMEDIATE_NAMESPACE_PREFIX.signature"

Again, the signature and the prepared transaction can be assembled:

Assemble delegation cert
 "$SCRIPTS_ROOT/assemble-cert.sh" --prepared-transaction "$INTERMEDIATE_NAMESPACE_PREFIX.prep" --signature "$INTERMEDIATE_NAMESPACE_PREFIX.signature" --signature-algorithm ecdsa256 --output "$INTERMEDIATE_NAMESPACE_PREFIX.cert"

7. Copy the Certificates to the Online Site

The generated certificates (never the root private key) need to be transferred to the online site. The public keys are included in the certificates and don’t need to be transported separately. You need to transfer both certificates, the root delegation and the intermediate delegation, to the online site.

8. Import the Certificates to the Node

On the target site, import the certificates into the waiting node using the console command

@ participant1.topology.init_id(identifier = "participant1", delegationFiles = Seq("tmp/certs/root_namespace.cert", "tmp/certs/intermediate_namespace.cert"))
@ participant1.health.status
res5: NodeStatus[ParticipantStatus] = Participant id: PAR::participant1::12203ea6d3bd5305e1f96503d2f5e8f0f77d104298443fff6b5752b8205644532de9
Uptime: 0.069676s
Ports:
    ledger: 30022
    admin: 30023
Connected synchronizers: None
Unhealthy synchronizers: None
Active: true
Components:
    memory_storage : Ok()
    connected-synchronizer : Not Initialized
    sync-ephemeral-state : Not Initialized
    sequencer-client : Not Initialized
    acs-commitment-processor : Not Initialized
Version: 3.4.0-SNAPSHOT
Supported protocol version(s): 34, dev

Alternatively, the Admin API can be used directly via grpccurl to initialize the node.

Pre-Generated Certificates

The certificates can also be provided directly via the node’s configuration file if they’ve been generated beforehand. In this scenario, instead of generating the intermediate key via the node’s generate_signing_key command as described above, they key must be generated on a KMS and its public key material downloaded. The same scripts can then be used to generate the certificate, with the exception that the intermediate public key will not be in the Canton format but in a DER format and should therefore be set with --target-pub-key. Once the certificates are available, you must register the intermediate KMS key by running:

      val intermediateKey = node.keys.secret
        .register_kms_signing_key(
          intermediateNsKmsKeyId,
          SigningKeyUsage.NamespaceOnly,
          name = s"${node.name}-${SigningKeyUsage.Namespace.identifier}",
        )
      // user-manual-entry-begin: ManualRegisterKmsNamespaceKey
      node.crypto.cryptoPrivateStore
        .existsPrivateKey(intermediateKey.id, Signing)
        .valueOrFail("intermediate key not registered")
        .futureValueUS

      // user-manual-entry-begin: ManualRegisterKmsKeys

      // Register the KMS signing key used to define the node identity.
      val namespaceKey = node.keys.secret
        .register_kms_signing_key(
          namespaceKmsKeyId,
          SigningKeyUsage.NamespaceOnly,
          name = s"${node.name}-${SigningKeyUsage.Namespace.identifier}",
        )

      // Register the KMS signing key used to authenticate the node toward the Sequencer.
      val sequencerAuthKey = node.keys.secret
        .register_kms_signing_key(
          sequencerAuthKmsKeyId,
          SigningKeyUsage.SequencerAuthenticationOnly,
          name = s"${node.name}-${SigningKeyUsage.SequencerAuthentication.identifier}",
        )

      // Register the signing key used to sign protocol messages.
      val signingKey = node.keys.secret
        .register_kms_signing_key(
          signingKmsKeyId,
          SigningKeyUsage.ProtocolOnly,
          name = s"${node.name}-${SigningKeyUsage.Protocol.identifier}",
        )

      // Register the encryption key.
      val encryptionKey = node.keys.secret
        .register_kms_encryption_key(encryptionKmsKeyId, name = node.name + "-encryption")

      // user-manual-entry-end: ManualRegisterKmsKeys

      (namespaceKey, sequencerAuthKey, signingKey, encryptionKey)
    } else {
      // architecture-handbook-entry-begin: ManualInitKeys

      // Create a signing key used to define the node identity.
      val namespaceKey =
        node.keys.secret
          .generate_signing_key(
            name = node.name + s"-${SigningKeyUsage.Namespace.identifier}",
            SigningKeyUsage.NamespaceOnly,
          )

      // Create a signing key used to authenticate the node toward the Sequencer.
      val sequencerAuthKey =
        node.keys.secret.generate_signing_key(
          name = node.name + s"-${SigningKeyUsage.SequencerAuthentication.identifier}",
          SigningKeyUsage.SequencerAuthenticationOnly,
        )

      // Create a signing key used to sign protocol messages.
      val signingKey =
        node.keys.secret
          .generate_signing_key(
            name = node.name + s"-${SigningKeyUsage.Protocol.identifier}",
            SigningKeyUsage.ProtocolOnly,
          )

      // Create the encryption key.
      val encryptionKey =
        node.keys.secret.generate_encryption_key(name = node.name + "-encryption")

      // architecture-handbook-entry-end: ManualInitKeys

      (namespaceKey, sequencerAuthKey, signingKey, encryptionKey)
    }

    // architecture-handbook-entry-begin: ManualInitNode

    // Use the fingerprint of this key for the node identity.
    val namespace = Namespace(namespaceKey.id)
    node.topology.init_id_from_uid(
      UniqueIdentifier.tryCreate("manual-" + node.name, namespace)
    )

    // Wait until the node is ready to receive the node identity.
    node.health.wait_for_ready_for_node_topology()

    // Create the self-signed root certificate.
    node.topology.namespace_delegations.propose_delegation(
      namespace,
      namespaceKey,
      CanSignAllMappings,
    )

    // Assign the new keys to this node.
    node.topology.owner_to_key_mappings.propose(
      OwnerToKeyMapping(
        node.id.member,
        NonEmpty(Seq, sequencerAuthKey, signingKey, encryptionKey),
      ),
      signedBy = Seq(namespaceKey.fingerprint, sequencerAuthKey.fingerprint, signingKey.fingerprint),
    )

    // architecture-handbook-entry-end: ManualInitNode

  }

}

and then import the certificates to the node.

This configuration directive has no effect once the node is initialized and can subsequently be removed.

Delegation Restrictions

You can further restrict the kind of topology transactions a delegation can authorize. The prepare-certs script exposes a --delegation-restrictions flag for that purpose.

Prepare delegation with signing restrictions
 "$SCRIPTS_ROOT/prepare-certs.sh" --delegation-restrictions PARTY_TO_PARTICIPANT,PARTY_TO_KEY_MAPPING --root-pub-key "$PUBLIC_KEY" --canton-target-pub-key "$CANTON_RESTRICTED_PUB_KEY" --output "$RESTRICTED_KEY_NAMESPACE_PREFIX"

The delegation can then be signed and assembled as before. Once the signed certificate is available, load it onto the node:

Load restricted key certificate onto node
 participant1.topology.transactions.load_single_from_file(s"$opensslKeysDirectory/restricted_key_namespace.cert", TopologyStoreId.Authorized)

Rotate the Intermediate Key

Create new Intermediate Key

In order to create another intermediate key, we follow the same steps as before. Create the key on the online site and export it.

Follow the same steps to create a new intermediate delegation for the new intermediate key:

  • copy to secure site

  • generate the intermediate delegation (skip self-signed root delegation as it has already been generated)

  • copy the certificate to the node site

The new intermediate delegation can then be imported into the node as shown here.

Once the new delegation has been imported, the old intermediate key can be revoked.

Revoking the Intermediate Key

To revoke the intermediate key, the root key needs to be used to sign a revocation transaction. The revocation transaction is prepared in the same way as the intermediate delegation:

Prepare revocation certificate
 "$SCRIPTS_ROOT/prepare-certs.sh" --revoke-delegation "$CANTON_NAMESPACE_DELEGATION_TO_REVOKE" --output "$REVOKED_DELEGATION_PREFIX"

The generated hash needs to be signed and then subsequently assembled into a certificate:

Assemble revocation certificate
 openssl pkeyutl -rawin -inkey "$PRIVATE_KEY" -keyform DER -sign < "$REVOKED_DELEGATION_PREFIX.hash" > "$REVOKED_DELEGATION_PREFIX.signature"

On the node site, the revocation certificate can be imported using:

Load revocation certificate onto node
 participant1.topology.transactions.load_single_from_file(s"$opensslKeysDirectory/revoked_delegation.cert", TopologyStoreId.Authorized)

Rotating the Root Namespace Key

You cannot rotate the root namespace key. If you need to discontinue the usage of the namespace, you need to create a new namespace, new parties and participants in that new namespace, and transfer the contracts to the new parties.