Identity Management

On-ledger identity management focuses on the distributed aspect of identities between Canton system entities, while user identity management focuses on participants individually managing access of their users to their ledger-apis.

Canton comes with a built in identity management system used to manage on-ledger identities. The technical details are explained in the architecture section, while this write up here is meant to give a high level explanation.

The identity management system is self-contained and built without a trusted central entity or pre-defined root certificate such that anyone can connect with anyone, without the need of some central approval and without the danger of losing self-sovereignty.

Introduction

What is a Canton Identity?

When two system entities such as participant, domain identity manager, mediator or sequencer communicate with each other, they will use asymmetric cryptography to encrypt messages and sign message contents such that only the recipient can decrypt the content and to ensure that the recipient can verify the authenticity of the message, or prove the origin of a certain message. Therefore, we need a method to uniquely identify the system entities and a way to associate encryption and signing keys with them.

On top of that, Canton uses the contract language DAML, which represents contract ownership and rights through parties. But parties are not primary members of the Canton synchronisation protocol. They are represented by participants and therefore we need to uniquely identify parties and relate them to participants, such that a participant can represent several parties (and in Canton, a party can be represented by several participants).

Unique Identifier

A Canton identity is built out of two components: a random string X and a fingerprint of a public key N. This combination is called a unique identifier (X,N) and is assumed to be globally unique by design. This unique identifier is used in Canton to refer to particular parties, participants or domain entities, and as such, a system entity (or party) is the combination of role (party, participant, mediator, sequencer, domain identity manager) and a unique identifier.

The system entities require knowledge about the keys which will be used for encryption and signing by the respective other entities and this knowledge needs to be shared among all affected system entities. This knowledge is distributed and therefore, the system entities require a way to verify that a certain association of an entity with a key is correct and valid. This is the purpose of the fingerprint of a public key in the unique identifier, which is referred to as Namespace. And the secret key of the corresponding namespace acts as the root of trust for that particular namespace, as explained later.

Identity Transactions

In order to remain flexible and be able to change keys and cryptographic algorithms, we don’t identify the entities using a single static key, but we need a way to dynamically associate participants or domain entities with keys and parties with participants. We do this through identity transactions.

An identity transaction establishes a certain association of a unique identifier with either a key or a relationship with another identifier. There are several different types of identity transactions. The most general one is the OwnerToKeyMapping, which as the name says, associates a key with a unique identifier. Such an identity transaction will inform all other system entities that a certain system entity is using a specific key for a specific purpose, such as participant Alice of namespace 12345.. is using the key identified through the fingerprint AABBCCDDEE.. to sign messages.

Now, this poses two questions: who authorizes these transactions? and who distributes them?

For the authorization, we need to look at the second part of the unique identifier, the Namespace. An identity transaction that refers to a particular unique identifier operates on that namespace and we require that such an identity transaction is authorized by the corresponding secret key through a cryptographic signature of the serialised identity transaction. This authorization can be either direct, if it is signed by the secret key of the namespace, or indirect, if it is signed by a delegated key. In order to delegate the signing right to another key, there are other identity transactions of type NamespaceDelegation or IdentifierDelegation that allow to do that. A namespace delegation allows to delegate entire namespaces to a certain key, such as saying the key identifier through the fingerprint AABBCCDDEE… is now allowed to authorize identity transactions within the namespace of the key VVWWXXYYZZ…. An identifier delegation allows to delegate authority over a certain identifier to a key, which means that the delegation key can only authorize identity transactions that act on a specific identifier and not the entire namespace.

Now, signing of identity transactions happens in an IdentityManager. Canton has many identity managers. In fact, every participant node and every domain have identity managers with exactly the same functional capabilities, just different impact. They can create new keys, new namespaces and the identity of new participants, parties and even domains. And they can export these identity transactions such that they can be imported at another identity manager. This allows to manage Canton identities in quite a wide range of ways. A participant can operate his own identity manager which allows him individually to manage his parties. Or he can associate himself with another identity manager and let him manage the parties that he represents or keys he uses. Or something in between, depending on the introduced delegations and associations.

