Restrict key usage

This page explains how to limit the usage of cryptographic keys of a Canton node to specific purposes, as best practice to contain damage of potentially compromised keys.

Signing key usage

Canton defines the following key usages for signing keys:

  • Namespace: a key that defines a node’s identity and signs topology requests (see namespace key management)

  • SequencerAuthentication: a key that authenticates members of the network towards a sequencer

  • Protocol: a key that signs various structures as part of the synchronization protocol execution

Restrict the usage of a signing key

Set the usage parameter with the desired usages when you generate a new key.

For example, use the following command to generate a signing key restricted to Protocol usage only:

@ val myKey = myNode.keys.secret.generate_signing_key(
      name = "mySigningKey",
      usage = SigningKeyUsage.ProtocolOnly,
    )
myKey : SigningPublicKey = SigningPublicKey(
  id = 122070511b47...,
  format = DER-encoded X.509 SubjectPublicKeyInfo,
  keySpec = EC-Curve25519,
  usage = Set(signing, proof-of-ownership)
)

Some predefined sets are available for common usages: NamespaceOnly, SequencerAuthenticationOnly, ProtocolOnly, All. Individual usages can otherwise be combined in a Set, but this is not recommended to avoid that keys are used for multiple purposes.

Note

Once specified during key generation, the set of usages is fixed and cannot be modified during the lifetime of that key. Instead, a new key must be generated and activated, and the old one deactivated. See key management for more information regarding the key management commands.

If you are using a Key Management Service (KMS) to handle Canton’s keys in external key storage mode, and want to use manually generated keys that are already stored in the KMS, set the usage parameter with the desired usages when you register the keys with Canton:

node.keys.secret.register_kms_signing_key(
  kmsKeyId,
  usage = SigningKeyUsage.ProtocolOnly,
  name = s"${node.name}-signing-new",
)

The returned key can be used in other key management commands.

Determine the usage of an existing signing key

To find what usage a signing key has been assigned, use the following command:

@ val intermediateKey = myNode.keys.public.list(
      filterFingerprint = myKey.id.unwrap,
    )
intermediateKey : Seq[PublicKeyWithName] = Vector(
  SigningPublicKeyWithName(
    publicKey = SigningPublicKey(
      id = 122070511b47...,
      format = DER-encoded X.509 SubjectPublicKeyInfo,
      keySpec = EC-Curve25519,
      usage = Set(signing, proof-of-ownership)
    ),
    name = Some(KeyName(mySigningKey))
  )
)

If you omit the filterFingerprint parameter, all the existing keys will be returned.

Determine the existing keys with a given usage

To find all the signing keys that have been assigned a given usage, use the following command:

@ val intermediateKey = myNode.keys.public.list(
      filterUsage = SigningKeyUsage.ProtocolOnly,
    )
intermediateKey : Seq[PublicKeyWithName] = Vector(
  SigningPublicKeyWithName(
    publicKey = SigningPublicKey(
      id = 122070511b47...,
      format = DER-encoded X.509 SubjectPublicKeyInfo,
      keySpec = EC-Curve25519,
      usage = Set(signing, proof-of-ownership)
    ),
    name = Some(KeyName(mySigningKey))
  ),
  SigningPublicKeyWithName(
    publicKey = SigningPublicKey(
      id = 1220ed3a8a56...,
      format = DER-encoded X.509 SubjectPublicKeyInfo,
      keySpec = EC-Curve25519,
      usage = Set(signing, proof-of-ownership)
    ),
    name = Some(KeyName(mediator1-signing))
  )
)

All the keys that have been assigned at least one of the usages specified in filterUsage will be returned.

Namespace key delegation restrictions

As described in the identity management page, Canton uses topology transactions to manage identities, and these transactions are signed by keys with Namespace usage. Namespace key management shows that you can create intermediate keys to sign topology transactions, thereby limiting exposure of the root namespace key. These intermediate keys can be further restricted in terms of which kind of topology transactions they can sign using fine-grained delegation restrictions.

