diff --git a/docs-main/global-synchronizer/reference/kms-driver-guide.mdx b/docs-main/global-synchronizer/reference/kms-driver-guide.mdx index c7a1babd5..2fc8bdfc1 100644 --- a/docs-main/global-synchronizer/reference/kms-driver-guide.mdx +++ b/docs-main/global-synchronizer/reference/kms-driver-guide.mdx @@ -107,15 +107,20 @@ trait DriverFactory extends api.DriverFactory { `v1.KmsDriverFactory` is a specialization of the generic DriverFactory which defines the driver type as a KmsDriver and the API version as 1. ```scala -// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.digitalasset.canton.crypto.kms.driver.api +package com.digitalasset.canton.crypto.kms.driver.api.v1 + +import com.digitalasset.canton.crypto.kms.driver.api +import com.digitalasset.canton.driver.api.v1 + +trait KmsDriverFactory extends api.KmsDriverFactory with v1.DriverFactory { -import com.digitalasset.canton.driver.api.DriverFactory + override val version: Int = 1 -trait KmsDriverFactory extends DriverFactory { override type Driver <: KmsDriver + } ``` @@ -143,12 +148,224 @@ The API is designed as an asynchronous API using Futures. An OpenTelemetry trace Concretely the Scala interface is defined as the following: ```scala -// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.digitalasset.canton.crypto.kms.driver.api +package com.digitalasset.canton.crypto.kms.driver.api.v1 + +import com.digitalasset.canton.crypto.kms.driver.api +import io.opentelemetry.context.Context + +import scala.concurrent.Future + +/** The interface for a pluggable KMS implementation, that is, a KMS Driver. + * + * Cryptographic operations are asynchronous, i.e., they return a Future. In case of failure, the + * Future must fail with a [[KmsDriverException]]. Transient failures should still fail the Future, + * but the exception's `retryable` flag should be set to true. An exception should only be thrown + * for driver operations (e.g., sign), and not, for example, for health checks. + * + * Each KMS operation takes an OpenTelemetry [[io.opentelemetry.context.Context]] as a trace + * context that can optionally be propagated to the external KMS. + */ +trait KmsDriver extends api.KmsDriver with AutoCloseable { + + require( + supportedSigningKeySpecs.nonEmpty, + "Supported signing key specifications must not be empty.", + ) + require( + supportedSigningAlgoSpecs.nonEmpty, + "Supported signing algorithm specifications must not be empty.", + ) + require( + supportedEncryptionKeySpecs.nonEmpty, + "Supported encryption key specifications must not be empty.", + ) + require( + supportedEncryptionAlgoSpecs.nonEmpty, + "Supported encryption algorithm specifications must not be empty.", + ) + + /** Returns the current health of the driver. The driver should not throw an exception; instead, + * it should return a [[com.digitalasset.canton.crypto.kms.driver.api.v1.KmsDriverHealth]] value. + * + * @return + * A future that completes with the driver's health. + */ + def health: Future[KmsDriverHealth] + + /** The supported signing key specifications by the driver. This must not be empty. */ + def supportedSigningKeySpecs: Set[SigningKeySpec] + + /** The supported signing algorithm specifications by the driver. This must not be empty. */ + def supportedSigningAlgoSpecs: Set[SigningAlgoSpec] + + /** The supported encryption key specifications by the driver. This must not be empty. */ + def supportedEncryptionKeySpecs: Set[EncryptionKeySpec] + + /** The supported encryption algorithm specifications by the driver. This must not be empty. */ + def supportedEncryptionAlgoSpecs: Set[EncryptionAlgoSpec] + + /** Generate a new signing key pair. + * + * @param signingKeySpec + * The key specification for the new signing key pair. The caller ensures it is a + * [[supportedSigningKeySpecs]]. + * @param keyName + * An optional descriptive name for the key pair, max 300 characters long. + * + * @return + * A future that completes with the unique KMS key identifier, max 300 characters long. + */ + def generateSigningKeyPair( + signingKeySpec: SigningKeySpec, + keyName: Option[String], + )(traceContext: Context): Future[String] + + /** Generate a new asymmetric encryption key pair. + * + * @param encryptionKeySpec + * The key specification of the new encryption key pair. The caller ensures it is a + * [[supportedEncryptionKeySpecs]]. + * @param keyName + * An optional descriptive name for the key pair, max 300 characters long. + * + * @return + * A future that completes with the unique KMS key identifier, max 300 characters long. + */ + def generateEncryptionKeyPair( + encryptionKeySpec: EncryptionKeySpec, + keyName: Option[String], + )(traceContext: Context): Future[String] + + /** Generate a new symmetric encryption key. The default symmetric key specification of the KMS is + * used. + * + * @param keyName + * An optional descriptive name for the symmetric key, max 300 characters long. + * + * @return + * A future that completes with the unique KMS key identifier, max 300 characters long. + */ + def generateSymmetricKey(keyName: Option[String])(traceContext: Context): Future[String] + + /** Sign the given data using the private key identified by the keyId with the given signing + * algorithm specification. If the `algoSpec` is not compatible with the key spec of `keyId` then + * this method must fail with a non-retryable exception. + * + * @param data + * The data to be signed with the specified signature algorithm. The upper bound of the data + * size is 4kb. + * @param keyId + * The identifier of the private signing key. + * @param algoSpec + * The signature algorithm specification. The caller ensures it is a + * [[supportedSigningAlgoSpecs]]. + * + * @return + * A future that completes with the signature. + */ + def sign(data: Array[Byte], keyId: String, algoSpec: SigningAlgoSpec)( + traceContext: Context + ): Future[Array[Byte]] + + /** Asymmetrically decrypt the given ciphertext using the private key identified by the keyId with + * the given asymmetric encryption algorithm specification. If the `algoSpec` is not compatible + * with the key spec of `keyId` then this method must fail with a non-retryable exception. + * + * @param ciphertext + * The asymmetrically encrypted ciphertext that needs to be decrypted. The length of the + * ciphertext depends on the parameters of the asymmetric encryption algorithm. Implementations + * may assume that the length of the ciphertext is at most 6144 bytes in any case. + * @param keyId + * The identifier of the private encryption key to perform the asymmetric decryption with. + * @param algoSpec + * The asymmetric encryption algorithm specification. The caller ensures it is a + * [[supportedEncryptionAlgoSpecs]]. + * + * @return + * A future that completes with the plaintext. + */ + def decryptAsymmetric( + ciphertext: Array[Byte], + keyId: String, + algoSpec: EncryptionAlgoSpec, + )(traceContext: Context): Future[Array[Byte]] + + /** Symmetrically encrypt the given plaintext using the symmetric encryption key identified by the + * keyId. The same/default symmetric encryption algorithm of the KMS must be used for both + * symmetric encryption and decryption. + * + * @param data + * The plaintext to symmetrically encrypt. The upper bound of the data size is 4kb. + * @param keyId + * The identifier of the symmetric encryption key. + * + * @return + * A future that completes with the ciphertext. + */ + def encryptSymmetric(data: Array[Byte], keyId: String)(traceContext: Context): Future[Array[Byte]] + + /** Symmetrically decrypt the given ciphertext using the symmetric encryption key identified by + * the keyId. The same/default symmetric encryption algorithm of the KMS must be used for both + * symmetric encryption and decryption. + * + * @param ciphertext + * The ciphertext to symmetrically decrypt. The upper bound of the ciphertext size is 6144 + * bytes. + * @param keyId + * The identifier of the symmetric encryption key. + * + * @return + * A future that completes with the plaintext. + */ + def decryptSymmetric(ciphertext: Array[Byte], keyId: String)( + traceContext: Context + ): Future[Array[Byte]] + + /** Exports a public key from the KMS for the given key pair identified by keyId. + * + * @param keyId + * The identifier of the key pair. + * + * @return + * A future that completes with the exported [[PublicKey]] + */ + def getPublicKey(keyId: String)(traceContext: Context): Future[PublicKey] + + /** Asserts that the key given by its identifier exists and is active. + * + * @param keyId + * The identifier of the key to be checked. + * + * @return + * A future that completes successfully if the key exists and is active. Otherwise, the future + * must have been failed. + */ + def keyExistsAndIsActive(keyId: String)(traceContext: Context): Future[Unit] -trait KmsDriver + /** Deletes a key given by its identifier from the KMS. + * + * @param keyId + * The identifier of the key to be deleted. + * + * @return + * A future that completes when the key has been deleted or the deletion of the key has been + * scheduled. + */ + def deleteKey(keyId: String)(traceContext: Context): Future[Unit] + +} + +/** A public key exported from the KMS. + * + * @param key + * The DER-encoded X.509 public key (SubjectPublicKeyInfo). EC keys must be uncompressed. + * @param spec + * The key specification of the key pair + */ +final case class PublicKey(key: Array[Byte], spec: KeySpec) ``` ### Error Handling and Health