The difference between the domain identity manager and the participant identity manager is that the domain identity manager establishes the valid identity state in a particular domain by distributing identity transactions in a way that every domain member ends up with the same identity state. However, he is just a gate keeper of the domain that decides who is let in and who not on that particular domain, but the actual identity statements originate from various sources. As such, the domain identity manager can only block the distribution, but he can not fake identity transactions.

The participant identity manager only manages an isolated identity state. However, there is a dispatcher attached to this particular identity manager that attempts to register locally registered identities with remote domains, by sending them to the domain identity managers, who then decide on whether they want to include them or not.

The careful reader will have noted that the described identity system indeed does not have a single root of trust or decision maker on who is part of the overall system or not. But also that the identity state for the distributed synchronisation varies from domain to domain, allowing very flexible topologies and setups.

Life of a Party

In the tutorials, we use the participant.parties.enable("name") function to setup a party on a participant. To understand the identity management system in Canton, it helps to look at the steps under the hood of how a new party is added:

  1. The participant.parties.enable function determines the unique identifier of the participant: participant.id.
  2. The party name is built as name::<namespace>, where the ``namespace` is the one of the participant.
  3. A new party to participant mapping is authorized on the admin-api: participant.identity.party_to_participant_mappings.authorize(...)
  4. The ParticipantIdentityManager gets invoked by the GRPC request, creating a new SignedIdentityTransaction and tests whether the authorization can be added to the local identity state. If it can, the new identity transaction is added to the store.
  5. The ParticipantIdentityDispatcher picks up the new transaction and requests the addition on all domains via the RegisterIdentityTransactionRequest domain service per domain.
  6. A domain receives this request and processes it according to the policy (reject, queue, approve). The default setting is approve for convenience during development.
  7. The approve policy attempts to add the new identity transaction to the DomainIdentityManager.
  8. The DomainIdentityManager checks whether the new identity transaction can be added to the domain identity state. If yes, it gets written to the local identity store.
  9. The DomainIdentityDispatcher picks up the new transaction and sends it to all participants (and back to itself) through the sequencer.
  10. The sequencer timestamps the transaction and embeds it into the transaction stream.
  11. The participants receive the transaction, verify the integrity and correctness against the identity state and add it to the state with the timestamp of the sequencer, such that everyone has a synchronous identity state.

Note that the participant.parties.enable macro only works if the participant controls his namespace himself, either directly by having the namespace key or through delegation (via NamespaceDelegation).

Participant Onboarding

Key to support topological flexibility is that participants can easily be added to new domains. Therefore, the on-boarding of new participants to domains needs to be secure but convenient. Looking at the console command, we note that in most examples, we are using the connect command to connect a participant to a domain. The connect command just wraps a set of admin-api commands:

      val certificates = OptionUtil.emptyStringAsNone(certificatesPath).map { path =>
        BinaryFileUtil.readByteStringFromFile(path) match {
          case Left(err) => throw new IllegalArgumentException(s"failed to load ${path}: ${err}")
          case Right(bs) => bs
        }
      }
      DomainConnectionConfig(domainAlias,
                             connection,
                             manualConnect = manualConnect,
                             essentialState,
                             certificates,
                             priority,
                             sequencerSelectionPolicy = sequencerSelectionPolicy)
      register(config.copy(manualConnect = true))
      if (!config.manualConnect) {
        confirm_agreement(config.domain)
        reconnect(config.domain, waitUntilPackagesReady)
        // now update the domain settings to auto-connect
        modify(config)
      }

We note that from a user perspective, all that needs to happen by default is to provide the connection information and accepting the terms of service (if required by the domain) to set up a new domain connection. There is no separate on-boarding step performed, no giant certificate signing exercise happens, everything is set up during the first connection attempt. However, quite a few steps happen behind the scenes. Therefore, we briefly summarise the process here step by step:

  1. The administrator of an existing participant needs to invoke the domains.register command to add a new domain. The mandatory arguments are a domain alias (used internally to refer to a particular connection) and the domain connection URL (http or https) including an optional port http[s]://hostname[:port]/path. Optional are a certificates path for a custom TLS certificate chain (otherwise the default jre root certificates are used) and the essential state of a domain. The essential state contains the current domain entity keys.
  2. The participant will contact the DomainService and check if using the domain service requires the signing of specific terms of services. If required, the terms of service will be displayed to the user and an approval will be locally stored at the participant for later. If approved, the participant will attempt to connect to the domain.
  3. The participant opens a GRPC channel to the DomainService.
  4. The participant verifies that the remote domain is running a protocol version compatible with the participant’s version using the DomainService.handshake. If the participant runs an incompatible protocol version, the connection will fail.
  5. The participant will download the essential state from the domain. The essential state contains the identity transactions of the domain entities and the domain-id. It is required by the participant in order to ensure that it talking to the correct domain. If the state has been provided previously during the domains.register call (or in a previous session), the two states will be compared. If they are not equal, the connection will fail. If the essential state was not provided during the domains.register call, the participant will use the one downloaded. We assume here that the essential state is obtained by the participant through a secure channel such that it is sure to be talking to the right domain. Therefore, this secure channel can be either something happening outside of Canton or can be provided by TLS during the first time we contact a domain.
  6. The participant will download the domain parameters, which are the parameters used for the transaction protocol on the particular domain. This happens on every re-connect.
  7. The participant sets up an identity pusher which is the process that tries to push all identity transactions created at the participant node’s identity manager to the domain identity manager. If the participant is using its identity manager to manage its identity on its own, these transactions contain all the information about the participant node keys and the registered parties.
  8. The domain receives the set of identity transactions of the participant node through the DomainIdentityService.RegisterIdentityTransaction. The registration service inspects the validity of the transactions and decides based on the configured domain on-boarding policy. The currently supported policies are auto-accept, queue, reject. While auto-accept is convenient for permission-less systems and for development, it will accept any new participant and any identity transaction. The queue policy will store the new requested identity transaction in a store and let the domain administrator decide on whether a certain identity transaction will be accepted or not. The policy reject will just reject any identity transaction by default. Therefore, whether a participant can join a domain or not is a decision of the domain operator. Canton does not make any assumption on how and when a domain operator decides, but lets the operator implement any process. Note that the currently implemented policies are experimental features so far with limited documentation and the final version will allow domain operators to define their own identity transaction request processing policy rather than having to pick from a pre-defined set.
  9. The auto-accept policy auto-approves all identity transactions as long as they are properly authorised and adds them to the domain identity state. If a new participant appears, the participant is automatically enabled.
  10. When a participant is (re-)enabled, the domain identity dispatcher analyses the set of identity transactions the participant has missed before. It sends these transactions to the participant via the sequencer, before publicly enabling the participant. Therefore, when the participant starts to read messages from the sequencer, the initially received messages will be the identity state of the domain.
  11. Now, as the participant is properly enabled on the domain and its signing key is known, the participant can subscribe to the SequencerService with its identity. In order to do that and in order to verify the authorisation of any action on the SequencerService, the participant requires to obtain an authorization token from the domain. For this purpose, the participant requests a challenge from the domain. The domain will provide it with a nonce and the fingerprint of the key to be used for authentication. The participant signs this nonce (together with the domain id) using the corresponding private key. The reason for the fingerprint is simple: the participant needs to sign the token using the participants signing key as defined by the domain identity state. However, as the participant will learn the true domain identity state only by reading from the sequencer service, it can not know what the key is. Therefore, the domain discloses this part of the domain identity state as part of the authorisation challenge.
  12. Using the created authentication token, the participant starts to use the SequencerService. On the domain side, the domain verifies the authenticity and validity of the token by verifying that the token is the expected one and is signed by the participant’s signing key.
  13. As mentioned above, the first set of messages received by the participant through the sequencer will contain the domain identity state, which includes the signing keys of the domain entities. These messages are signed by the sequencer and identity manager and the only way to verify the correctness of these messages is by knowing these keys beforehand, which is exactly the purpose of the essential state.
  14. Once the initial identity transactions have been read, the participant is ready to process transactions and send commands.

Default Initialization

The default initialization behaviour of participant and domain nodes is to run their own identity manager. This provides a convenient, automatic way to configure the nodes and make them usable without manual intervention, but it can be turned off by setting the auto-init = false configuration option before the first startup.

During the auto initialization, the following steps will happen:

  1. On the domain, we generate four signing keys: one for the namespace and one each for the sequencer, mediator and identity manager. On the participant, we create a namespace key, a signing key and an encryption key for the participant.
  2. Using the fingerprint of the namespace, we generate the participant identity. For understandability, we use the node name used in the configuration file. This will change into a random identifier for privacy reasons. Once we’ve generated it, we set it using the set_id admin-api call.
  3. We create a root certificate as NamespaceDelegation using the namespace key, signing with the namespace key.
  4. Then, we create an OwnerToKeyMapping for the participant or domain entities.

Identity Setup Guide

As explained, Canton nodes auto-initialise themselves by default, running their own identity managers. This is convenient for development and prototyping. Actual deployments require more care and therefore, this section should serve as a brief guideline.

Canton identity managers have one crucial task they must not fail at: do not lose access to or control of the root of trust (namespace keys). Any other key problem can somehow be recovered by revoking an old key and issuing a new owner to key association. Therefore, it is advisable that participants and parties are associated with a namespace managed by an identity manager that has sufficient operational setups to guarantee the security and integrity of the namespace.

Therefore, a participant or domain can

  1. Run his own identity manager with his identity namespace key as part of the participant node.
  2. Run his own identity manager on a detached computer in a self-built setup that exports identity transactions and transports them to the respective node (i.e. via burned CD roms).
  3. Ask a trusted identity manager to issue a set of identifiers within the trusted identity managers namespace as delegations and import the delegations to the local participant identity manager.
  4. Let a trusted identity manager manage all the identity state on-behalf.

Obviously, there are more combinations and options possible, but these options here describe some common options with different security and recoverability options.

In order to reduce the risk of losing namespace keys, additional keys can be created and allowed to operate on a certain namespace. In fact, we recommend doing this and avoid storing the root key on a live node.

User Identity Management

Up to here, we covered how on ledger identities are managed. However, every participant needs to manage the access to his local ledger-api and be able to permission applications to read or write to that api on behalf of one or more parties. This is done through the appropriate authorization configuration in the ledger-api configuration section.

The default implemented authorization will be based on JWT token inspection which can be used with OAuth2, but custom authorization methods can be implemented as a plugin.

Note

This is currently being delivered as an upstream feature and will be exposed as soon as it is available.

Cookbook

Adding a new Party to a Participant

The simplest operation is adding a new party to a participant. For this, we add it normally at the identity manager of the participant, which in the default case is part of the participant node. There is a simple macro to enable the party on a given participant if the participant is running his own identity manager:

    val name = "Gottlieb"
    participant1.parties.enable(name)

This will create a new party in the namespace of the participants identity manager.

And there is the corresponding disable macro:

    participant1.parties.disable(name)

The macros themselves just use identity.party_to_participant_mappings.authorize to create the new party, but add some convenience such as automatically determining the parameters for the authorize call.

Note

Please note that the participant.parties.enable macro will add the parties to the same namespace as the participant is in. It only works if the participant has authority over that namespace either by possessing the root or a delegated key.

Party on two Nodes

Assuming we have party (“Jesper”, N1) which we want to host on two participants: (“participant1”, N1) and (“participant2”, N2). In this case, we have the party “Jesper” in namespace N1, whereas the participant2 is in namespace N2. Therefore, we first need to enable the party on the first node, and then we need to authorize the mapping of the party to the participant on both identity managers, as given in below code snippet.

    // enable party on participant1 (will invoke identity.party_to_participant_mappings.authorize) under the hood
    val partyId = participant1.parties.enable("Jesper")
    val p2id    = participant2.id

    // authorize mapping of Jesper to P2 on the identity manager of Jesper
    participant1.identity.party_to_participant_mappings.authorize(
      IdentityChangeOp.Add,
      None, // Key used to authorize this transaction will be automatically determined
      party = partyId, // party unique identifier
      participant = p2id, // participant unique identifier
      side = RequestSide.From, // request side is From if signed by the party idm, To if signed by the participant idm.
      permission = ParticipantPermission.Submission // optional argument defaulting to `Submission`.
    )
    // authorize mapping of Jesper to P2 on the identity manager of P2
    participant2.identity.party_to_participant_mappings.authorize(
      IdentityChangeOp.Add,
      None,
      partyId,
      p2id,
      side = RequestSide.To,
      permission = ParticipantPermission.Submission
    )

Please note however that this currently only works for newly permissioned parties as we don’t yet support migrating the current active contract set.

Note that we can restrict the permission of the node by setting the appropriate ParticipantPermission in the authorization call to either Observation or Confirmation instead of the default Submission. This allows to create setups where a party is hosted with Submission permissions on one node and Observation on another to increase the liveness of the system.

Note

The distinction between Submission and Confirmation is only enforced in the participant node. A malicious participant node with Confirmation permission for a certain party can submit transactions in the name of the party. This is due to Canton’s high level of privacy where validators may not learn the identity of the submitting participant. Therefore, a party who delegates Confirmation permissions to a participant should trust the participant sufficiently.

Move Secret Key to Offline Storage

An identity is ultimately bound to a particular secret key. Owning that secret key gives full authority over the entire namespace. From a security standpoint, it is therefore critical to keep the namespace secret key confidential. This can be achieved by moving the key off the node for offline storage. The identity management system can still be used by creating a new key and an appropriate intermediate certificate. The following steps illustrate how:

    // fingerprint of namespace giving key
    val participantId = participant1.id
    val namespace     = participantId.uid.namespace.fingerprint

    // create new key
    val context     = "new-identity-key"
    val fingerprint = participant1.keys.secret.generate(Set(KeyPurpose.Signing), context).publicKey.fingerprint

    // create an intermediate certificate authority through a namespace delegation
    // we do this by adding a new namespace delegation for the newly generated key
    // and we sign this using the root namespace key
    participant1.identity.namespace_delegations.authorize(IdentityChangeOp.Add, Some(namespace), namespace, fingerprint)

    // export namespace key to file for offline storage, in this example, it's a temporary file
    val privateKeyFile = java.io.File.createTempFile("namespace", ".key")
    participant1.keys.secret.export(namespace, Some(privateKeyFile.getAbsolutePath))

    // delete namespace key (very dangerous ...)
    participant1.keys.secret.delete(namespace, force = true)

Later on, when the root namespace key is required for some operations, it can be imported again on the original node or on another, using the following steps:

    // import it back wherever needed
    other.keys.secret.load(privateKeyFile.getAbsolutePath, "newly-imported-identity-key")

Rolling Keys

Canton supports rolling keys during live operation. This works with any key, signing or encryption. In order to ensure continuous operation, we first need to add the new key and then recall the previous key.

    // Create a new signing key
    val newKey = node.keys.secret.generate(Set(KeyPurpose.Signing), name)

    // Authorize the new key as the new signing key
    // The result is that the owner will now have two signing keys, but by convention always the first one added is
    // used by everybody.
    node.identity.owner_to_key_mappings.authorize(IdentityChangeOp.Add,
                                                  None,
                                                  owner,
                                                  newKey.publicKey.fingerprint,
                                                  Set(KeyPurpose.Signing))

    // wait until new key has appeared everywhere (only relevant if you roll the sequencer key)
    eventually() {
      node.keys.public
        .list_by_owner(owner)
        .flatMap(_.keys.map(_.fingerprint)) should contain(newKey.publicKey.fingerprint)
    }

    // Remove old key by sending the matching `Remove` transaction
    node.identity.owner_to_key_mappings.authorize(IdentityChangeOp.Remove,
                                                  None,
                                                  owner,
                                                  currentFingerprint,
                                                  Set(KeyPurpose.Signing))