diff --git a/UNRELEASED.md b/UNRELEASED.md index f492d03cea..c6c1f6656f 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -48,6 +48,13 @@ The Ledger API update service now exposes a `GetUpdateByHash` endpoint. Given a - `.replication.connection-pool.connection.client-connection-check-interval` is introduced that allows configuring the PostgreSQL-specific `client_connection_check_interval` parameter for DB locked connections. This is a safety mechanism to prevent hanging connections in case of network issues. The default value is 5 seconds. +- GCP KMS configuration gains a new `key-version-overrides` map that lets operators specify a custom + cryptoKey version per cryptoKey id (e.g. `key-version-overrides = { "my-imported-key" = "3" }`). + Previously, Canton always used version `"1"`. Keys not listed in the map continue to default to + version `"1"`, so existing configurations are unchanged. This is useful for cryptoKeys whose key + material was [imported into GCP KMS](https://cloud.google.com/kms/docs/importing-a-key), where each + import creates a new cryptoKey version and multiple versions can be relevant even for asymmetric + keys. - BREAKING: Removed the `protocolVersion` parameter from all `.topology..list` console commands as it was not working properly. - *BREAKING*: `kms-driver-api` and `kms-driver-testing` are now published to Maven Central, and will no longer be available in Artifactory. - Connection pool metrics: diff --git a/community/base/src/main/scala/com/digitalasset/canton/config/KmsConfig.scala b/community/base/src/main/scala/com/digitalasset/canton/config/KmsConfig.scala index 2d0dff0311..39e2f71140 100644 --- a/community/base/src/main/scala/com/digitalasset/canton/config/KmsConfig.scala +++ b/community/base/src/main/scala/com/digitalasset/canton/config/KmsConfig.scala @@ -107,6 +107,14 @@ object KmsConfig { * retry configuration * @param endpointOverride * the [optional] endpoint for a proxy to be used by the KMS client. + * @param keyVersionOverrides + * per-cryptoKey overrides for the cryptoKey version that Canton should use when interacting + * with that key. Each map entry is ` -> `. Keys not listed default to + * version `"1"`, which matches how Canton creates new cryptoKeys (it never adds a new + * version to an existing one). This is intended for cryptoKeys whose key material was + * [imported into GCP KMS](https://cloud.google.com/kms/docs/importing-a-key), where each + * import creates a new cryptoKey version and multiple versions can be relevant even for + * asymmetric keys. */ final case class Gcp( locationId: String, @@ -115,6 +123,7 @@ object KmsConfig { auditLogging: Boolean = false, override val retries: RetryConfig = RetryConfig(), endpointOverride: Option[String] = None, + keyVersionOverrides: Map[String, String] = Map.empty, ) extends KmsConfig object Gcp { diff --git a/community/base/src/main/scala/com/digitalasset/canton/crypto/kms/gcp/GcpKms.scala b/community/base/src/main/scala/com/digitalasset/canton/crypto/kms/gcp/GcpKms.scala index 80233a3234..858d063bf9 100644 --- a/community/base/src/main/scala/com/digitalasset/canton/crypto/kms/gcp/GcpKms.scala +++ b/community/base/src/main/scala/com/digitalasset/canton/crypto/kms/gcp/GcpKms.scala @@ -73,10 +73,27 @@ class GcpKms( private lazy val loggerKms = new GcpRequestResponseLogger(config.auditLogging, loggerFactory) - /* Identifies the version for all asymmetric GCP keys. Canton always opts to generate a new keys - * rather than adding a new version for that key. - */ - private val gcpKeyversion = "1" + /** Resolves the GCP cryptoKey version Canton should use for the given [[KmsKeyId]]. If the + * key id is listed in [[KmsConfig.Gcp.keyVersionOverrides]], its mapped version is returned; + * otherwise [[GcpKms.defaultGcpKeyVersion]] (`"1"`) is returned, matching Canton's behavior + * of never adding a new version to a cryptoKey it created. + */ + private def keyVersionFor(keyId: KmsKeyId): String = + config.keyVersionOverrides.getOrElse(keyId.unwrap, GcpKms.defaultGcpKeyVersion) + + private def cryptoKeyVersionName(keyId: KmsKeyId): gcp.CryptoKeyVersionName = + gcp.CryptoKeyVersionName.of( + config.projectId, + config.locationId, + config.keyRingId, + keyId.unwrap, + keyVersionFor(keyId), + ) + + private def cryptoKeyName(keyId: KmsKeyId): gcp.CryptoKeyName = + // Symmetric keys: GCP picks the primary version on encrypt and recovers it from the + // ciphertext on decrypt, so no per-key version is needed here. + gcp.CryptoKeyName.of(config.projectId, config.locationId, config.keyRingId, keyId.unwrap) private val errorMessagesToRetry = Set( @@ -295,14 +312,7 @@ class GcpKms( ec: ExecutionContext, tc: TraceContext, ): EitherT[FutureUnlessShutdown, KmsError, gcp.PublicKey] = { - val keyVersionName = - gcp.CryptoKeyVersionName.of( - config.projectId, - config.locationId, - config.keyRingId, - keyId.unwrap, - gcpKeyversion, - ) + val keyVersionName = cryptoKeyVersionName(keyId) loggerKms.withLogging[gcp.PublicKey]( loggerKms.getPublicKeyRequestMsg(keyId.unwrap), publicKey => loggerKms.getPublicKeyResponseMsg(keyId.unwrap, publicKey.getAlgorithm.name), @@ -453,13 +463,7 @@ class GcpKms( ec: ExecutionContext, tc: TraceContext, ): EitherT[FutureUnlessShutdown, KmsError, ByteString6144] = { - val keyName = - gcp.CryptoKeyName.of( - config.projectId, - config.locationId, - config.keyRingId, - keyId.unwrap, - ) + val keyName = cryptoKeyName(keyId) val encryptionAlgorithm = CryptoKeyVersionAlgorithm.GOOGLE_SYMMETRIC_ENCRYPTION for { dataEnc <- loggerKms.withLogging[ByteString]( @@ -496,13 +500,7 @@ class GcpKms( ec: ExecutionContext, tc: TraceContext, ): EitherT[FutureUnlessShutdown, KmsError, ByteString4096] = { - val keyName = - gcp.CryptoKeyName.of( - config.projectId, - config.locationId, - config.keyRingId, - keyId.unwrap, - ) + val keyName = cryptoKeyName(keyId) val encryptionAlgorithm = CryptoKeyVersionAlgorithm.GOOGLE_SYMMETRIC_ENCRYPTION for { dataPlain <- loggerKms.withLogging[ByteString]( @@ -539,14 +537,7 @@ class GcpKms( ec: ExecutionContext, tc: TraceContext, ): EitherT[FutureUnlessShutdown, KmsError, ByteString190] = { - val keyName = - gcp.CryptoKeyVersionName.of( - config.projectId, - config.locationId, - config.keyRingId, - keyId.unwrap, - gcpKeyversion, - ) + val keyName = cryptoKeyVersionName(keyId) for { encryptionAlgorithm <- convertToGcpAsymmetricEncryptionSpec(encryptionAlgorithmSpec) .leftMap(err => KmsDecryptError(keyId, err)) @@ -614,14 +605,7 @@ class GcpKms( ec: ExecutionContext, tc: TraceContext, ): EitherT[FutureUnlessShutdown, KmsError, ByteString] = { - val keyVersionName = - gcp.CryptoKeyVersionName.of( - config.projectId, - config.locationId, - config.keyRingId, - keyId.unwrap, - gcpKeyversion, - ) + val keyVersionName = cryptoKeyVersionName(keyId) signingAlgorithmSpec match { case SigningAlgorithmSpec.EcDsaSha256 => signingKeySpec match { @@ -677,14 +661,7 @@ class GcpKms( ec: ExecutionContext, tc: TraceContext, ): EitherT[FutureUnlessShutdown, KmsError, Unit] = { - val keyVersionName = - gcp.CryptoKeyVersionName.of( - config.projectId, - config.locationId, - config.keyRingId, - keyId.unwrap, - gcpKeyversion, - ) + val keyVersionName = cryptoKeyVersionName(keyId) loggerKms.withLogging[Unit]( loggerKms.deleteKeyRequestMsg(keyId.unwrap), _ => loggerKms.deleteKeyResponseMsg(keyId.unwrap), @@ -709,14 +686,7 @@ class GcpKms( ec: ExecutionContext, tc: TraceContext, ): EitherT[FutureUnlessShutdown, KmsError, gcp.CryptoKeyVersion] = { - val keyVersionName = - gcp.CryptoKeyVersionName.of( - config.projectId, - config.locationId, - config.keyRingId, - keyId.unwrap, - gcpKeyversion, - ) + val keyVersionName = cryptoKeyVersionName(keyId) loggerKms.withLogging[gcp.CryptoKeyVersion]( loggerKms.retrieveKeyMetadataRequestMsg(keyId.unwrap), keyMetadata => @@ -746,6 +716,13 @@ class GcpKms( object GcpKms extends Kms.SupportedSchemes { + /** Default cryptoKey version used by Canton when no override is specified for a given key + * via [[KmsConfig.Gcp.keyVersionOverrides]]. Canton creates new cryptoKeys in GCP KMS + * rather than adding a new version to an existing one, so version `"1"` is always correct + * for Canton-generated keys. + */ + private[gcp] val defaultGcpKeyVersion: String = "1" + val supportedSigningKeySpecs: NonEmpty[Set[SigningKeySpec]] = NonEmpty.mk( Set, diff --git a/docs-open/src/sphinx/participant/howtos/secure/kms/configuration/kms_gcp_config.rst b/docs-open/src/sphinx/participant/howtos/secure/kms/configuration/kms_gcp_config.rst index f184011a52..cd28394ed3 100644 --- a/docs-open/src/sphinx/participant/howtos/secure/kms/configuration/kms_gcp_config.rst +++ b/docs-open/src/sphinx/participant/howtos/secure/kms/configuration/kms_gcp_config.rst @@ -21,6 +21,18 @@ node's configuration file. A KMS for GCP is configured in the following way: - ``project-id`` specifies which project are we binding to. - ``key-ring-id`` specifies the keyring to use. Multi region keys are enabled for an entire keyring. Therefore, the KMS operator is responsible for setting the keyring correctly depending on the systems' needs. - ``audit-logging`` flag that enables logging of every call made to the GCP KMS +- ``key-version-overrides`` (optional) maps a GCP cryptoKey id to the cryptoKey version Canton + should use for that key. Keys not listed default to version ``"1"``, which matches Canton's + behavior of creating a new cryptoKey rather than adding a version to an existing one. This + is intended for cryptoKeys whose key material was + `imported into GCP KMS `_, where each + import creates a new cryptoKey version and multiple versions can be relevant even for + asymmetric keys. Example:: + + key-version-overrides = { + "my-imported-signing-key" = "3" + "my-imported-encryption-key" = "2" + } Configure GCP credentials and permissions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs-open/src/sphinx/participant/howtos/secure/kms/mode/external_key_storage.rst b/docs-open/src/sphinx/participant/howtos/secure/kms/mode/external_key_storage.rst index 7eecca9c05..e0ea75c85f 100644 --- a/docs-open/src/sphinx/participant/howtos/secure/kms/mode/external_key_storage.rst +++ b/docs-open/src/sphinx/participant/howtos/secure/kms/mode/external_key_storage.rst @@ -76,3 +76,11 @@ where `xyzKmsKeyId` is the KMS key identifier for a specific key (e.g. `KMS Key .. note:: When using `AWS cross account keys `_ the key ID can't be used, use the key `ARN` instead. + +.. note:: + For GCP KMS, the key identifier is the cryptoKey id (e.g. ``my-signing-key``). By + default, Canton uses version ``"1"`` of the cryptoKey. If you registered a cryptoKey + whose key material was `imported into GCP KMS `_ + and want to target a specific cryptoKey version, set + :ref:`\`\`key-version-overrides\`\` ` on the GCP KMS config, mapping the + cryptoKey id to the desired version.