Canton defines the following restrictions for namespace keys:

// the key can sign all currently known mappings and all mappings that will be added in future releases
message CanSignAllMappings {}
// the key can sign all currently known mappings and all mappings that will be added in future releases, except for
// namespace delegations
message CanSignAllButNamespaceDelegations {}
// the key can only sign the explicitly specified mappings
message CanSignSpecificMappings {
  repeated Enums.TopologyMappingCode mappings = 1;
}

Restrict the types of mappings that a key can sign

You must first create a new key with at least Namespace usage, then delegate the signing privileges for a namespace to that key, specifying the desired restrictions in the delegationRestriction parameter.

For example, in the following, the first command creates an intermediate key for the root namespace, then the second one delegates the signing privileges to it, allowing it to sign all types of topology mappings except namespace delegations themselves:

@ val intermediateKey = myNode.keys.secret.generate_signing_key(
      name = "intermediate-key",
      usage = SigningKeyUsage.NamespaceOnly,
    )
intermediateKey : SigningPublicKey = SigningPublicKey(
  id = 12200a176d27...,
  format = DER-encoded X.509 SubjectPublicKeyInfo,
  keySpec = EC-Curve25519,
  usage = namespace
)
@ myNode.topology.namespace_delegations.propose_delegation(
      namespace = myNode.namespace,
      targetKey = intermediateKey,
      delegationRestriction = CanSignAllButNamespaceDelegations,
    )
res5: SignedTopologyTransaction[TopologyChangeOp, NamespaceDelegation] = SignedTopologyTransaction(
  TopologyTransaction(
    NamespaceDelegation(
      122009299340...,
      SigningPublicKey(
        id = 12200a176d27...,
        format = DER-encoded X.509 SubjectPublicKeyInfo,
        keySpec = EC-Curve25519,
        usage = namespace
      ),
      CanSignAllButNamespaceDelegations
    ),
    serial = 1,
    operation = Replace
  ),
  signatures = 122009299340...
)

Like all topology mappings, the delegation restrictions on a namespace key can be updated. To do so, issue a new propose_delegation command with the updated delegation restrictions.

Warning

If you define intermediate namespace keys with restricted delegations, you must ensure that all authorizations are covered and that by combining multiple namespace keys together, they can authorize all topology transactions. Otherwise, some operational procedures may fail. For example, rotating a key will fail if there is not at least one namespace key that can authorize OwnerToKey mappings.

In such cases, the operation will fail with a “Could not find an appropriate signing key to issue the topology transaction” error message and code TOPOLOGY_NO_APPROPRIATE_SIGNING_KEY_IN_STORE.

Determine the restrictions of an existing namespace key

To find what signing restrictions have been defined on a namespace key, use the following command:

@ myNode.topology.namespace_delegations.list(
      store = TopologyStoreId.Authorized,
      filterTargetKey = Some(intermediateKey.id),
    )
res6: Seq[topology.ListNamespaceDelegationResult] = Vector(
  ListNamespaceDelegationResult(
    context = BaseResult(
      storeId = Authorized,
      validFrom = 2025-07-17T22:20:03.987545Z,
      validUntil = None,
      sequenced = 2025-07-17T22:20:03.987545Z,
      operation = Replace,
      transactionHash = <ByteString@3cfe1da5 size=34 contents="\022 \347\373\000\025Z\265\325\335\023\025\233\235\373\355\b\343x7\223\276\030X*\352\200H\026\324\276V\236\236">,
      serial = PositiveNumeric(value = 1),
      signedBy = Vector(122009299340...)
    ),
    item = NamespaceDelegation(
      122009299340...,
      SigningPublicKey(
        id = 12200a176d27...,
        format = DER-encoded X.509 SubjectPublicKeyInfo,
        keySpec = EC-Curve25519,
        usage = namespace
      ),
      CanSignAllButNamespaceDelegations
    )
  )
)

If you omit the filterTargetKey parameter, all the existing namespace delegations will be returned.