diff --git a/ledger-service/jwt/src/main/scala/com/digitalasset/jwt/JwtTimestampLeeway.scala b/ledger-service/jwt/src/main/scala/com/digitalasset/jwt/JwtTimestampLeeway.scala index 4e50f2bf32..1ce3a8f7b0 100644 --- a/ledger-service/jwt/src/main/scala/com/digitalasset/jwt/JwtTimestampLeeway.scala +++ b/ledger-service/jwt/src/main/scala/com/digitalasset/jwt/JwtTimestampLeeway.scala @@ -17,7 +17,7 @@ final case class JwtTimestampLeeway( trait Leeway { def getVerifier( algorithm: Algorithm, - mbJwtTimestampLeeway: Option[JwtTimestampLeeway] = None, + jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, ): com.auth0.jwt.interfaces.JWTVerifier = { def addLeeway( verification: Verification, @@ -35,7 +35,7 @@ trait Leeway { } } val defaultVerifier = JWT.require(algorithm) - val verification = mbJwtTimestampLeeway.fold(defaultVerifier)(addLeeway(defaultVerifier, _)) + val verification = jwtTimestampLeeway.fold(defaultVerifier)(addLeeway(defaultVerifier, _)) verification.build() } } diff --git a/ledger/ledger-runner-common/src/main/scala/com/daml/ledger/runner/common/ConfigAdaptor.scala b/ledger/ledger-runner-common/src/main/scala/com/daml/ledger/runner/common/ConfigAdaptor.scala index 2c75f8aa3a..6868f5fb60 100644 --- a/ledger/ledger-runner-common/src/main/scala/com/daml/ledger/runner/common/ConfigAdaptor.scala +++ b/ledger/ledger-runner-common/src/main/scala/com/daml/ledger/runner/common/ConfigAdaptor.scala @@ -39,5 +39,5 @@ class ConfigAdaptor { } def authService(participantConfig: ParticipantConfig): AuthService = - participantConfig.authentication.create() + participantConfig.authentication.create(participantConfig.jwtTimestampLeeway) } diff --git a/ledger/ledger-runner-common/src/main/scala/com/daml/ledger/runner/common/PureConfigReaderWriter.scala b/ledger/ledger-runner-common/src/main/scala/com/daml/ledger/runner/common/PureConfigReaderWriter.scala index 217266aebc..76bdd51e06 100644 --- a/ledger/ledger-runner-common/src/main/scala/com/daml/ledger/runner/common/PureConfigReaderWriter.scala +++ b/ledger/ledger-runner-common/src/main/scala/com/daml/ledger/runner/common/PureConfigReaderWriter.scala @@ -232,8 +232,8 @@ class PureConfigReaderWriter(secure: Boolean = true) { implicit val userManagementConfigConvert: ConfigConvert[UserManagementConfig] = deriveConvert[UserManagementConfig] - implicit val jwtTimestampLeewayConfigConvert: ConfigConvert[JwtTimestampLeeway] = - deriveConvert[JwtTimestampLeeway] + implicit val jwtTimestampLeewayConfigConvert: ConfigConvert[Option[JwtTimestampLeeway]] = + optConvertEnabled(deriveConvert[JwtTimestampLeeway]) implicit val authServiceConfigUnsafeJwtHmac256Reader : ConfigReader[AuthServiceConfig.UnsafeJwtHmac256] = diff --git a/ledger/ledger-runner-common/src/test/lib/com/daml/ledger/runner/common/ArbitraryConfig.scala b/ledger/ledger-runner-common/src/test/lib/com/daml/ledger/runner/common/ArbitraryConfig.scala index 0fb2b5607b..9090717210 100644 --- a/ledger/ledger-runner-common/src/test/lib/com/daml/ledger/runner/common/ArbitraryConfig.scala +++ b/ledger/ledger-runner-common/src/test/lib/com/daml/ledger/runner/common/ArbitraryConfig.scala @@ -199,28 +199,23 @@ object ArbitraryConfig { val UnsafeJwtHmac256 = for { secret <- Gen.alphaStr - mbJwtTimestampLeeway <- Gen.option(jwtTimestampLeewayGen) - } yield AuthServiceConfig.UnsafeJwtHmac256(secret, mbJwtTimestampLeeway) + } yield AuthServiceConfig.UnsafeJwtHmac256(secret) val JwtRs256Crt = for { certificate <- Gen.alphaStr - mbJwtTimestampLeeway <- Gen.option(jwtTimestampLeewayGen) - } yield AuthServiceConfig.JwtRs256(certificate, mbJwtTimestampLeeway) + } yield AuthServiceConfig.JwtRs256(certificate) val JwtEs256Crt = for { certificate <- Gen.alphaStr - mbJwtTimestampLeeway <- Gen.option(jwtTimestampLeewayGen) - } yield AuthServiceConfig.JwtEs256(certificate, mbJwtTimestampLeeway) + } yield AuthServiceConfig.JwtEs256(certificate) val JwtEs512Crt = for { certificate <- Gen.alphaStr - mbJwtTimestampLeeway <- Gen.option(jwtTimestampLeewayGen) - } yield AuthServiceConfig.JwtEs512(certificate, mbJwtTimestampLeeway) + } yield AuthServiceConfig.JwtEs512(certificate) val JwtRs256Jwks = for { url <- Gen.alphaStr - mbJwtTimestampLeeway <- Gen.option(jwtTimestampLeewayGen) - } yield AuthServiceConfig.JwtRs256Jwks(url, mbJwtTimestampLeeway) + } yield AuthServiceConfig.JwtRs256Jwks(url) val authServiceConfig = Gen.oneOf( Gen.const(AuthServiceConfig.Wildcard), @@ -423,10 +418,12 @@ object ArbitraryConfig { indexService <- indexServiceConfig indexer <- indexerConfig lfValueTranslationCache <- lfValueTranslationCache + jwtTimestampLeeway <- Gen.option(jwtTimestampLeewayGen) } yield ParticipantConfig( apiServer = apiServer, authentication = AuthServiceConfig.Wildcard, // hardcoded to wildcard, as otherwise it // will be redacted and cannot be checked for isomorphism + jwtTimestampLeeway = jwtTimestampLeeway, dataSourceProperties = dataSourceProperties, indexService = indexService, indexer = indexer, diff --git a/ledger/ledger-runner-common/src/test/scala/com/daml/ledger/runner/common/PureConfigReaderWriterSpec.scala b/ledger/ledger-runner-common/src/test/scala/com/daml/ledger/runner/common/PureConfigReaderWriterSpec.scala index 3ed1d0d3cb..17cb5a31f9 100644 --- a/ledger/ledger-runner-common/src/test/scala/com/daml/ledger/runner/common/PureConfigReaderWriterSpec.scala +++ b/ledger/ledger-runner-common/src/test/scala/com/daml/ledger/runner/common/PureConfigReaderWriterSpec.scala @@ -133,6 +133,63 @@ class PureConfigReaderWriterSpec compare(Duration.ofHours(1), "3600 seconds") } + behavior of "JwtTimestampLeeway" + + it should "read/write against predefined values" in { + def compare(configString: String, expectedValue: Option[JwtTimestampLeeway]) = { + convert(jwtTimestampLeewayConfigConvert, configString).value shouldBe expectedValue + + } + compare( + """ + | enabled = true + | default = 1 + |""".stripMargin, + Some(JwtTimestampLeeway(Some(1), None, None, None)), + ) + compare( + """ + | enabled = true + | expires-at = 2 + |""".stripMargin, + Some(JwtTimestampLeeway(None, Some(2), None, None)), + ) + compare( + """ + | enabled = true + | issued-at = 3 + |""".stripMargin, + Some(JwtTimestampLeeway(None, None, Some(3), None)), + ) + compare( + """ + | enabled = true + | not-before = 4 + |""".stripMargin, + Some(JwtTimestampLeeway(None, None, None, Some(4))), + ) + compare( + """ + | enabled = true + | default = 1 + | expires-at = 2 + | issued-at = 3 + | not-before = 4 + |""".stripMargin, + Some(JwtTimestampLeeway(Some(1), Some(2), Some(3), Some(4))), + ) + compare( + """ + | enabled = false + | default = 1 + | expires-at = 2 + | issued-at = 3 + | not-before = 4 + |""".stripMargin, + None, + ) + } + behavior of "PureConfigReaderWriter VersionRange[LanguageVersion]" it should "read/write against predefined values" in { @@ -354,8 +411,8 @@ class PureConfigReaderWriterSpec it should "be isomorphic and support redaction" in forAll(ArbitraryConfig.authServiceConfig) { generatedValue => val redacted = generatedValue match { - case AuthServiceConfig.UnsafeJwtHmac256(_, jwtTimestampLeeway) => - AuthServiceConfig.UnsafeJwtHmac256("", jwtTimestampLeeway) + case AuthServiceConfig.UnsafeJwtHmac256(_) => + AuthServiceConfig.UnsafeJwtHmac256("") case _ => generatedValue } val insecureWriter = new PureConfigReaderWriter(false) @@ -383,78 +440,6 @@ class PureConfigReaderWriterSpec "type = unsafe-jwt-hmac-256\nsecret=mysecret2", AuthServiceConfig.UnsafeJwtHmac256("mysecret2"), ) - compare( - "type = unsafe-jwt-hmac-256\nsecret=mysecret3", - AuthServiceConfig.UnsafeJwtHmac256("mysecret3", None), - ) - compare( - """ - |type = unsafe-jwt-hmac-256 - |secret = mysecret3 - |jwt-timestamp-leeway { - | default = 1 - |} - |""".stripMargin, - AuthServiceConfig.UnsafeJwtHmac256( - "mysecret3", - Some(JwtTimestampLeeway(Some(1), None, None, None)), - ), - ) - compare( - """ - |type = unsafe-jwt-hmac-256 - |secret = mysecret3 - |jwt-timestamp-leeway { - | expires-at = 2 - |} - |""".stripMargin, - AuthServiceConfig.UnsafeJwtHmac256( - "mysecret3", - Some(JwtTimestampLeeway(None, Some(2), None, None)), - ), - ) - compare( - """ - |type = unsafe-jwt-hmac-256 - |secret = mysecret3 - |jwt-timestamp-leeway { - | issued-at = 3 - |} - |""".stripMargin, - AuthServiceConfig.UnsafeJwtHmac256( - "mysecret3", - Some(JwtTimestampLeeway(None, None, Some(3), None)), - ), - ) - compare( - """ - |type = unsafe-jwt-hmac-256 - |secret = mysecret3 - |jwt-timestamp-leeway { - | not-before = 4 - |} - |""".stripMargin, - AuthServiceConfig.UnsafeJwtHmac256( - "mysecret3", - Some(JwtTimestampLeeway(None, None, None, Some(4))), - ), - ) - compare( - """ - |type = unsafe-jwt-hmac-256 - |secret = mysecret3 - |jwt-timestamp-leeway { - | default = 1 - | expires-at = 2 - | issued-at = 3 - | not-before = 4 - |} - |""".stripMargin, - AuthServiceConfig.UnsafeJwtHmac256( - "mysecret3", - Some(JwtTimestampLeeway(Some(1), Some(2), Some(3), Some(4))), - ), - ) compare( "type = jwt-rs-256\ncertificate=certfile", AuthServiceConfig.JwtRs256("certfile"), diff --git a/ledger/participant-integration-api/src/main/scala/platform/LedgerApiServer.scala b/ledger/participant-integration-api/src/main/scala/platform/LedgerApiServer.scala index aa81dc800e..d05225f966 100644 --- a/ledger/participant-integration-api/src/main/scala/platform/LedgerApiServer.scala +++ b/ledger/participant-integration-api/src/main/scala/platform/LedgerApiServer.scala @@ -161,7 +161,7 @@ class LedgerApiServer( ledgerFeatures = ledgerFeatures, participantId = participantId, authService = authService, - jwtTimestampLeeway = participantConfig.authentication.jwtTimestampLeeway, + jwtTimestampLeeway = participantConfig.jwtTimestampLeeway, ) } diff --git a/ledger/participant-integration-api/src/main/scala/platform/apiserver/AuthServiceConfig.scala b/ledger/participant-integration-api/src/main/scala/platform/apiserver/AuthServiceConfig.scala index 92241efe5b..0114a0435b 100644 --- a/ledger/participant-integration-api/src/main/scala/platform/apiserver/AuthServiceConfig.scala +++ b/ledger/participant-integration-api/src/main/scala/platform/apiserver/AuthServiceConfig.scala @@ -8,79 +8,73 @@ import com.daml.jwt.{ ECDSAVerifier, HMAC256Verifier, JwksVerifier, - RSA256Verifier, JwtTimestampLeeway, + JwtVerifier, + RSA256Verifier, } import com.daml.ledger.api.auth.{AuthService, AuthServiceJWT, AuthServiceWildcard} sealed trait AuthServiceConfig { - def create(): AuthService - def jwtTimestampLeeway: Option[JwtTimestampLeeway] + def create(jwtTimestampLeeway: Option[JwtTimestampLeeway]): AuthService } object AuthServiceConfig { /** [default] Allows everything */ final object Wildcard extends AuthServiceConfig { - override def create(): AuthService = AuthServiceWildcard - def jwtTimestampLeeway: Option[JwtTimestampLeeway] = None + override def create(jwtTimestampLeeway: Option[JwtTimestampLeeway]): AuthService = + AuthServiceWildcard } /** [UNSAFE] Enables JWT-based authorization with shared secret HMAC256 signing: USE THIS EXCLUSIVELY FOR TESTING */ - final case class UnsafeJwtHmac256( - secret: String, - jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, - ) extends AuthServiceConfig { - private lazy val verifier = + final case class UnsafeJwtHmac256(secret: String) extends AuthServiceConfig { + private def verifier(jwtTimestampLeeway: Option[JwtTimestampLeeway]): JwtVerifier = HMAC256Verifier(secret, jwtTimestampLeeway).valueOr(err => throw new IllegalArgumentException( s"Failed to create HMAC256 verifier (secret: $secret): $err" ) ) - override def create(): AuthService = AuthServiceJWT(verifier) + def create(jwtTimestampLeeway: Option[JwtTimestampLeeway]): AuthService = AuthServiceJWT( + verifier(jwtTimestampLeeway) + ) } /** Enables JWT-based authorization, where the JWT is signed by RSA256 with the verifying public key loaded from the given X509 certificate file (.crt) */ - final case class JwtRs256( - certificate: String, - jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, - ) extends AuthServiceConfig { - private lazy val verifier = RSA256Verifier + final case class JwtRs256(certificate: String) extends AuthServiceConfig { + private def verifier(jwtTimestampLeeway: Option[JwtTimestampLeeway]) = RSA256Verifier .fromCrtFile(certificate, jwtTimestampLeeway) .valueOr(err => throw new IllegalArgumentException(s"Failed to create RSA256 verifier: $err")) - override def create(): AuthService = AuthServiceJWT(verifier) + override def create(jwtTimestampLeeway: Option[JwtTimestampLeeway]): AuthService = + AuthServiceJWT(verifier(jwtTimestampLeeway)) } /** "Enables JWT-based authorization, where the JWT is signed by ECDSA256 with the verifying public key loaded from the given X509 certificate file (.crt)" */ - final case class JwtEs256( - certificate: String, - jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, - ) extends AuthServiceConfig { - private lazy val verifier = ECDSAVerifier + final case class JwtEs256(certificate: String) extends AuthServiceConfig { + private def verifier(jwtTimestampLeeway: Option[JwtTimestampLeeway]) = ECDSAVerifier .fromCrtFile(certificate, Algorithm.ECDSA256(_, null), jwtTimestampLeeway) .valueOr(err => throw new IllegalArgumentException(s"Failed to create ECDSA256 verifier: $err") ) - override def create(): AuthService = AuthServiceJWT(verifier) + override def create(jwtTimestampLeeway: Option[JwtTimestampLeeway]): AuthService = + AuthServiceJWT(verifier(jwtTimestampLeeway)) } /** Enables JWT-based authorization, where the JWT is signed by ECDSA512 with the verifying public key loaded from the given X509 certificate file (.crt) */ - final case class JwtEs512( - certificate: String, - jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, - ) extends AuthServiceConfig { - private lazy val verifier = ECDSAVerifier + final case class JwtEs512(certificate: String) extends AuthServiceConfig { + private def verifier(jwtTimestampLeeway: Option[JwtTimestampLeeway]) = ECDSAVerifier .fromCrtFile(certificate, Algorithm.ECDSA512(_, null), jwtTimestampLeeway) .valueOr(err => throw new IllegalArgumentException(s"Failed to create ECDSA512 verifier: $err") ) - override def create(): AuthService = AuthServiceJWT(verifier) + override def create(jwtTimestampLeeway: Option[JwtTimestampLeeway]): AuthService = + AuthServiceJWT(verifier(jwtTimestampLeeway)) } /** Enables JWT-based authorization, where the JWT is signed by RSA256 with the verifying public key loaded from the given JWKS URL */ - final case class JwtRs256Jwks(url: String, jwtTimestampLeeway: Option[JwtTimestampLeeway] = None) - extends AuthServiceConfig { - private lazy val verifier = JwksVerifier(url, jwtTimestampLeeway) - override def create(): AuthService = AuthServiceJWT(verifier) + final case class JwtRs256Jwks(url: String) extends AuthServiceConfig { + private def verifier(jwtTimestampLeeway: Option[JwtTimestampLeeway]) = + JwksVerifier(url, jwtTimestampLeeway) + override def create(jwtTimestampLeeway: Option[JwtTimestampLeeway]): AuthService = + AuthServiceJWT(verifier(jwtTimestampLeeway)) } } diff --git a/ledger/participant-integration-api/src/main/scala/platform/config/ParticipantConfig.scala b/ledger/participant-integration-api/src/main/scala/platform/config/ParticipantConfig.scala index b471b1c225..8163dcdaec 100644 --- a/ledger/participant-integration-api/src/main/scala/platform/config/ParticipantConfig.scala +++ b/ledger/participant-integration-api/src/main/scala/platform/config/ParticipantConfig.scala @@ -3,6 +3,7 @@ package com.daml.platform.config +import com.daml.jwt.JwtTimestampLeeway import com.daml.lf.data.Ref import com.daml.platform.apiserver.{ApiServerConfig, AuthServiceConfig} import com.daml.platform.configuration.IndexServiceConfig @@ -15,6 +16,7 @@ import scala.concurrent.duration._ final case class ParticipantConfig( apiServer: ApiServerConfig = ApiServerConfig(), authentication: AuthServiceConfig = AuthServiceConfig.Wildcard, + jwtTimestampLeeway: Option[JwtTimestampLeeway] = None, dataSourceProperties: DataSourceProperties = DataSourceProperties( connectionPool = ConnectionPoolConfig( connectionPoolSize = 16, diff --git a/ledger/sandbox-on-x/BUILD.bazel b/ledger/sandbox-on-x/BUILD.bazel index 75a09945c4..bce2816912 100644 --- a/ledger/sandbox-on-x/BUILD.bazel +++ b/ledger/sandbox-on-x/BUILD.bazel @@ -34,6 +34,7 @@ da_scala_library( "//daml-lf/language", "//daml-lf/transaction", "//language-support/scala/bindings", + "//ledger-service/jwt", "//ledger/caching", "//ledger/error", "//ledger/ledger-api-auth", diff --git a/ledger/sandbox-on-x/reference.conf b/ledger/sandbox-on-x/reference.conf index 0d0b8757d4..4aac3759ae 100644 --- a/ledger/sandbox-on-x/reference.conf +++ b/ledger/sandbox-on-x/reference.conf @@ -326,22 +326,25 @@ ledger { # JWKS URL for jwt-rs-256-jwks algorithm # url = "https://localhost/jwks.json" - - #jwt-timestamp-leeway { - # # Define the default window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid. - # default = 500 - # - # # Set a specific leeway window in seconds in which the Expires At ("exp") Claim will still be valid. - # expires-at = 5 - # - # # Set a specific leeway window in seconds in which the Issued At ("iat") Claim will still be valid. - # issued-at = 5 - # - # # Set a specific leeway window in seconds in which the Not Before ("nbf") Claim will still be valid. - # not-before = 5 - #} } + # jwt-timestamp-leeway { + # # Enables leeway for JWT tokens with provided configuration below. + # enabled = true + + # # Define the default window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid. + # default = 500 + # + # # Set a specific leeway window in seconds in which the Expires At ("exp") Claim will still be valid. + # expires-at = 5 + # + # # Set a specific leeway window in seconds in which the Issued At ("iat") Claim will still be valid. + # issued-at = 5 + # + # # Set a specific leeway window in seconds in which the Not Before ("nbf") Claim will still be valid. + # not-before = 5 + #} + data-source-properties { # This section defines connection pool properties for the participant node. The Indexer does have it's own data # source properties. diff --git a/ledger/sandbox-on-x/src/main/scala/com/daml/ledger/sandbox/SandboxOnXForTest.scala b/ledger/sandbox-on-x/src/main/scala/com/daml/ledger/sandbox/SandboxOnXForTest.scala index 21eeb0ba02..d6ba2ecb44 100644 --- a/ledger/sandbox-on-x/src/main/scala/com/daml/ledger/sandbox/SandboxOnXForTest.scala +++ b/ledger/sandbox-on-x/src/main/scala/com/daml/ledger/sandbox/SandboxOnXForTest.scala @@ -6,7 +6,6 @@ package com.daml.ledger.sandbox import com.daml.ledger.api.auth.{AuthService, AuthServiceWildcard} import com.daml.ledger.configuration.Configuration import com.daml.ledger.runner.common.Config -import com.daml.platform.config.{MetricsConfig, ParticipantConfig => _ParticipantConfig} import com.daml.lf.data.Ref import com.daml.lf.data.Ref.ParticipantId import com.daml.lf.engine.{EngineConfig => _EngineConfig} @@ -14,6 +13,7 @@ import com.daml.lf.language.LanguageVersion import com.daml.platform.apiserver.SeedService.Seeding import com.daml.platform.apiserver.{AuthServiceConfig, ApiServerConfig => _ApiServerConfig} import com.daml.platform.config.MetricsConfig.MetricRegistryType +import com.daml.platform.config.{MetricsConfig, ParticipantConfig => _ParticipantConfig} import com.daml.platform.configuration.{InitialLedgerConfiguration, PartyConfiguration} import com.daml.platform.indexer.{IndexerConfig => _IndexerConfig} import com.daml.platform.store.DbSupport.ParticipantDataSourceConfig diff --git a/ledger/sandbox-on-x/src/main/scala/com/daml/ledger/sandbox/SandboxOnXRunner.scala b/ledger/sandbox-on-x/src/main/scala/com/daml/ledger/sandbox/SandboxOnXRunner.scala index 0dddad67b6..93d0d17b34 100644 --- a/ledger/sandbox-on-x/src/main/scala/com/daml/ledger/sandbox/SandboxOnXRunner.scala +++ b/ledger/sandbox-on-x/src/main/scala/com/daml/ledger/sandbox/SandboxOnXRunner.scala @@ -259,13 +259,14 @@ object SandboxOnXRunner { extra: BridgeConfig, ): Unit = { val apiServerConfig = participantConfig.apiServer - val authentication = participantConfig.authentication.create() match { - case _: AuthServiceJWT => "JWT-based authentication" - case AuthServiceNone => "none authenticated" - case _: AuthServiceStatic => "static authentication" - case AuthServiceWildcard => "all unauthenticated allowed" - case other => other.getClass.getSimpleName - } + val authentication = + participantConfig.authentication.create(participantConfig.jwtTimestampLeeway) match { + case _: AuthServiceJWT => "JWT-based authentication" + case AuthServiceNone => "none authenticated" + case _: AuthServiceStatic => "static authentication" + case AuthServiceWildcard => "all unauthenticated allowed" + case other => other.getClass.getSimpleName + } val ledgerDetails = Seq[(String, String)](