mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-19 16:57:40 +03:00
parent
36fe0abb09
commit
576560428f
@ -59,37 +59,6 @@ if [[ "$NAME" == "linux" ]]; then
|
||||
bazel build //daml-script/runner:daml-script-binary_distribute.jar
|
||||
cp bazel-bin/daml-script/runner/daml-script-binary_distribute.jar $OUTPUT_DIR/artifactory/$SCRIPT
|
||||
|
||||
NON_REPUDIATION=non-repudiation-$RELEASE_TAG-ee.jar
|
||||
bazel build //runtime-components/non-repudiation-app:non-repudiation-app_distribute.jar
|
||||
cp bazel-bin/runtime-components/non-repudiation-app/non-repudiation-app_distribute.jar $OUTPUT_DIR/artifactory/$NON_REPUDIATION
|
||||
|
||||
NON_REPUDIATION_CORE_JAR=non-repudiation-core-$RELEASE_TAG.jar
|
||||
NON_REPUDIATION_CORE_POM=non-repudiation-core-$RELEASE_TAG.pom
|
||||
NON_REPUDIATION_CORE_SRC=non-repudiation-core-$RELEASE_TAG-sources.jar
|
||||
NON_REPUDIATION_CORE_DOC=non-repudiation-core-$RELEASE_TAG-javadoc.jar
|
||||
bazel build \
|
||||
//runtime-components/non-repudiation-core/... \
|
||||
//runtime-components/non-repudiation-core:non-repudiation-core_javadoc \
|
||||
//runtime-components/non-repudiation-core:libnon-repudiation-core-src.jar
|
||||
cp bazel-bin/runtime-components/non-repudiation-core/libnon-repudiation-core.jar $OUTPUT_DIR/artifactory/$NON_REPUDIATION_CORE_JAR
|
||||
cp bazel-bin/runtime-components/non-repudiation-core/non-repudiation-core_pom.xml $OUTPUT_DIR/artifactory/$NON_REPUDIATION_CORE_POM
|
||||
cp bazel-bin/runtime-components/non-repudiation-core/libnon-repudiation-core-src.jar $OUTPUT_DIR/artifactory/$NON_REPUDIATION_CORE_SRC
|
||||
cp bazel-bin/runtime-components/non-repudiation-core/non-repudiation-core_javadoc.jar $OUTPUT_DIR/artifactory/$NON_REPUDIATION_CORE_DOC
|
||||
|
||||
|
||||
NON_REPUDIATION_CLIENT_JAR=non-repudiation-client-$RELEASE_TAG.jar
|
||||
NON_REPUDIATION_CLIENT_POM=non-repudiation-client-$RELEASE_TAG.pom
|
||||
NON_REPUDIATION_CLIENT_SRC=non-repudiation-client-$RELEASE_TAG-sources.jar
|
||||
NON_REPUDIATION_CLIENT_DOC=non-repudiation-client-$RELEASE_TAG-javadoc.jar
|
||||
bazel build \
|
||||
//runtime-components/non-repudiation-client/... \
|
||||
//runtime-components/non-repudiation-client:non-repudiation-client_javadoc \
|
||||
//runtime-components/non-repudiation-client:libnon-repudiation-client-src.jar
|
||||
cp bazel-bin/runtime-components/non-repudiation-client/libnon-repudiation-client.jar $OUTPUT_DIR/artifactory/$NON_REPUDIATION_CLIENT_JAR
|
||||
cp bazel-bin/runtime-components/non-repudiation-client/non-repudiation-client_pom.xml $OUTPUT_DIR/artifactory/$NON_REPUDIATION_CLIENT_POM
|
||||
cp bazel-bin/runtime-components/non-repudiation-client/libnon-repudiation-client-src.jar $OUTPUT_DIR/artifactory/$NON_REPUDIATION_CLIENT_SRC
|
||||
cp bazel-bin/runtime-components/non-repudiation-client/non-repudiation-client_javadoc.jar $OUTPUT_DIR/artifactory/$NON_REPUDIATION_CLIENT_DOC
|
||||
|
||||
mkdir -p $OUTPUT_DIR/split-release/daml-libs/daml-script
|
||||
bazel build //daml-script/daml:daml-script-dars
|
||||
cp bazel-bin/daml-script/daml/*.dar $OUTPUT_DIR/split-release/daml-libs/daml-script/
|
||||
|
@ -28,7 +28,6 @@ push() {
|
||||
TRIGGER_RUNNER=daml-trigger-runner-$RELEASE_TAG.jar
|
||||
TRIGGER_SERVICE=trigger-service-$RELEASE_TAG-ee.jar
|
||||
SCRIPT_RUNNER=daml-script-$RELEASE_TAG.jar
|
||||
NON_REPUDIATION=non-repudiation-$RELEASE_TAG-ee.jar
|
||||
HTTP_JSON=http-json-$RELEASE_TAG-ee.jar
|
||||
|
||||
cd $INPUTS
|
||||
@ -36,21 +35,11 @@ push daml-trigger-runner $TRIGGER_RUNNER
|
||||
push daml-trigger-runner $TRIGGER_RUNNER.asc
|
||||
push daml-script-runner $SCRIPT_RUNNER
|
||||
push daml-script-runner $SCRIPT_RUNNER.asc
|
||||
push non-repudiation $NON_REPUDIATION
|
||||
push non-repudiation $NON_REPUDIATION.asc
|
||||
push trigger-service $TRIGGER_SERVICE
|
||||
push trigger-service $TRIGGER_SERVICE.asc
|
||||
push http-json $HTTP_JSON
|
||||
push http-json $HTTP_JSON.asc
|
||||
|
||||
for base in non-repudiation-core non-repudiation-client; do
|
||||
for end in .jar .pom -sources.jar -javadoc.jar; do
|
||||
for sign in "" .asc; do
|
||||
push connect-ee-mvn/com/daml/$base $base-${RELEASE_TAG}${end}${sign}
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
# For the split release process these are not published to artifactory.
|
||||
if [[ "$#" -lt 3 || $3 != "split" ]]; then
|
||||
for platform in linux macos windows; do
|
||||
|
@ -14,7 +14,7 @@ import com.daml.cliopts.Logging.LogEncoder
|
||||
import com.daml.grpc.adapter.{PekkoExecutionSequencerPool, ExecutionSequencerFactory}
|
||||
import com.daml.http.metrics.HttpJsonApiMetrics
|
||||
import com.daml.http.util.Logging.{InstanceUUID, instanceUUIDLogCtx}
|
||||
import com.daml.http.{HttpService, StartSettings, nonrepudiation}
|
||||
import com.daml.http.{HttpService, StartSettings}
|
||||
import com.daml.integrationtest._
|
||||
import com.daml.jwt.JwtSigner
|
||||
import com.daml.jwt.domain.DecodedJwt
|
||||
@ -140,7 +140,6 @@ trait JsonApiFixture
|
||||
override val wsConfig = None
|
||||
override val allowNonHttps = true
|
||||
override val authConfig = None
|
||||
override val nonRepudiation = nonrepudiation.Configuration.Cli.Empty
|
||||
override val logLevel = None
|
||||
override val logEncoder = LogEncoder.Plain
|
||||
override val metricsReporter: Option[MetricsReporter] = None
|
||||
|
@ -9,5 +9,5 @@ object Cli extends CliBase {
|
||||
override protected def configParser(getEnvVar: String => Option[String])(implicit
|
||||
jcd: JdbcConfigDefaults
|
||||
): OptionParser =
|
||||
new OptionParser(getEnvVar) with NonRepudiationOptions
|
||||
new OptionParser(getEnvVar)
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.http
|
||||
|
||||
import java.nio.file.Paths
|
||||
|
||||
trait NonRepudiationOptions { this: scopt.OptionParser[JsonApiCli] =>
|
||||
|
||||
opt[String]("non-repudiation-certificate-path")
|
||||
.action((path, config) =>
|
||||
config
|
||||
.copy(nonRepudiation = config.nonRepudiation.copy(certificateFile = Some(Paths.get(path))))
|
||||
)
|
||||
.text(NonRepudiationOptions.helpText)
|
||||
|
||||
opt[String]("non-repudiation-private-key-path")
|
||||
.action((path, config) =>
|
||||
config
|
||||
.copy(nonRepudiation = config.nonRepudiation.copy(privateKeyFile = Some(Paths.get(path))))
|
||||
)
|
||||
.text(NonRepudiationOptions.helpText)
|
||||
|
||||
opt[String]("non-repudiation-private-key-algorithm")
|
||||
.action((algorithm, config) =>
|
||||
config
|
||||
.copy(nonRepudiation = config.nonRepudiation.copy(privateKeyAlgorithm = Some(algorithm)))
|
||||
)
|
||||
.text(NonRepudiationOptions.helpText)
|
||||
|
||||
}
|
||||
|
||||
object NonRepudiationOptions {
|
||||
|
||||
private val helpText: String =
|
||||
"""EARLY ACCESS FEATURE
|
||||
|--non-repudiation-certificate-path, --non-repudiation-private-key-path and --non-repudiation-private-key-algorithm
|
||||
|must be passed together. All commands issued by the HTTP JSON API will be signed with the private key and the X.509
|
||||
|certificate at the provided paths. This is relevant exclusively if you are using the non-repudiation middleware.""".stripMargin
|
||||
|
||||
}
|
@ -41,7 +41,6 @@ private[http] final case class Config(
|
||||
authConfig: Option[AuthConfig] = None,
|
||||
allowNonHttps: Boolean = false,
|
||||
wsConfig: Option[WebsocketConfig] = None,
|
||||
nonRepudiation: nonrepudiation.Configuration.Cli = nonrepudiation.Configuration.Cli.Empty,
|
||||
logLevel: Option[LogLevel] = None, // the default is in logback.xml
|
||||
logEncoder: LogEncoder = LogEncoder.Plain,
|
||||
metricsReporter: Option[MetricsReporter] = None,
|
||||
|
@ -68,7 +68,6 @@ private[http] final case class FileBasedConfig(
|
||||
authConfig: Option[AuthConfig] = None,
|
||||
) {
|
||||
def toConfig(
|
||||
nonRepudiation: nonrepudiation.Configuration.Cli,
|
||||
logLevel: Option[LogLevel], // the default is in logback.xml
|
||||
logEncoder: LogEncoder,
|
||||
): Config = {
|
||||
@ -89,7 +88,6 @@ private[http] final case class FileBasedConfig(
|
||||
authConfig = authConfig,
|
||||
allowNonHttps = allowInsecureTokens,
|
||||
wsConfig = websocketConfig,
|
||||
nonRepudiation = nonRepudiation,
|
||||
logLevel = logLevel,
|
||||
logEncoder = logEncoder,
|
||||
metricsReporter = metrics.map(_.reporter),
|
||||
|
@ -34,7 +34,6 @@ private[http] final case class JsonApiCli(
|
||||
authConfig: Option[AuthConfig] = None,
|
||||
allowNonHttps: Boolean = false,
|
||||
wsConfig: Option[WebsocketConfig] = None,
|
||||
nonRepudiation: nonrepudiation.Configuration.Cli = nonrepudiation.Configuration.Cli.Empty,
|
||||
logLevel: Option[LogLevel] = None, // the default is in logback.xml
|
||||
logEncoder: LogEncoder = LogEncoder.Plain,
|
||||
metricsReporter: Option[MetricsReporter] = None,
|
||||
@ -63,7 +62,6 @@ private[http] final case class JsonApiCli(
|
||||
authConfig = authConfig,
|
||||
allowNonHttps = allowNonHttps,
|
||||
wsConfig = wsConfig,
|
||||
nonRepudiation = nonRepudiation,
|
||||
logLevel = logLevel,
|
||||
logEncoder = logEncoder,
|
||||
metricsReporter = metricsReporter,
|
||||
@ -78,7 +76,6 @@ private[http] final case class JsonApiCli(
|
||||
case Right(fileBasedConfig) =>
|
||||
Some(
|
||||
fileBasedConfig.toConfig(
|
||||
nonRepudiation,
|
||||
logLevel,
|
||||
logEncoder,
|
||||
)
|
||||
|
@ -32,7 +32,6 @@ trait StartSettings {
|
||||
val packageMaxInboundMessageSize: Option[Int]
|
||||
val maxInboundMessageSize: Int
|
||||
val healthTimeoutSeconds: Int
|
||||
val nonRepudiation: nonrepudiation.Configuration.Cli
|
||||
val logLevel: Option[LogLevel]
|
||||
val logEncoder: LogEncoder
|
||||
val metricsReporter: Option[MetricsReporter]
|
||||
|
@ -1,57 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.http.nonrepudiation
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
sealed abstract class Configuration[F[_]] {
|
||||
def certificateFile: F[Path]
|
||||
def privateKeyFile: F[Path]
|
||||
def privateKeyAlgorithm: F[String]
|
||||
}
|
||||
|
||||
object Configuration {
|
||||
|
||||
type Id[X] = X
|
||||
|
||||
final case class Validated(
|
||||
certificateFile: Path,
|
||||
privateKeyFile: Path,
|
||||
privateKeyAlgorithm: String,
|
||||
) extends Configuration[Id]
|
||||
|
||||
final case class Cli(
|
||||
certificateFile: Option[Path],
|
||||
privateKeyFile: Option[Path],
|
||||
privateKeyAlgorithm: Option[String],
|
||||
) extends Configuration[Option] {
|
||||
lazy val validated: Try[Option[Validated]] =
|
||||
(certificateFile, privateKeyFile, privateKeyAlgorithm) match {
|
||||
case (None, None, None) =>
|
||||
Success(None)
|
||||
case (Some(cf), Some(kf), Some(ka)) =>
|
||||
Success(Some(Validated(cf, kf, ka)))
|
||||
case _ =>
|
||||
Failure(validationError())
|
||||
}
|
||||
}
|
||||
|
||||
object Cli {
|
||||
|
||||
def apply(
|
||||
certificateFile: Path,
|
||||
privateKeyFile: Path,
|
||||
privateKeyAlgorithm: String,
|
||||
): Cli = Cli(Some(certificateFile), Some(privateKeyFile), Some(privateKeyAlgorithm))
|
||||
|
||||
val Empty: Cli = Cli(None, None, None)
|
||||
|
||||
}
|
||||
|
||||
private def validationError(): IllegalArgumentException =
|
||||
new IllegalArgumentException("Either all or none of the non-repudiation options must be passed")
|
||||
|
||||
}
|
@ -15,7 +15,6 @@ da_scala_library(
|
||||
deps = [
|
||||
"//daml-lf/data",
|
||||
"//language-support/scala/bindings-pekko",
|
||||
"//ledger-service/http-json-cli:base",
|
||||
"//ledger-service/utils",
|
||||
"//libs-scala/contextualized-logging",
|
||||
"//libs-scala/rs-grpc-bridge",
|
||||
@ -26,9 +25,7 @@ da_scala_library(
|
||||
|
||||
deps = {
|
||||
"ce": [],
|
||||
"ee": [
|
||||
"//runtime-components/non-repudiation-client",
|
||||
],
|
||||
"ee": [],
|
||||
}
|
||||
|
||||
[
|
||||
@ -43,9 +40,8 @@ deps = {
|
||||
deps = deps.get(edition) + [
|
||||
":base",
|
||||
"//ledger/ledger-api-client",
|
||||
"//libs-scala/rs-grpc-bridge",
|
||||
"//ledger-service/http-json-cli:{}".format(edition),
|
||||
"//libs-scala/contextualized-logging",
|
||||
"//libs-scala/rs-grpc-bridge",
|
||||
"@maven//:io_grpc_grpc_api",
|
||||
"@maven//:io_grpc_grpc_netty",
|
||||
],
|
||||
|
@ -14,7 +14,6 @@ object LedgerClient extends LedgerClientBase {
|
||||
ledgerHost: String,
|
||||
ledgerPort: Int,
|
||||
clientChannelConfig: LedgerClientChannelConfiguration,
|
||||
nonRepudiationConfig: nonrepudiation.Configuration.Cli,
|
||||
)(implicit executionContext: ExecutionContext): Future[NettyChannelBuilder] =
|
||||
Future(clientChannelConfig.builderFor(ledgerHost, ledgerPort))
|
||||
|
||||
|
@ -3,61 +3,18 @@
|
||||
|
||||
package com.daml.http
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.security.{KeyFactory, PrivateKey}
|
||||
import java.security.cert.{CertificateFactory, X509Certificate}
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
|
||||
import com.daml.ledger.client.configuration.LedgerClientChannelConfiguration
|
||||
import com.daml.nonrepudiation.client.SigningInterceptor
|
||||
import io.grpc.netty.NettyChannelBuilder
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.Try
|
||||
|
||||
object LedgerClient extends LedgerClientBase {
|
||||
|
||||
private def loadCertificate(
|
||||
path: Path
|
||||
): Try[X509Certificate] = {
|
||||
val newInputStream = Try(Files.newInputStream(path))
|
||||
val certificate =
|
||||
for {
|
||||
input <- newInputStream
|
||||
factory <- Try(CertificateFactory.getInstance("X.509"))
|
||||
certificate <- Try(factory.generateCertificate(input).asInstanceOf[X509Certificate])
|
||||
} yield certificate
|
||||
newInputStream.foreach(_.close())
|
||||
certificate
|
||||
}
|
||||
|
||||
private def loadPrivateKey(
|
||||
path: Path,
|
||||
algorithm: String,
|
||||
): Try[PrivateKey] =
|
||||
for {
|
||||
bytes <- Try(Files.readAllBytes(path))
|
||||
keySpec <- Try(new PKCS8EncodedKeySpec(bytes))
|
||||
factory <- Try(KeyFactory.getInstance(algorithm))
|
||||
key <- Try(factory.generatePrivate(keySpec))
|
||||
} yield key
|
||||
|
||||
def channelBuilder(
|
||||
ledgerHost: String,
|
||||
ledgerPort: Int,
|
||||
clientChannelConfig: LedgerClientChannelConfiguration,
|
||||
nonRepudiationConfig: nonrepudiation.Configuration.Cli,
|
||||
)(implicit executionContext: ExecutionContext): Future[NettyChannelBuilder] = {
|
||||
val base = clientChannelConfig.builderFor(ledgerHost, ledgerPort)
|
||||
Future
|
||||
.fromTry(nonRepudiationConfig.validated)
|
||||
.map(_.fold(base) { config =>
|
||||
val channelWithInterceptor =
|
||||
for {
|
||||
certificate <- loadCertificate(config.certificateFile)
|
||||
key <- loadPrivateKey(config.privateKeyFile, config.privateKeyAlgorithm)
|
||||
} yield base.intercept(SigningInterceptor.signCommands(key, certificate))
|
||||
channelWithInterceptor.get
|
||||
})
|
||||
Future.successful(clientChannelConfig.builderFor(ledgerHost, ledgerPort))
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,6 @@ trait LedgerClientBase {
|
||||
ledgerHost: String,
|
||||
ledgerPort: Int,
|
||||
clientChannelConfig: LedgerClientChannelConfiguration,
|
||||
nonRepudiationConfig: nonrepudiation.Configuration.Cli,
|
||||
)(implicit executionContext: ExecutionContext): Future[NettyChannelBuilder]
|
||||
|
||||
private def buildLedgerClient(
|
||||
@ -44,7 +43,6 @@ trait LedgerClientBase {
|
||||
ledgerPort: Int,
|
||||
clientConfig: LedgerClientConfiguration,
|
||||
clientChannelConfig: LedgerClientChannelConfiguration,
|
||||
nonRepudiationConfig: nonrepudiation.Configuration.Cli,
|
||||
)(implicit
|
||||
ec: ExecutionContext,
|
||||
aesf: ExecutionSequencerFactory,
|
||||
@ -53,7 +51,6 @@ trait LedgerClientBase {
|
||||
ledgerHost,
|
||||
ledgerPort,
|
||||
clientChannelConfig,
|
||||
nonRepudiationConfig,
|
||||
).map(builder => DamlLedgerClient.fromBuilder(builder, clientConfig))
|
||||
|
||||
def fromRetried(
|
||||
@ -61,7 +58,6 @@ trait LedgerClientBase {
|
||||
ledgerPort: Int,
|
||||
clientConfig: LedgerClientConfiguration,
|
||||
clientChannelConfig: LedgerClientChannelConfiguration,
|
||||
nonRepudiationConfig: nonrepudiation.Configuration.Cli,
|
||||
maxInitialConnectRetryAttempts: Int,
|
||||
)(implicit
|
||||
ec: ExecutionContext,
|
||||
@ -78,7 +74,6 @@ trait LedgerClientBase {
|
||||
ledgerPort,
|
||||
clientConfig,
|
||||
clientChannelConfig,
|
||||
nonRepudiationConfig,
|
||||
)
|
||||
client.onComplete {
|
||||
case Success(_) =>
|
||||
@ -104,7 +99,6 @@ trait LedgerClientBase {
|
||||
ledgerPort: Int,
|
||||
clientConfig: LedgerClientConfiguration,
|
||||
clientChannelConfig: LedgerClientChannelConfiguration,
|
||||
nonRepudiationConfig: nonrepudiation.Configuration.Cli,
|
||||
)(implicit
|
||||
ec: ExecutionContext,
|
||||
aesf: ExecutionSequencerFactory,
|
||||
@ -114,7 +108,6 @@ trait LedgerClientBase {
|
||||
ledgerPort,
|
||||
clientConfig,
|
||||
clientChannelConfig,
|
||||
nonRepudiationConfig,
|
||||
)
|
||||
.map(_.right)
|
||||
.recover { case NonFatal(e) =>
|
||||
|
@ -92,7 +92,6 @@ object HttpServiceTestFixture extends LazyLogging with Assertions with Inside {
|
||||
useTls: UseTls = UseTls.NoTls,
|
||||
useHttps: UseHttps = UseHttps.NoHttps,
|
||||
wsConfig: Option[WebsocketConfig] = None,
|
||||
nonRepudiation: nonrepudiation.Configuration.Cli = nonrepudiation.Configuration.Cli.Empty,
|
||||
ledgerIdOverwrite: Option[LedgerId] = None,
|
||||
token: Option[Jwt] = None,
|
||||
targetScope: Option[String] = None,
|
||||
@ -125,7 +124,6 @@ object HttpServiceTestFixture extends LazyLogging with Assertions with Inside {
|
||||
staticContentConfig = staticContentConfig,
|
||||
authConfig = targetScope.map(scope => new AuthConfig(Some(scope))),
|
||||
packageReloadInterval = doNotReloadPackages,
|
||||
nonRepudiation = nonRepudiation,
|
||||
)
|
||||
httpService <- stripLeft(
|
||||
HttpService.start(
|
||||
|
@ -325,7 +325,6 @@ alias(
|
||||
"@maven//:org_scalatest_scalatest_shouldmatchers",
|
||||
"@maven//:org_scalaz_scalaz_core",
|
||||
"@maven//:org_tpolecat_doobie_core",
|
||||
"@maven//:org_tpolecat_doobie_hikari",
|
||||
"@maven//:org_tpolecat_doobie_free",
|
||||
"@maven//:org_typelevel_cats_core",
|
||||
"@maven//:org_typelevel_cats_effect",
|
||||
@ -360,20 +359,13 @@ alias(
|
||||
"//libs-scala/contextualized-logging",
|
||||
"//libs-scala/crypto",
|
||||
"//libs-scala/db-utils",
|
||||
"//libs-scala/doobie-slf4j",
|
||||
"//libs-scala/jwt",
|
||||
"//libs-scala/ports",
|
||||
"//libs-scala/ports:ports-testing",
|
||||
"//libs-scala/postgresql-testing",
|
||||
"//libs-scala/resources",
|
||||
"//libs-scala/resources-grpc",
|
||||
"//libs-scala/scala-utils",
|
||||
"//libs-scala/timer-utils",
|
||||
"//observability/metrics",
|
||||
"//observability/metrics:metrics-test-lib",
|
||||
"//runtime-components/non-repudiation",
|
||||
"//runtime-components/non-repudiation-postgresql",
|
||||
"//runtime-components/non-repudiation-testing",
|
||||
"//test-common:dar-files-default-lib",
|
||||
"//test-common/canton/it-lib",
|
||||
"@maven//:com_google_guava_guava",
|
||||
@ -531,8 +523,6 @@ alias(
|
||||
"//libs-scala/timer-utils",
|
||||
"//observability/metrics",
|
||||
"//observability/metrics:metrics-test-lib",
|
||||
"//runtime-components/non-repudiation",
|
||||
"//runtime-components/non-repudiation-postgresql",
|
||||
"//test-common/canton/it-lib",
|
||||
"@maven//:org_scalatest_scalatest_compatible",
|
||||
],
|
||||
|
@ -1,45 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.http
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import org.apache.pekko.http.scaladsl.model.StatusCodes
|
||||
import com.daml.nonrepudiation.CommandIdString
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements"))
|
||||
abstract class NonRepudiationTest extends AbstractNonRepudiationTest {
|
||||
|
||||
import HttpServiceTestFixture._
|
||||
|
||||
"fail to work through the non-repudiation proxy" in withSetup { fixture =>
|
||||
import fixture.db
|
||||
val expectedParty = "Alice"
|
||||
val expectedNumber = "abc123"
|
||||
val expectedCommandId = UUID.randomUUID.toString
|
||||
val meta = Some(
|
||||
domain.CommandMeta(
|
||||
commandId = Some(domain.CommandId(expectedCommandId)),
|
||||
actAs = None,
|
||||
readAs = None,
|
||||
submissionId = None,
|
||||
deduplicationPeriod = None,
|
||||
disclosedContracts = None,
|
||||
)
|
||||
)
|
||||
val domainParty = domain.Party(expectedParty)
|
||||
val command = accountCreateCommand(domainParty, expectedNumber).copy(meta = meta)
|
||||
postCreateCommand(command, fixture)
|
||||
.flatMap(inside(_) { case domain.ErrorResponse(_, _, status, _) =>
|
||||
status shouldBe StatusCodes.InternalServerError
|
||||
val payloads = db.signedPayloads.get(CommandIdString.wrap(expectedCommandId))
|
||||
payloads shouldBe empty
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class NonRepudiationTestCustomToken
|
||||
extends NonRepudiationTest
|
||||
with AbstractHttpServiceIntegrationTestFunsCustomToken
|
@ -1,52 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.http
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import org.apache.pekko.http.scaladsl.model.StatusCodes
|
||||
import com.daml.ledger.api.v1.command_submission_service.SubmitRequest
|
||||
import com.daml.nonrepudiation.CommandIdString
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements"))
|
||||
final class NonRepudiationTest
|
||||
extends AbstractNonRepudiationTest
|
||||
with AbstractHttpServiceIntegrationTestFunsCustomToken {
|
||||
|
||||
import HttpServiceTestFixture._
|
||||
|
||||
"correctly sign a command" in withSetup { fixture =>
|
||||
import fixture.db
|
||||
val expectedParty = "Alice"
|
||||
val expectedNumber = "abc123"
|
||||
val expectedCommandId = UUID.randomUUID.toString
|
||||
val meta = Some(
|
||||
domain.CommandMeta(
|
||||
commandId = Some(domain.CommandId(expectedCommandId)),
|
||||
actAs = None,
|
||||
readAs = None,
|
||||
submissionId = None,
|
||||
deduplicationPeriod = None,
|
||||
disclosedContracts = None,
|
||||
)
|
||||
)
|
||||
for {
|
||||
(domainParty, headers) <- fixture.getUniquePartyAndAuthHeaders(expectedParty)
|
||||
command = accountCreateCommand(domainParty, expectedNumber).copy(meta = meta)
|
||||
res <- postCreateCommand(command, fixture, headers)
|
||||
_ <- inside(res) { case domain.OkResponse(_, _, status) =>
|
||||
status shouldBe StatusCodes.OK
|
||||
val payloads = db.signedPayloads.get(CommandIdString.wrap(expectedCommandId))
|
||||
payloads should have size 1
|
||||
val signedCommand = SubmitRequest.parseFrom(payloads.head.payload.unsafeArray)
|
||||
val commands = signedCommand.getCommands.commands
|
||||
commands should have size 1
|
||||
val actualFields = commands.head.getCreate.getCreateArguments.fields.map(stripIdentifiers)
|
||||
val expectedFields = command.payload.fields.map(stripIdentifiers)
|
||||
actualFields should contain theSameElementsAs expectedFields
|
||||
}
|
||||
} yield succeed
|
||||
}
|
||||
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.http
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Clock
|
||||
|
||||
import org.apache.pekko.http.scaladsl.model.Uri
|
||||
import com.daml.doobie.logging.Slf4jLogHandler
|
||||
import com.daml.http.dbbackend.JdbcConfig
|
||||
import com.daml.http.json.{DomainJsonDecoder, DomainJsonEncoder}
|
||||
import com.daml.ledger.api.v1.command_service.CommandServiceGrpc
|
||||
import com.daml.ledger.api.v1.command_submission_service.CommandSubmissionServiceGrpc
|
||||
import com.daml.ledger.api.v1.value.Value.Sum
|
||||
import com.daml.ledger.api.v1.value.{RecordField, Value, Variant}
|
||||
import com.daml.ledger.client.withoutledgerid.{LedgerClient => DamlLedgerClient}
|
||||
import com.daml.metrics.api.noop.NoOpMetricsFactory
|
||||
import com.daml.nonrepudiation.{Metrics, NonRepudiationProxy}
|
||||
import com.daml.nonrepudiation.postgresql.{Tables, createTransactor}
|
||||
import com.daml.nonrepudiation.testing.generateKeyAndCertificate
|
||||
import com.daml.ports.{FreePort, Port}
|
||||
import com.daml.resources.grpc.GrpcResourceOwnerFactories
|
||||
import com.daml.testing.postgresql.PostgresAroundEach
|
||||
import io.grpc.Server
|
||||
import io.grpc.netty.{NettyChannelBuilder, NettyServerBuilder}
|
||||
import org.scalatest.freespec.AsyncFreeSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.{Assertion, BeforeAndAfterEach, Inside}
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration.DurationInt
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements"))
|
||||
abstract class AbstractNonRepudiationTest
|
||||
extends AsyncFreeSpec
|
||||
with Matchers
|
||||
with Inside
|
||||
with BeforeAndAfterEach
|
||||
with AbstractHttpServiceIntegrationTestFuns
|
||||
with PostgresAroundEach {
|
||||
|
||||
import AbstractNonRepudiationTest._
|
||||
import HttpServiceTestFixture._
|
||||
|
||||
private var nonRepudiation: nonrepudiation.Configuration.Cli = _
|
||||
private var certificate: X509Certificate = _
|
||||
|
||||
override protected def beforeEach(): Unit = {
|
||||
super.beforeEach()
|
||||
val (key, cert) = generateKeyAndCertificate()
|
||||
certificate = cert
|
||||
val certificatePath = Files.createTempFile("non-repudiation-test", "certificate")
|
||||
val privateKeyPath = Files.createTempFile("non-repudiation-test", "key")
|
||||
Files.write(certificatePath, cert.getEncoded)
|
||||
Files.write(privateKeyPath, key.getEncoded)
|
||||
nonRepudiation =
|
||||
nonrepudiation.Configuration.Cli(certificatePath, privateKeyPath, key.getAlgorithm)
|
||||
}
|
||||
|
||||
override protected def afterEach(): Unit = {
|
||||
super.afterEach()
|
||||
nonRepudiation.certificateFile.foreach(Files.delete)
|
||||
nonRepudiation.privateKeyFile.foreach(Files.delete)
|
||||
}
|
||||
|
||||
override val jdbcConfig: Option[JdbcConfig] = None
|
||||
|
||||
override val staticContentConfig: Option[StaticContentConfig] = None
|
||||
|
||||
override val useTls: UseTls = UseTls.NoTls
|
||||
|
||||
override val wsConfig: Option[WebsocketConfig] = None
|
||||
|
||||
protected def stripIdentifiers(field: RecordField): RecordField =
|
||||
field.copy(value = Some(stripIdentifiers(field.getValue)))
|
||||
|
||||
// Doesn't aim at being complete, neither in stripping identifiers recursively
|
||||
// Only covers variant because it's the only case interesting for the test cases here
|
||||
private def stripIdentifiers(value: Value): Value =
|
||||
value match {
|
||||
case Value(Sum.Variant(Variant(Some(_), constructor, value))) =>
|
||||
Value(Sum.Variant(Variant(None, constructor, value)))
|
||||
case _ => value
|
||||
}
|
||||
|
||||
private def withParticipant[A] =
|
||||
usingLedger[A]() _
|
||||
|
||||
private def withJsonApi[A](participantPort: Port) =
|
||||
HttpServiceTestFixture.withHttpService[A](
|
||||
testName = testId,
|
||||
ledgerPort = participantPort,
|
||||
jdbcConfig = jdbcConfig,
|
||||
staticContentConfig = staticContentConfig,
|
||||
leakPasswords = LeakPasswords.No,
|
||||
useTls = useTls,
|
||||
wsConfig = wsConfig,
|
||||
nonRepudiation = nonRepudiation,
|
||||
) _
|
||||
|
||||
protected def withSetup[A](test: SetupFixture => Future[Assertion]) =
|
||||
withParticipant { case (participantPort, _: DamlLedgerClient, _) =>
|
||||
val participantChannelBuilder =
|
||||
NettyChannelBuilder
|
||||
.forAddress("localhost", participantPort.value)
|
||||
.usePlaintext()
|
||||
|
||||
val proxyPort = FreePort.find()
|
||||
|
||||
val proxyBuilder = NettyServerBuilder.forPort(proxyPort.value)
|
||||
|
||||
val setup =
|
||||
for {
|
||||
participantChannel <- GrpcResourceOwnerFactories.forChannel(
|
||||
participantChannelBuilder,
|
||||
shutdownTimeout = 5.seconds,
|
||||
)
|
||||
transactor <- createTransactor(
|
||||
postgresDatabase.url,
|
||||
postgresDatabase.userName,
|
||||
postgresDatabase.password,
|
||||
maxPoolSize = 10,
|
||||
GrpcResourceOwnerFactories,
|
||||
)
|
||||
db = Tables.initialize(transactor)(Slf4jLogHandler(getClass))
|
||||
_ = db.certificates.put(certificate)
|
||||
proxy <- NonRepudiationProxy.owner(
|
||||
participantChannel,
|
||||
proxyBuilder,
|
||||
db.certificates,
|
||||
db.signedPayloads,
|
||||
Clock.systemUTC(),
|
||||
new Metrics(NoOpMetricsFactory),
|
||||
CommandServiceGrpc.CommandService.scalaDescriptor.fullName,
|
||||
CommandSubmissionServiceGrpc.CommandSubmissionService.scalaDescriptor.fullName,
|
||||
)
|
||||
} yield (proxy, db)
|
||||
|
||||
setup.use { case (_: Server, db: Tables) =>
|
||||
withJsonApi(proxyPort) { (uri, encoder, _: DomainJsonDecoder, _: DamlLedgerClient) =>
|
||||
test(SetupFixture(db, uri, encoder))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object AbstractNonRepudiationTest {
|
||||
import AbstractHttpServiceIntegrationTestFuns.{UriFixture, EncoderFixture}
|
||||
final case class SetupFixture(db: Tables, uri: Uri, encoder: DomainJsonEncoder)
|
||||
extends UriFixture
|
||||
with EncoderFixture
|
||||
}
|
@ -118,7 +118,6 @@ object HttpService {
|
||||
ledgerPort,
|
||||
clientConfig,
|
||||
clientChannelConfiguration,
|
||||
startSettings.nonRepudiation,
|
||||
)
|
||||
): ET[DamlLedgerClient]
|
||||
|
||||
@ -130,7 +129,6 @@ object HttpService {
|
||||
packageMaxInboundMessageSize.fold(clientChannelConfiguration)(size =>
|
||||
clientChannelConfiguration.copy(maxInboundMessageSize = size)
|
||||
),
|
||||
startSettings.nonRepudiation,
|
||||
)
|
||||
): ET[DamlLedgerClient]
|
||||
|
||||
@ -407,7 +405,6 @@ object HttpService {
|
||||
ledgerPort: Int,
|
||||
clientConfig: LedgerClientConfiguration,
|
||||
clientChannelConfig: LedgerClientChannelConfiguration,
|
||||
nonRepudiationConfig: nonrepudiation.Configuration.Cli,
|
||||
)(implicit
|
||||
ec: ExecutionContext,
|
||||
aesf: ExecutionSequencerFactory,
|
||||
@ -419,7 +416,6 @@ object HttpService {
|
||||
ledgerPort,
|
||||
clientConfig,
|
||||
clientChannelConfig,
|
||||
nonRepudiationConfig,
|
||||
MaxInitialLedgerConnectRetryAttempts,
|
||||
)
|
||||
.map(
|
||||
|
@ -86,9 +86,6 @@ object Main {
|
||||
s", authConfig=${config.authConfig.shows}" +
|
||||
s", allowNonHttps=${config.allowNonHttps.shows}" +
|
||||
s", wsConfig=${config.wsConfig.shows}" +
|
||||
s", nonRepudiationCertificateFile=${config.nonRepudiation.certificateFile: Option[Path]}" +
|
||||
s", nonRepudiationPrivateKeyFile=${config.nonRepudiation.privateKeyFile: Option[Path]}" +
|
||||
s", nonRepudiationPrivateKeyAlgorithm=${config.nonRepudiation.privateKeyAlgorithm: Option[String]}" +
|
||||
s", surrogateTpIdCacheMaxEntries=${config.surrogateTpIdCacheMaxEntries: Option[Long]}" +
|
||||
")"
|
||||
)
|
||||
|
@ -75,11 +75,6 @@ object Security {
|
||||
/** Whether a secure configuration can effectively be enforced */
|
||||
case object SecureConfiguration extends Property
|
||||
|
||||
/** Assurance that the sender of information is provided with proof of delivery and the recipient is provided
|
||||
* with proof of the sender’s identity, so neither can later deny having processed the information.
|
||||
*/
|
||||
case object NonRepudiation extends Property
|
||||
|
||||
/** Ability to withstand and recover from attacks, threats or incidents */
|
||||
case object Resilience extends Property
|
||||
}
|
||||
|
@ -1,60 +0,0 @@
|
||||
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
load(
|
||||
"//bazel_tools:scala.bzl",
|
||||
"da_scala_library",
|
||||
"da_scala_test",
|
||||
)
|
||||
|
||||
da_scala_library(
|
||||
name = "non-repudiation-api",
|
||||
srcs = glob(["src/main/scala/**/*.scala"]),
|
||||
scala_deps = [
|
||||
"@maven//:org_apache_pekko_pekko_actor",
|
||||
"@maven//:org_apache_pekko_pekko_http",
|
||||
"@maven//:org_apache_pekko_pekko_http_core",
|
||||
"@maven//:org_apache_pekko_pekko_http_spray_json",
|
||||
"@maven//:org_apache_pekko_pekko_stream",
|
||||
"@maven//:io_spray_spray_json",
|
||||
],
|
||||
visibility = [
|
||||
"//:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//libs-scala/resources",
|
||||
"//runtime-components/non-repudiation",
|
||||
"@maven//:com_google_guava_guava",
|
||||
"@maven//:org_slf4j_slf4j_api",
|
||||
],
|
||||
)
|
||||
|
||||
da_scala_test(
|
||||
name = "test",
|
||||
srcs = glob(["src/test/scala/**/*.scala"]),
|
||||
resources = [
|
||||
"src/test/resources/logback-test.xml",
|
||||
],
|
||||
scala_deps = [
|
||||
"@maven//:org_apache_pekko_pekko_actor",
|
||||
"@maven//:org_apache_pekko_pekko_http",
|
||||
"@maven//:org_apache_pekko_pekko_http_core",
|
||||
"@maven//:org_apache_pekko_pekko_http_spray_json",
|
||||
"@maven//:org_apache_pekko_pekko_stream",
|
||||
"@maven//:io_spray_spray_json",
|
||||
],
|
||||
runtime_deps = [
|
||||
"@maven//:ch_qos_logback_logback_classic",
|
||||
],
|
||||
deps = [
|
||||
":non-repudiation-api",
|
||||
"//canton:ledger_api_proto_scala",
|
||||
"//libs-scala/ports",
|
||||
"//libs-scala/ports:ports-testing",
|
||||
"//libs-scala/resources",
|
||||
"//runtime-components/non-repudiation",
|
||||
"//runtime-components/non-repudiation-testing",
|
||||
"@maven//:com_google_guava_guava",
|
||||
"@maven//:org_slf4j_slf4j_api",
|
||||
],
|
||||
)
|
@ -1,68 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.api
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem
|
||||
import org.apache.pekko.http.scaladsl.Http
|
||||
import org.apache.pekko.http.scaladsl.server.Directives.{concat, pathPrefix}
|
||||
import org.apache.pekko.http.scaladsl.server.Route
|
||||
import com.daml.nonrepudiation.{CertificateRepository, CommandIdString, SignedPayloadRepository}
|
||||
import com.daml.resources.{AbstractResourceOwner, HasExecutionContext, ReleasableResource, Resource}
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
object NonRepudiationApi {
|
||||
|
||||
// We don't need access to the underlying resource, we use
|
||||
// the owner only to manage the server's life cycle
|
||||
def owner[Context: HasExecutionContext](
|
||||
address: InetSocketAddress,
|
||||
shutdownTimeout: FiniteDuration,
|
||||
certificateRepository: CertificateRepository,
|
||||
signedPayloadRepository: SignedPayloadRepository.Read[CommandIdString],
|
||||
actorSystem: ActorSystem,
|
||||
): AbstractResourceOwner[Context, Unit] =
|
||||
new NonRepudiationApi[Context](
|
||||
address,
|
||||
shutdownTimeout,
|
||||
certificateRepository,
|
||||
signedPayloadRepository,
|
||||
actorSystem,
|
||||
).map(_ => ())
|
||||
|
||||
}
|
||||
|
||||
final class NonRepudiationApi[Context: HasExecutionContext] private (
|
||||
address: InetSocketAddress,
|
||||
shutdownTimeout: FiniteDuration,
|
||||
certificates: CertificateRepository,
|
||||
signedPayloads: SignedPayloadRepository.Read[CommandIdString],
|
||||
actorSystem: ActorSystem,
|
||||
) extends AbstractResourceOwner[Context, Http.ServerBinding] {
|
||||
|
||||
private val route: Route =
|
||||
pathPrefix("v1") {
|
||||
concat(
|
||||
pathPrefix("certificate") { v1.CertificatesEndpoint(certificates) },
|
||||
pathPrefix("command") { v1.SignedPayloadsEndpoint(signedPayloads) },
|
||||
)
|
||||
}
|
||||
|
||||
private def bindNewServer(): Future[Http.ServerBinding] = {
|
||||
implicit val system: ActorSystem = actorSystem
|
||||
Http().newServerAt(address.getAddress.getHostAddress, address.getPort).bind(route)
|
||||
}
|
||||
|
||||
override def acquire()(implicit context: Context): Resource[Context, Http.ServerBinding] =
|
||||
ReleasableResource(bindNewServer()) { server =>
|
||||
for {
|
||||
_ <- server.unbind()
|
||||
_ <- server.terminate(shutdownTimeout)
|
||||
} yield ()
|
||||
}
|
||||
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.api
|
||||
|
||||
import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
|
||||
import org.apache.pekko.http.scaladsl.model.StatusCodes
|
||||
import org.apache.pekko.http.scaladsl.server.Directives.complete
|
||||
import org.apache.pekko.http.scaladsl.server.{ExceptionHandler, StandardRoute}
|
||||
import org.slf4j.Logger
|
||||
import spray.json.{DefaultJsonProtocol, JsonFormat, RootJsonFormat}
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
object Result {
|
||||
|
||||
final case class Success[A](result: A, status: Int)
|
||||
|
||||
final case class Failure(error: String, status: Int)
|
||||
|
||||
trait JsonProtocol extends SprayJsonSupport with DefaultJsonProtocol {
|
||||
|
||||
protected implicit def successFormat[A: JsonFormat]: RootJsonFormat[Success[A]] =
|
||||
jsonFormat2(Success[A])
|
||||
|
||||
protected implicit val failureFormat: RootJsonFormat[Failure] =
|
||||
jsonFormat2(Failure)
|
||||
|
||||
protected def rejectBadInput(error: String): StandardRoute =
|
||||
complete(StatusCodes.BadRequest, Failure(error, status = 400))
|
||||
|
||||
protected def logAndReport(logger: Logger)(error: String): ExceptionHandler =
|
||||
ExceptionHandler { case NonFatal(exception) =>
|
||||
logger.error("An exception occurred on the server while processing a request", exception)
|
||||
complete(StatusCodes.InternalServerError, Failure(error, status = 500))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.api.v1
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.security.cert.{CertificateFactory, X509Certificate}
|
||||
|
||||
import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
|
||||
import org.apache.pekko.http.scaladsl.server.Directives._
|
||||
import org.apache.pekko.http.scaladsl.server.Route
|
||||
import com.daml.nonrepudiation.api.Result
|
||||
import com.daml.nonrepudiation.{CertificateRepository, FingerprintBytes}
|
||||
import com.google.common.io.BaseEncoding
|
||||
import org.slf4j.{Logger, LoggerFactory}
|
||||
import spray.json.{DefaultJsonProtocol, RootJsonFormat}
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
private[api] final class CertificatesEndpoint private (certificates: CertificateRepository)
|
||||
extends Result.JsonProtocol
|
||||
with CertificatesEndpoint.JsonProtocol {
|
||||
|
||||
import CertificatesEndpoint._
|
||||
|
||||
private def putCertificate(certificate: X509Certificate): Route =
|
||||
handleExceptions(logAndReport(logger)(UnableToAddTheCertificate)) {
|
||||
val fingerprint = certificates.put(certificate)
|
||||
complete(Result.Success(encode(fingerprint), 200))
|
||||
}
|
||||
|
||||
private def getCertificate(fingerprint: FingerprintBytes): Route =
|
||||
handleExceptions(logAndReport(logger)(UnableToRetrieveTheCertificate)) {
|
||||
certificates
|
||||
.get(fingerprint)
|
||||
.fold(reject)(certificate => complete(Result.Success(encode(certificate), 200)))
|
||||
}
|
||||
|
||||
private val route: Route =
|
||||
concat(
|
||||
put {
|
||||
decodeRequest {
|
||||
entity(as[CertificatesEndpoint.Certificate]) {
|
||||
decode(_).fold(rejectBadInput, putCertificate)
|
||||
}
|
||||
}
|
||||
},
|
||||
path(Segment) { fingerprint =>
|
||||
get {
|
||||
decode(fingerprint).fold(rejectBadInput, getCertificate)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
object CertificatesEndpoint {
|
||||
|
||||
def apply(certificates: CertificateRepository): Route =
|
||||
new CertificatesEndpoint(certificates).route
|
||||
|
||||
private val logger: Logger = LoggerFactory.getLogger(classOf[CertificatesEndpoint])
|
||||
|
||||
private[api] val InvalidCertificateString: String =
|
||||
"Invalid upload, the 'certificate' field must contain a URL-safe base64-encoded X.509 certificate"
|
||||
|
||||
private[api] val InvalidCertificateFormat: String =
|
||||
"Invalid certificate format, the certificate must be a valid X.509 certificate"
|
||||
|
||||
private[api] val UnableToAddTheCertificate: String =
|
||||
"An error occurred when trying to add the certificate, please try again."
|
||||
|
||||
private[api] val UnableToRetrieveTheCertificate: String =
|
||||
"An error occurred when trying to retrieve the certificate, please try again."
|
||||
|
||||
private[api] val InvalidFingerprintString: String =
|
||||
"Invalid request, the fingerprint must be a URL-safe base64-encoded array of bytes representing the SHA256 of the DER representation of an X.509 certificate"
|
||||
|
||||
private val certificateFactory: CertificateFactory =
|
||||
CertificateFactory.getInstance("X.509")
|
||||
|
||||
private def decodeCertificate(bytes: Array[Byte]): Either[String, X509Certificate] =
|
||||
Try(
|
||||
certificateFactory
|
||||
.generateCertificate(new ByteArrayInputStream(bytes))
|
||||
.asInstanceOf[X509Certificate]
|
||||
).toEither.left.map(_ => InvalidCertificateFormat)
|
||||
|
||||
private def decodeBytes(string: String, error: String): Either[String, Array[Byte]] =
|
||||
Try(BaseEncoding.base64Url().decode(string)).toEither.left.map(_ => error)
|
||||
|
||||
private def decode(request: Certificate): Either[String, X509Certificate] =
|
||||
decodeBytes(request.certificate, InvalidCertificateString).flatMap(decodeCertificate)
|
||||
|
||||
private def decode(fingerprint: String): Either[String, FingerprintBytes] =
|
||||
decodeBytes(fingerprint, InvalidFingerprintString).map(FingerprintBytes.wrap)
|
||||
|
||||
final case class Certificate(certificate: String)
|
||||
|
||||
private def encode(bytes: FingerprintBytes): Fingerprint =
|
||||
Fingerprint(BaseEncoding.base64Url().encode(bytes.unsafeArray))
|
||||
|
||||
// URL-safe encoding is used for certificate as well even though they don't require
|
||||
// it to make sure there is only one encoding used across the API (fingerprints can
|
||||
// be passed in URLs for GETs and thus are required to be URL-safe)
|
||||
private def encode(certificate: X509Certificate): Certificate =
|
||||
Certificate(BaseEncoding.base64Url().encode(certificate.getEncoded))
|
||||
|
||||
final case class Fingerprint(fingerprint: String)
|
||||
|
||||
private[api] trait JsonProtocol extends SprayJsonSupport with DefaultJsonProtocol {
|
||||
|
||||
protected implicit val certificateFormat: RootJsonFormat[Certificate] =
|
||||
jsonFormat1(Certificate.apply)
|
||||
|
||||
protected implicit val fingerprintFormat: RootJsonFormat[Fingerprint] =
|
||||
jsonFormat1(Fingerprint.apply)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.api.v1
|
||||
|
||||
import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
|
||||
import org.apache.pekko.http.scaladsl.server.Directives._
|
||||
import org.apache.pekko.http.scaladsl.server.Route
|
||||
import com.daml.nonrepudiation.api.Result
|
||||
import com.daml.nonrepudiation.{CommandIdString, SignedPayload, SignedPayloadRepository}
|
||||
import com.google.common.io.BaseEncoding
|
||||
import org.slf4j.{Logger, LoggerFactory}
|
||||
import spray.json.{DefaultJsonProtocol, RootJsonFormat}
|
||||
|
||||
import scala.collection.immutable.ArraySeq
|
||||
|
||||
private[api] final class SignedPayloadsEndpoint private (
|
||||
signedPayloads: SignedPayloadRepository.Read[CommandIdString]
|
||||
) extends Result.JsonProtocol
|
||||
with SignedPayloadsEndpoint.JsonProtocol {
|
||||
|
||||
import SignedPayloadsEndpoint._
|
||||
|
||||
private val route: Route =
|
||||
path(Segment.map(CommandIdString.wrap)) { commandId =>
|
||||
get {
|
||||
handleExceptions(logAndReport(logger)(UnableToRetrieveTheSignedPayload)) {
|
||||
val responses = signedPayloads.get(commandId).map(toResponse)
|
||||
if (responses.nonEmpty) {
|
||||
complete(Result.Success(responses, 200))
|
||||
} else {
|
||||
reject
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object SignedPayloadsEndpoint {
|
||||
|
||||
def apply(signedPayloads: SignedPayloadRepository.Read[CommandIdString]): Route =
|
||||
new SignedPayloadsEndpoint(signedPayloads).route
|
||||
|
||||
private val logger: Logger = LoggerFactory.getLogger(classOf[SignedPayloadsEndpoint])
|
||||
|
||||
private[api] val UnableToRetrieveTheSignedPayload: String =
|
||||
"An error occurred when trying to retrieve the signed payload, please try again."
|
||||
|
||||
final case class Response(
|
||||
algorithm: String,
|
||||
fingerprint: String,
|
||||
payload: String,
|
||||
signature: String,
|
||||
timestamp: Long,
|
||||
)
|
||||
|
||||
private def base64Url(bytes: ArraySeq.ofByte): String =
|
||||
BaseEncoding.base64Url().encode(bytes.unsafeArray)
|
||||
|
||||
def toResponse(signedPayload: SignedPayload): Response =
|
||||
Response(
|
||||
algorithm = signedPayload.algorithm,
|
||||
fingerprint = base64Url(signedPayload.fingerprint),
|
||||
payload = base64Url(signedPayload.payload),
|
||||
signature = base64Url(signedPayload.signature),
|
||||
timestamp = signedPayload.timestamp.toEpochMilli,
|
||||
)
|
||||
|
||||
private[api] trait JsonProtocol extends SprayJsonSupport with DefaultJsonProtocol {
|
||||
|
||||
protected implicit val apiSignedPayloadFormat: RootJsonFormat[Response] =
|
||||
jsonFormat5(Response.apply)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="stderr-appender" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<target>System.err</target>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>trace</level>
|
||||
</filter>
|
||||
<encoder>
|
||||
<pattern>%date{"yyyy-MM-dd'T'HH:mm:ss.SSSXXX", UTC} %-5level %logger{5}@[%-4.30thread] - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="${LOGLEVEL:-ERROR}">
|
||||
<appender-ref ref="stderr-appender"/>
|
||||
</root>
|
||||
|
||||
<logger name="io.netty" level="WARN">
|
||||
<appender-ref ref="stderr-appender"/>
|
||||
</logger>
|
||||
<logger name="io.grpc.netty" level="WARN">
|
||||
<appender-ref ref="stderr-appender"/>
|
||||
</logger>
|
||||
</configuration>
|
@ -1,320 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.api
|
||||
|
||||
import java.net.{InetAddress, InetSocketAddress}
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem
|
||||
import org.apache.pekko.http.scaladsl.Http
|
||||
import org.apache.pekko.http.scaladsl.client.RequestBuilding.{Get, Put}
|
||||
import org.apache.pekko.http.scaladsl.model.{ContentTypes, HttpEntity, HttpResponse}
|
||||
import org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal
|
||||
import com.daml.nonrepudiation.api.v1.{CertificatesEndpoint, SignedPayloadsEndpoint}
|
||||
import com.daml.nonrepudiation.testing._
|
||||
import com.daml.nonrepudiation.{
|
||||
AlgorithmString,
|
||||
CertificateRepository,
|
||||
CommandIdString,
|
||||
FingerprintBytes,
|
||||
PayloadBytes,
|
||||
SignatureBytes,
|
||||
SignedPayload,
|
||||
SignedPayloadRepository,
|
||||
}
|
||||
import com.daml.ports.FreePort
|
||||
import com.google.common.io.BaseEncoding
|
||||
import org.scalatest.flatspec.AsyncFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.{Assertion, BeforeAndAfterAll, OptionValues}
|
||||
import spray.json.JsonFormat
|
||||
|
||||
import scala.concurrent.duration.DurationInt
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.util.Random
|
||||
|
||||
final class NonRepudiationApiSpec
|
||||
extends AsyncFlatSpec
|
||||
with Matchers
|
||||
with OptionValues
|
||||
with BeforeAndAfterAll
|
||||
with Result.JsonProtocol
|
||||
with SignedPayloadsEndpoint.JsonProtocol
|
||||
with CertificatesEndpoint.JsonProtocol {
|
||||
|
||||
import NonRepudiationApiSpec._
|
||||
|
||||
private implicit val actorSystem: ActorSystem = ActorSystem("non-repudiation-api-test")
|
||||
|
||||
behavior of "NonRepudiationApi"
|
||||
|
||||
it should "correctly retrieve saved payloads by identifier" in withApi() {
|
||||
(baseUrl, _, signedPayloads) =>
|
||||
val commandId = UUID.randomUUID.toString
|
||||
|
||||
val firstSignedPayload =
|
||||
SignedPayload(
|
||||
algorithm = AlgorithmString.wrap("SuperCryptoStuff256"),
|
||||
fingerprint = FingerprintBytes.wrap("fingerprint-1".getBytes),
|
||||
payload = PayloadBytes.wrap(generateCommand(commandId = commandId).toByteArray),
|
||||
signature = SignatureBytes.wrap("signature-1".getBytes),
|
||||
timestamp = Instant.ofEpochMilli(42),
|
||||
)
|
||||
|
||||
val firstExpectedResponse = SignedPayloadsEndpoint.toResponse(firstSignedPayload)
|
||||
|
||||
val secondSignedPayload =
|
||||
SignedPayload(
|
||||
algorithm = AlgorithmString.wrap("SuperCryptoStuff512"),
|
||||
fingerprint = FingerprintBytes.wrap("fingerprint-2".getBytes),
|
||||
payload = PayloadBytes.wrap(generateCommand(commandId = commandId).toByteArray),
|
||||
signature = SignatureBytes.wrap("signature-2".getBytes),
|
||||
timestamp = Instant.ofEpochMilli(47),
|
||||
)
|
||||
|
||||
val secondExpectedResponse = SignedPayloadsEndpoint.toResponse(secondSignedPayload)
|
||||
|
||||
for {
|
||||
firstResponse <- getSignedPayload(baseUrl, commandId)
|
||||
_ = firstResponse.status.intValue shouldBe 404
|
||||
_ = signedPayloads.put(toSignedPayload(firstExpectedResponse))
|
||||
secondResponse <- getSignedPayload(baseUrl, commandId)
|
||||
_ = secondResponse.status.intValue shouldBe 200
|
||||
payloadsAfterFirstAdd <- expect[List[SignedPayloadsEndpoint.Response]](secondResponse)
|
||||
_ = signedPayloads.put(toSignedPayload(secondExpectedResponse))
|
||||
thirdResponse <- getSignedPayload(baseUrl, commandId)
|
||||
_ = thirdResponse.status.intValue shouldBe 200
|
||||
payloadsAfterSecondAdd <- expect[List[SignedPayloadsEndpoint.Response]](thirdResponse)
|
||||
} yield {
|
||||
payloadsAfterFirstAdd.result should contain only firstExpectedResponse
|
||||
payloadsAfterSecondAdd.result should contain.allOf(
|
||||
firstExpectedResponse,
|
||||
secondExpectedResponse,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
it should "return the expected error if a signed payload cannot be retrieved" in withApi(
|
||||
signedPayloads = NonRepudiationApiSpec.BrokenSignedPayloads
|
||||
) { (baseUrl, _, _) =>
|
||||
for {
|
||||
response <- getSignedPayload(baseUrl, "not-really-important")
|
||||
failure <- Unmarshal(response).to[Result.Failure]
|
||||
} yield {
|
||||
response.status.intValue shouldBe 500
|
||||
failure.status shouldBe 500
|
||||
failure.error shouldBe SignedPayloadsEndpoint.UnableToRetrieveTheSignedPayload
|
||||
}
|
||||
}
|
||||
|
||||
it should "correctly show a certificate in the backend" in withApi() {
|
||||
(baseUrl, certificates, _) =>
|
||||
val (_, expectedCertificate) = generateKeyAndCertificate()
|
||||
val fingerprintBytes = FingerprintBytes.compute(expectedCertificate)
|
||||
val fingerprint = BaseEncoding.base64Url().encode(fingerprintBytes.unsafeArray)
|
||||
|
||||
for {
|
||||
firstResponse <- getCertificate(baseUrl, fingerprint)
|
||||
_ = firstResponse.status.intValue shouldBe 404
|
||||
expectedFingerprint = certificates.put(expectedCertificate)
|
||||
_ = fingerprintBytes shouldEqual expectedFingerprint
|
||||
secondResponse <- getCertificate(baseUrl, fingerprint)
|
||||
secondResponseBody <- expect[CertificatesEndpoint.Certificate](secondResponse)
|
||||
} yield {
|
||||
val encodedCertificate = secondResponseBody.result.certificate
|
||||
val certificateBytes = BaseEncoding.base64Url().decode(encodedCertificate)
|
||||
certificateBytes shouldEqual expectedCertificate.getEncoded
|
||||
}
|
||||
}
|
||||
|
||||
it should "correctly upload a certificate to the backend" in withApi() {
|
||||
(baseUrl, certificates, _) =>
|
||||
val (_, expectedCertificate) = generateKeyAndCertificate()
|
||||
val fingerprintBytes = FingerprintBytes.compute(expectedCertificate)
|
||||
val expectedFingerprint = BaseEncoding.base64Url().encode(fingerprintBytes.unsafeArray)
|
||||
|
||||
certificates.get(fingerprintBytes) shouldBe empty
|
||||
|
||||
for {
|
||||
response <- putCertificate(baseUrl, expectedCertificate)
|
||||
_ = response.status.intValue shouldBe 200
|
||||
responseBody <- expect[CertificatesEndpoint.Fingerprint](response)
|
||||
_ = responseBody.result.fingerprint shouldBe expectedFingerprint
|
||||
} yield {
|
||||
val certificate = certificates.get(fingerprintBytes).value
|
||||
certificate.getEncoded shouldEqual expectedCertificate.getEncoded
|
||||
}
|
||||
}
|
||||
|
||||
it should "return the expected error if the fingerprint string is malformed" in withApi() {
|
||||
(baseUrl, _, _) =>
|
||||
for {
|
||||
response <- getCertificate(baseUrl, "not-a-fingerprint")
|
||||
result <- Unmarshal(response).to[Result.Failure]
|
||||
} yield {
|
||||
response.status.intValue shouldBe 400
|
||||
result.status shouldBe 400
|
||||
result.error shouldBe CertificatesEndpoint.InvalidFingerprintString
|
||||
}
|
||||
}
|
||||
|
||||
it should "return 400 if the request is malformed" in withApi() { (baseUrl, _, _) =>
|
||||
for {
|
||||
response <- putCertificate(baseUrl, """{"foobar":42}""")
|
||||
} yield response.status.intValue shouldBe 400
|
||||
}
|
||||
|
||||
it should "return the expected error if the certificate string is malformed" in withApi() {
|
||||
(baseUrl, _, _) =>
|
||||
for {
|
||||
response <- putCertificate(baseUrl, """{"certificate":"Ceci n'est pas un certificat"}""")
|
||||
result <- Unmarshal(response).to[Result.Failure]
|
||||
} yield {
|
||||
response.status.intValue shouldBe 400
|
||||
result.status shouldBe 400
|
||||
result.error shouldBe CertificatesEndpoint.InvalidCertificateString
|
||||
}
|
||||
}
|
||||
|
||||
it should "return the expected error if the certificate is invalid" in withApi() {
|
||||
(baseUrl, _, _) =>
|
||||
val randomBytes = Array.fill(2048)(Random.nextInt().toByte)
|
||||
val malformedCertificate = BaseEncoding.base64Url().encode(randomBytes)
|
||||
for {
|
||||
response <- putCertificate(baseUrl, s"""{"certificate":"$malformedCertificate"}""")
|
||||
result <- Unmarshal(response).to[Result.Failure]
|
||||
} yield {
|
||||
response.status.intValue shouldBe 400
|
||||
result.status shouldBe 400
|
||||
result.error shouldBe CertificatesEndpoint.InvalidCertificateFormat
|
||||
}
|
||||
}
|
||||
|
||||
it should "return the expected error if a certificate cannot be added" in withApi(certificates =
|
||||
NonRepudiationApiSpec.BrokenCertificates
|
||||
) { (baseUrl, _, _) =>
|
||||
val (_, expectedCertificate) = generateKeyAndCertificate()
|
||||
val fingerprintBytes = FingerprintBytes.compute(expectedCertificate)
|
||||
val expectedFingerprint = BaseEncoding.base64Url().encode(fingerprintBytes.unsafeArray)
|
||||
|
||||
for {
|
||||
response <- getCertificate(baseUrl, expectedFingerprint)
|
||||
result <- Unmarshal(response).to[Result.Failure]
|
||||
} yield {
|
||||
response.status.intValue shouldBe 500
|
||||
result.status shouldBe 500
|
||||
result.error shouldBe CertificatesEndpoint.UnableToRetrieveTheCertificate
|
||||
}
|
||||
}
|
||||
|
||||
it should "return the expected error if a certificated cannot be retrieved" in withApi(
|
||||
certificates = NonRepudiationApiSpec.BrokenCertificates
|
||||
) { (baseUrl, _, _) =>
|
||||
val (_, expectedCertificate) = generateKeyAndCertificate()
|
||||
|
||||
for {
|
||||
response <- putCertificate(baseUrl, expectedCertificate)
|
||||
result <- Unmarshal(response).to[Result.Failure]
|
||||
} yield {
|
||||
response.status.intValue shouldBe 500
|
||||
result.status shouldBe 500
|
||||
result.error shouldBe CertificatesEndpoint.UnableToAddTheCertificate
|
||||
}
|
||||
}
|
||||
|
||||
def withApi(
|
||||
certificates: CertificateRepository = new Certificates,
|
||||
signedPayloads: SignedPayloadRepository[CommandIdString] = new SignedPayloads[CommandIdString],
|
||||
)(
|
||||
test: (
|
||||
String,
|
||||
CertificateRepository,
|
||||
SignedPayloadRepository[CommandIdString],
|
||||
) => Future[Assertion]
|
||||
): Future[Assertion] = {
|
||||
|
||||
val port = FreePort.find().value
|
||||
|
||||
val address = new InetSocketAddress(InetAddress.getLoopbackAddress, port)
|
||||
|
||||
val api =
|
||||
NonRepudiationApi.owner(
|
||||
address = address,
|
||||
shutdownTimeout = 10.seconds,
|
||||
certificates,
|
||||
signedPayloads,
|
||||
actorSystem,
|
||||
)
|
||||
|
||||
val baseUrl = s"http://${address.getAddress.getHostAddress}:${address.getPort}"
|
||||
|
||||
api.use { _ => test(baseUrl, certificates, signedPayloads) }
|
||||
|
||||
}
|
||||
|
||||
private def getSignedPayload(baseUrl: String, commandId: String): Future[HttpResponse] =
|
||||
Http().singleRequest(Get(s"$baseUrl/v1/command/$commandId"))
|
||||
|
||||
private def getCertificate(baseUrl: String, fingerprint: String): Future[HttpResponse] =
|
||||
Http().singleRequest(Get(s"$baseUrl/v1/certificate/$fingerprint"))
|
||||
|
||||
private def putCertificate(
|
||||
baseUrl: String,
|
||||
certificate: X509Certificate,
|
||||
): Future[HttpResponse] = {
|
||||
val encodedCertificate = BaseEncoding.base64Url().encode(certificate.getEncoded)
|
||||
val request = CertificatesEndpoint.Certificate(encodedCertificate)
|
||||
putCertificate(baseUrl, certificateFormat.write(request).toString)
|
||||
}
|
||||
|
||||
private def putCertificate(baseUrl: String, string: String): Future[HttpResponse] = {
|
||||
val entity = HttpEntity(contentType = ContentTypes.`application/json`, string)
|
||||
Http().singleRequest(Put(s"$baseUrl/v1/certificate").withEntity(entity))
|
||||
}
|
||||
|
||||
def expect[A: JsonFormat](response: HttpResponse): Future[Result.Success[A]] =
|
||||
Unmarshal(response).to[Result.Success[A]]
|
||||
|
||||
override protected def afterAll(): Unit = {
|
||||
super.afterAll()
|
||||
val _ = Await.result(actorSystem.terminate(), 5.seconds)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object NonRepudiationApiSpec {
|
||||
|
||||
private object BrokenCertificates extends CertificateRepository {
|
||||
|
||||
override def get(fingerprint: FingerprintBytes): Option[X509Certificate] =
|
||||
throw new UnsupportedOperationException("The certificate repository is broken")
|
||||
|
||||
override def put(certificate: X509Certificate): FingerprintBytes =
|
||||
throw new UnsupportedOperationException("The certificate repository is broken")
|
||||
|
||||
}
|
||||
|
||||
private object BrokenSignedPayloads extends SignedPayloadRepository[CommandIdString] {
|
||||
|
||||
override def get(key: CommandIdString): Iterable[SignedPayload] =
|
||||
throw new UnsupportedOperationException("The signed payload repository is broken")
|
||||
|
||||
override def put(signedPayload: SignedPayload): Unit =
|
||||
throw new UnsupportedOperationException("The signed payload repository is broken")
|
||||
|
||||
}
|
||||
|
||||
def toSignedPayload(response: SignedPayloadsEndpoint.Response): SignedPayload =
|
||||
SignedPayload(
|
||||
algorithm = AlgorithmString.wrap(response.algorithm),
|
||||
fingerprint = FingerprintBytes.wrap(BaseEncoding.base64Url().decode(response.fingerprint)),
|
||||
payload = PayloadBytes.wrap(BaseEncoding.base64Url().decode(response.payload)),
|
||||
signature = SignatureBytes.wrap(BaseEncoding.base64Url().decode(response.signature)),
|
||||
timestamp = Instant.ofEpochMilli(response.timestamp),
|
||||
)
|
||||
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.api.v1
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import com.daml.nonrepudiation.{
|
||||
AlgorithmString,
|
||||
FingerprintBytes,
|
||||
PayloadBytes,
|
||||
SignatureBytes,
|
||||
SignedPayload,
|
||||
}
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
final class SignedPayloadsEndpointSpec extends AnyFlatSpec with Matchers {
|
||||
|
||||
behavior of "SignedPayloadEndpoint.toResponse"
|
||||
|
||||
it should "convert the backend signed payload into the expected representation" in {
|
||||
|
||||
val algorithm = "algorithm"
|
||||
val fingerprint = "fingerprint".getBytes
|
||||
val payload = "payload".getBytes
|
||||
val signature = "signature".getBytes
|
||||
val timestamp = 42L
|
||||
|
||||
val signedPayload =
|
||||
SignedPayload(
|
||||
algorithm = AlgorithmString.wrap(algorithm),
|
||||
fingerprint = FingerprintBytes.wrap(fingerprint),
|
||||
payload = PayloadBytes.wrap(payload),
|
||||
signature = SignatureBytes.wrap(signature),
|
||||
timestamp = Instant.ofEpochMilli(timestamp),
|
||||
)
|
||||
|
||||
SignedPayloadsEndpoint.toResponse(signedPayload) shouldBe SignedPayloadsEndpoint.Response(
|
||||
algorithm = algorithm,
|
||||
fingerprint = "ZmluZ2VycHJpbnQ=", // URL-safe base64 encoded "fingerprint"
|
||||
payload = "cGF5bG9hZA==", // URL-safe base64 encoded "payload"
|
||||
signature = "c2lnbmF0dXJl", // URL-safe base64 encoded "signature"
|
||||
timestamp = timestamp,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
load(
|
||||
"//bazel_tools:scala.bzl",
|
||||
"da_scala_binary",
|
||||
"da_scala_test",
|
||||
)
|
||||
|
||||
da_scala_binary(
|
||||
name = "non-repudiation-app",
|
||||
srcs = glob(["src/main/scala/**/*.scala"]),
|
||||
main_class = "com.daml.nonrepudiation.app.NonRepudiationApp",
|
||||
scala_deps = [
|
||||
"@maven//:com_github_scopt_scopt",
|
||||
"@maven//:org_apache_pekko_pekko_actor",
|
||||
"@maven//:org_apache_pekko_pekko_stream",
|
||||
"@maven//:org_tpolecat_doobie_core",
|
||||
"@maven//:org_tpolecat_doobie_hikari",
|
||||
"@maven//:org_typelevel_cats_effect",
|
||||
],
|
||||
tags = ["ee-jar-license"],
|
||||
runtime_deps = [
|
||||
"@maven//:ch_qos_logback_logback_classic",
|
||||
"@maven//:org_postgresql_postgresql",
|
||||
],
|
||||
deps = [
|
||||
"//canton:ledger_api_proto_scala",
|
||||
"//libs-scala/doobie-slf4j",
|
||||
"//libs-scala/resources",
|
||||
"//libs-scala/resources-pekko",
|
||||
"//runtime-components/non-repudiation",
|
||||
"//runtime-components/non-repudiation-api",
|
||||
"//runtime-components/non-repudiation-postgresql",
|
||||
"@maven//:io_grpc_grpc_api",
|
||||
"@maven//:io_grpc_grpc_netty",
|
||||
"@maven//:org_slf4j_slf4j_api",
|
||||
],
|
||||
)
|
||||
|
||||
da_scala_test(
|
||||
name = "test",
|
||||
srcs = glob(["src/test/scala/**/*.scala"]),
|
||||
scala_deps = [
|
||||
"@maven//:com_github_scopt_scopt",
|
||||
],
|
||||
deps = [
|
||||
":non-repudiation-app",
|
||||
],
|
||||
)
|
@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.app
|
||||
|
||||
import java.net.{InetAddress, InetSocketAddress}
|
||||
|
||||
import scala.concurrent.duration.{DurationInt, FiniteDuration}
|
||||
|
||||
object Configuration {
|
||||
|
||||
private val LocalHost = InetAddress.getLocalHost.getHostAddress
|
||||
private val LoopbackAddress = InetAddress.getLoopbackAddress.getHostAddress
|
||||
|
||||
private val ApiDefaultPort: Int = 7882 // Non-repudiation -> NR -> N = 78 and R = 82 in ASCII
|
||||
private val ParticipantDefaultPort: Int = 6865
|
||||
private val ProxyDefaultPort: Int = ParticipantDefaultPort
|
||||
|
||||
val Default: Configuration =
|
||||
Configuration(
|
||||
participantAddress = new InetSocketAddress(LocalHost, ParticipantDefaultPort),
|
||||
proxyAddress = new InetSocketAddress(LoopbackAddress, ProxyDefaultPort),
|
||||
apiAddress = new InetSocketAddress(LoopbackAddress, ApiDefaultPort),
|
||||
apiShutdownTimeout = 10.seconds,
|
||||
databaseJdbcUrl = "jdbc:postgresql:/",
|
||||
databaseJdbcUsername = "nonrepudiation",
|
||||
databaseJdbcPassword = "nonrepudiation",
|
||||
databaseMaxPoolSize = 10,
|
||||
metricsReportingPeriod = 5.seconds,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
final case class Configuration private (
|
||||
participantAddress: InetSocketAddress,
|
||||
proxyAddress: InetSocketAddress,
|
||||
apiAddress: InetSocketAddress,
|
||||
apiShutdownTimeout: FiniteDuration,
|
||||
databaseJdbcUrl: String,
|
||||
databaseJdbcUsername: String,
|
||||
databaseJdbcPassword: String,
|
||||
databaseMaxPoolSize: Int,
|
||||
metricsReportingPeriod: FiniteDuration,
|
||||
)
|
@ -1,90 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.app
|
||||
|
||||
import java.time.Clock
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem
|
||||
import com.daml.doobie.logging.Slf4jLogHandler
|
||||
import com.daml.ledger.api.v1.command_service.CommandServiceGrpc.CommandService
|
||||
import com.daml.ledger.api.v1.command_submission_service.CommandSubmissionServiceGrpc.CommandSubmissionService
|
||||
import com.daml.nonrepudiation.api.NonRepudiationApi
|
||||
import com.daml.nonrepudiation.postgresql.{Tables, createTransactor}
|
||||
import com.daml.nonrepudiation.{Metrics, NonRepudiationProxy}
|
||||
import com.daml.resources.pekko.PekkoResourceOwnerFactories
|
||||
import com.daml.resources.{
|
||||
AbstractResourceOwner,
|
||||
HasExecutionContext,
|
||||
ProgramResource,
|
||||
ResourceOwnerFactories,
|
||||
}
|
||||
import io.grpc.Server
|
||||
import io.grpc.netty.{NettyChannelBuilder, NettyServerBuilder}
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
object NonRepudiationApp {
|
||||
|
||||
private[app] val Name = "non-repudiation-app"
|
||||
|
||||
private val resourceFactory = new ResourceOwnerFactories[ExecutionContext]
|
||||
with PekkoResourceOwnerFactories[ExecutionContext] {
|
||||
override protected implicit val hasExecutionContext: HasExecutionContext[ExecutionContext] =
|
||||
HasExecutionContext.`ExecutionContext has itself`
|
||||
}
|
||||
|
||||
def main(args: Array[String]): Unit = {
|
||||
|
||||
val configuration: Configuration =
|
||||
OptionParser.parse(args, Configuration.Default).getOrElse(sys.exit(1))
|
||||
|
||||
val program = new ProgramResource(appOwner(configuration))
|
||||
|
||||
program.run(identity)
|
||||
|
||||
}
|
||||
|
||||
def appOwner(
|
||||
configuration: Configuration
|
||||
): AbstractResourceOwner[ExecutionContext, Server] = {
|
||||
|
||||
val participantChannel =
|
||||
NettyChannelBuilder.forAddress(configuration.participantAddress).usePlaintext().build()
|
||||
|
||||
val proxyChannelBuilder =
|
||||
NettyServerBuilder.forAddress(configuration.proxyAddress)
|
||||
|
||||
for {
|
||||
actorSystem <- resourceFactory.forActorSystem(() => ActorSystem(Name))
|
||||
transactor <- createTransactor(
|
||||
configuration.databaseJdbcUrl,
|
||||
configuration.databaseJdbcUsername,
|
||||
configuration.databaseJdbcPassword,
|
||||
configuration.databaseMaxPoolSize,
|
||||
resourceFactory,
|
||||
)
|
||||
logHandler = Slf4jLogHandler(getClass)
|
||||
db = Tables.initialize(transactor)(logHandler)
|
||||
_ <- NonRepudiationApi.owner(
|
||||
configuration.apiAddress,
|
||||
configuration.apiShutdownTimeout,
|
||||
db.certificates,
|
||||
db.signedPayloads,
|
||||
actorSystem,
|
||||
)
|
||||
metrics <- Metrics.owner(configuration.metricsReportingPeriod)
|
||||
proxy <- NonRepudiationProxy.owner(
|
||||
participantChannel,
|
||||
proxyChannelBuilder,
|
||||
db.certificates,
|
||||
db.signedPayloads,
|
||||
Clock.systemUTC(),
|
||||
metrics,
|
||||
CommandService.scalaDescriptor.fullName,
|
||||
CommandSubmissionService.scalaDescriptor.fullName,
|
||||
)
|
||||
} yield proxy
|
||||
}
|
||||
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.app
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
private[app] object OptionParser extends scopt.OptionParser[Configuration](NonRepudiationApp.Name) {
|
||||
|
||||
head(NonRepudiationApp.Name)
|
||||
|
||||
opt[String]("ledger-host")
|
||||
.required()
|
||||
.action(setLedgerHost)
|
||||
.text("The host address of the participant. Required.")
|
||||
|
||||
opt[String]("ledger-port")
|
||||
.required()
|
||||
.action(setLedgerPort)
|
||||
.text("The port of the participant. Required.")
|
||||
|
||||
opt[Map[String, String]]("jdbc")
|
||||
.required()
|
||||
.action(setJdbcConfiguration)
|
||||
.text(
|
||||
"""Contains comma-separated key-value pairs. Where:
|
||||
| url -- JDBC connection URL, beginning with jdbc:postgresql,
|
||||
| user -- user name for database user with permissions to create tables,
|
||||
| password -- password of database user,
|
||||
|Example: "url=jdbc:postgresql://localhost:5432/nonrepudiation,user=nonrepudiation,password=secret"
|
||||
|""".stripMargin
|
||||
)
|
||||
|
||||
opt[String]("proxy-host")
|
||||
.action(setProxyHost)
|
||||
.text(
|
||||
s"The interface over which the proxy will be exposed. Defaults to ${Configuration.Default.proxyAddress.getAddress}."
|
||||
)
|
||||
|
||||
opt[String]("proxy-port")
|
||||
.action(setProxyPort)
|
||||
.text(
|
||||
s"The port over which the proxy will be exposed. Defaults to ${Configuration.Default.proxyAddress.getPort}."
|
||||
)
|
||||
|
||||
opt[String]("api-host")
|
||||
.action(setApiHost)
|
||||
.text(
|
||||
s"The interface over which the non-repudiation API will be exposed. Defaults to ${Configuration.Default.apiAddress.getAddress}."
|
||||
)
|
||||
|
||||
opt[String]("api-port")
|
||||
.action(setApiPort)
|
||||
.text(
|
||||
s"The port over which the non-repudiation API will be exposed. Defaults to ${Configuration.Default.apiAddress.getPort}."
|
||||
)
|
||||
|
||||
opt[Int]("database-max-pool-size")
|
||||
.action((size, configuration) => configuration.copy(databaseMaxPoolSize = size))
|
||||
.text(
|
||||
s"The maximum number of pooled connections to the non-repudiation database. Defaults to ${Configuration.Default.databaseMaxPoolSize}."
|
||||
)
|
||||
|
||||
// TODO Add metrics reporting configuration once there is an alternative to SLF4J
|
||||
|
||||
private def setLedgerHost(host: String, configuration: Configuration): Configuration =
|
||||
configuration.copy(participantAddress = setHost(host, configuration.participantAddress))
|
||||
|
||||
private def setLedgerPort(port: String, configuration: Configuration): Configuration =
|
||||
configuration.copy(participantAddress = setPort(port, configuration.participantAddress))
|
||||
|
||||
private def setProxyHost(host: String, configuration: Configuration): Configuration =
|
||||
configuration.copy(proxyAddress = setHost(host, configuration.proxyAddress))
|
||||
|
||||
private def setProxyPort(port: String, configuration: Configuration): Configuration =
|
||||
configuration.copy(proxyAddress = setPort(port, configuration.proxyAddress))
|
||||
|
||||
private def setApiHost(host: String, configuration: Configuration): Configuration =
|
||||
configuration.copy(apiAddress = setHost(host, configuration.apiAddress))
|
||||
|
||||
private def setApiPort(port: String, configuration: Configuration): Configuration =
|
||||
configuration.copy(apiAddress = setPort(port, configuration.apiAddress))
|
||||
|
||||
private def setHost(host: String, address: InetSocketAddress): InetSocketAddress =
|
||||
new InetSocketAddress(host, address.getPort)
|
||||
|
||||
private def setPort(port: String, address: InetSocketAddress): InetSocketAddress =
|
||||
new InetSocketAddress(address.getAddress, port.toInt)
|
||||
|
||||
private def setJdbcConfiguration(
|
||||
jdbc: Map[String, String],
|
||||
configuration: Configuration,
|
||||
): Configuration =
|
||||
configuration.copy(
|
||||
databaseJdbcUrl = jdbc("url"),
|
||||
databaseJdbcUsername = jdbc("user"),
|
||||
databaseJdbcPassword = jdbc("password"),
|
||||
)
|
||||
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.app
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import org.scalatest.OptionValues
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
final class OptionParserSpec extends AnyFlatSpec with Matchers with OptionValues {
|
||||
|
||||
behavior of "OptionParser"
|
||||
|
||||
it should "parse the expected options" in {
|
||||
|
||||
val options = Array(
|
||||
"--ledger-host",
|
||||
"168.10.67.4",
|
||||
"--ledger-port",
|
||||
"7890",
|
||||
"--proxy-host",
|
||||
"168.10.67.5",
|
||||
"--proxy-port",
|
||||
"7891",
|
||||
"--api-host",
|
||||
"168.10.67.6",
|
||||
"--api-port",
|
||||
"7892",
|
||||
"--jdbc",
|
||||
"url=jdbc:postgresql:nr,user=psql,password=secret",
|
||||
"--database-max-pool-size",
|
||||
"30",
|
||||
)
|
||||
|
||||
val expectedConfiguration =
|
||||
Configuration(
|
||||
participantAddress = new InetSocketAddress("168.10.67.4", 7890),
|
||||
proxyAddress = new InetSocketAddress("168.10.67.5", 7891),
|
||||
apiAddress = new InetSocketAddress("168.10.67.6", 7892),
|
||||
apiShutdownTimeout = Configuration.Default.apiShutdownTimeout,
|
||||
databaseJdbcUrl = "jdbc:postgresql:nr",
|
||||
databaseJdbcUsername = "psql",
|
||||
databaseJdbcPassword = "secret",
|
||||
databaseMaxPoolSize = 30,
|
||||
metricsReportingPeriod = Configuration.Default.metricsReportingPeriod,
|
||||
)
|
||||
|
||||
OptionParser.parse(options, Configuration.Default).value shouldBe expectedConfiguration
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
load("//bazel_tools:java.bzl", "da_java_library")
|
||||
load("//bazel_tools:scala.bzl", "da_scala_test")
|
||||
|
||||
da_java_library(
|
||||
name = "non-repudiation-client",
|
||||
srcs = glob(["src/main/java/**/*.java"]),
|
||||
tags = [
|
||||
"javadoc_root_packages=com.daml.nonrepudiation.client",
|
||||
"maven_coordinates=com.daml:non-repudiation-client:__VERSION__",
|
||||
],
|
||||
visibility = [
|
||||
"//:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//canton:bindings-java",
|
||||
"//runtime-components/non-repudiation-core",
|
||||
"@maven//:com_google_guava_guava",
|
||||
"@maven//:io_grpc_grpc_api",
|
||||
"@maven//:io_grpc_grpc_inprocess",
|
||||
"@maven//:io_grpc_grpc_stub",
|
||||
],
|
||||
)
|
||||
|
||||
da_scala_test(
|
||||
name = "test",
|
||||
srcs = glob(["src/test/scala/**/*.scala"]),
|
||||
resources = [
|
||||
"src/test/resources/logback-test.xml",
|
||||
],
|
||||
runtime_deps = [
|
||||
"@maven//:ch_qos_logback_logback_classic",
|
||||
],
|
||||
deps = [
|
||||
"//canton:bindings-java",
|
||||
"//canton:ledger_api_proto_scala",
|
||||
"//language-support/java/bindings-rxjava",
|
||||
"//libs-scala/resources",
|
||||
"//libs-scala/resources-grpc",
|
||||
"//observability/metrics",
|
||||
"//runtime-components/non-repudiation",
|
||||
"//runtime-components/non-repudiation-client",
|
||||
"//runtime-components/non-repudiation-testing",
|
||||
"@maven//:io_grpc_grpc_api",
|
||||
"@maven//:io_grpc_grpc_netty",
|
||||
"@maven//:io_reactivex_rxjava2_rxjava",
|
||||
],
|
||||
)
|
@ -1,31 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.client;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
import io.grpc.MethodDescriptor;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
final class ByteMarshaller implements MethodDescriptor.Marshaller<byte[]> {
|
||||
|
||||
private ByteMarshaller() {}
|
||||
|
||||
public static final ByteMarshaller INSTANCE = new ByteMarshaller();
|
||||
|
||||
@Override
|
||||
public byte[] parse(InputStream value) {
|
||||
try {
|
||||
return ByteStreams.toByteArray(value);
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream stream(byte[] value) {
|
||||
return new ByteArrayInputStream(value);
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.client;
|
||||
|
||||
import com.daml.ledger.api.v1.CommandServiceGrpc;
|
||||
import com.daml.ledger.api.v1.CommandSubmissionServiceGrpc;
|
||||
import com.daml.nonrepudiation.Fingerprints;
|
||||
import com.daml.nonrepudiation.Headers;
|
||||
import com.daml.nonrepudiation.Signatures;
|
||||
import io.grpc.*;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* A gRPC client-side interceptor that uses a key pair to sign a payload and adds it as metadata to
|
||||
* the call, alongside a fingerprint of the public key and the algorithm used to sign.
|
||||
*/
|
||||
public final class SigningInterceptor implements ClientInterceptor {
|
||||
|
||||
private final PrivateKey key;
|
||||
private final byte[] fingerprint;
|
||||
private final String algorithm;
|
||||
private final Predicate<MethodDescriptor<?, ?>> signingPredicate;
|
||||
|
||||
private static final Set<String> commandIssuingServices =
|
||||
Collections.unmodifiableSet(
|
||||
new HashSet<>(
|
||||
Arrays.asList(
|
||||
CommandServiceGrpc.SERVICE_NAME, CommandSubmissionServiceGrpc.SERVICE_NAME)));
|
||||
|
||||
public static SigningInterceptor signCommands(PrivateKey key, X509Certificate certificate) {
|
||||
return new SigningInterceptor(
|
||||
key, certificate, method -> commandIssuingServices.contains(method.getServiceName()));
|
||||
}
|
||||
|
||||
// This is package private as it's not intended for general use and exists for testing
|
||||
// exclusively.
|
||||
SigningInterceptor(
|
||||
PrivateKey key,
|
||||
X509Certificate certificate,
|
||||
Predicate<MethodDescriptor<?, ?>> signingPredicate) {
|
||||
super();
|
||||
this.key = key;
|
||||
this.algorithm = certificate.getSigAlgName();
|
||||
this.fingerprint = Fingerprints.compute(certificate);
|
||||
this.signingPredicate = signingPredicate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
|
||||
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
|
||||
ClientCall<ReqT, RespT> call = next.newCall(method, callOptions);
|
||||
if (signingPredicate.test(method)) {
|
||||
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(call) {
|
||||
private Listener<RespT> responseListener = null;
|
||||
private Metadata headers = null;
|
||||
private int requested = 0;
|
||||
|
||||
@Override
|
||||
public void start(Listener<RespT> responseListener, Metadata headers) {
|
||||
// Delay start until we have the message body since
|
||||
// the signature in the Metadata depends on the body.
|
||||
this.responseListener = responseListener;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void request(int numMessages) {
|
||||
// Delay until we have the message body since the
|
||||
// signature in the Metadata depends on the body.
|
||||
requested += numMessages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(ReqT request) {
|
||||
byte[] requestBytes =
|
||||
ByteMarshaller.INSTANCE.parse(method.getRequestMarshaller().stream(request));
|
||||
byte[] signature = Signatures.sign(algorithm, key, requestBytes);
|
||||
headers.put(Headers.SIGNATURE, signature);
|
||||
headers.put(Headers.ALGORITHM, algorithm);
|
||||
headers.put(Headers.FINGERPRINT, fingerprint);
|
||||
delegate().start(responseListener, headers);
|
||||
delegate().request(requested);
|
||||
delegate().sendMessage(request);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(call) {};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="stderr-appender" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<target>System.err</target>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>trace</level>
|
||||
</filter>
|
||||
<encoder>
|
||||
<pattern>%date{"yyyy-MM-dd'T'HH:mm:ss.SSSXXX", UTC} %-5level %logger{5}@[%-4.30thread] - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="${LOGLEVEL:-WARN}">
|
||||
<appender-ref ref="stderr-appender"/>
|
||||
</root>
|
||||
|
||||
</configuration>
|
@ -1,116 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.client
|
||||
|
||||
import java.time.{Clock, Instant, ZoneId}
|
||||
|
||||
import com.daml.ledger.api.v1.CommandServiceOuterClass.SubmitAndWaitRequest
|
||||
import com.daml.ledger.javaapi.data.{Command, CommandsSubmission}
|
||||
import com.daml.ledger.api.v1.CommandsOuterClass.{Command => ProtoCommand}
|
||||
import com.daml.ledger.api.v1.command_service.CommandServiceGrpc.CommandService
|
||||
import com.daml.ledger.api.v1.command_submission_service.CommandSubmissionServiceGrpc.CommandSubmissionService
|
||||
import com.daml.ledger.rxjava.DamlLedgerClient
|
||||
import com.daml.nonrepudiation.testing._
|
||||
import com.daml.nonrepudiation.{
|
||||
AlgorithmString,
|
||||
CommandIdString,
|
||||
Metrics,
|
||||
NonRepudiationProxy,
|
||||
SignatureBytes,
|
||||
SignedPayload,
|
||||
}
|
||||
import com.daml.resources.grpc.{GrpcResourceOwnerFactories => Resources}
|
||||
import io.grpc.netty.NettyChannelBuilder
|
||||
import org.scalatest.Inside
|
||||
import org.scalatest.flatspec.AsyncFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import java.util.Collections.singletonList
|
||||
|
||||
import com.daml.metrics.api.noop.NoOpMetricsFactory
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.concurrent.duration.DurationInt
|
||||
|
||||
final class SigningInterceptorSpec extends AsyncFlatSpec with Matchers with Inside {
|
||||
|
||||
behavior of "SigningInterceptor"
|
||||
|
||||
it should "be possible to use it with the Java bindings" in {
|
||||
val (key, certificate) = generateKeyAndCertificate()
|
||||
val builders =
|
||||
DummyTestSetup.Builders(
|
||||
useNetworkStack = true,
|
||||
serviceExecutionContext = ExecutionContext.global,
|
||||
)
|
||||
|
||||
val certificates = new Certificates
|
||||
val signedPayloads = new SignedPayloads[CommandIdString]
|
||||
|
||||
val expectedAlgorithm = AlgorithmString.SHA256withRSA
|
||||
val expectedFingerprint = certificates.put(certificate)
|
||||
val expectedTimestamp = Instant.now()
|
||||
|
||||
val proxy =
|
||||
for {
|
||||
_ <- Resources.forServer(builders.participantServer, 5.seconds)
|
||||
participant <- Resources.forChannel(builders.participantChannel, 5.seconds)
|
||||
proxy <- NonRepudiationProxy.owner(
|
||||
participant,
|
||||
builders.proxyServer,
|
||||
certificates,
|
||||
signedPayloads,
|
||||
Clock.fixed(expectedTimestamp, ZoneId.systemDefault()),
|
||||
new Metrics(NoOpMetricsFactory),
|
||||
CommandService.scalaDescriptor.fullName,
|
||||
CommandSubmissionService.scalaDescriptor.fullName,
|
||||
)
|
||||
} yield proxy
|
||||
|
||||
proxy.use { _ =>
|
||||
val clientChannelBuilder =
|
||||
builders.proxyChannel
|
||||
.asInstanceOf[NettyChannelBuilder]
|
||||
.intercept(SigningInterceptor.signCommands(key, certificate))
|
||||
val client = DamlLedgerClient.newBuilder(clientChannelBuilder).build()
|
||||
client.connect()
|
||||
val command = ProtoCommand.parseFrom(generateCommand().getCommands.commands.head.toByteArray)
|
||||
val expectedWorkflowId = "workflow-id"
|
||||
val expectedApplicationId = "application-id"
|
||||
val expectedCommandId = "command-id"
|
||||
val expectedParty = "party-1"
|
||||
|
||||
val params = CommandsSubmission
|
||||
.create(
|
||||
expectedApplicationId,
|
||||
expectedCommandId,
|
||||
singletonList(Command.fromProtoCommand(command)),
|
||||
)
|
||||
.withWorkflowId(expectedWorkflowId)
|
||||
.withActAs(expectedParty)
|
||||
|
||||
client.getCommandClient
|
||||
.submitAndWait(params)
|
||||
.blockingGet()
|
||||
|
||||
withClue("Only the command should be signed, not the call to the ledger identity service") {
|
||||
signedPayloads.size shouldBe 1
|
||||
}
|
||||
|
||||
inside(signedPayloads.get(CommandIdString.wrap("command-id"))) {
|
||||
case Seq(SignedPayload(algorithm, fingerprint, payload, signature, timestamp)) =>
|
||||
val commands = SubmitAndWaitRequest.parseFrom(payload.unsafeArray).getCommands
|
||||
algorithm shouldBe expectedAlgorithm
|
||||
fingerprint shouldBe expectedFingerprint
|
||||
commands.getWorkflowId shouldBe expectedWorkflowId
|
||||
commands.getApplicationId shouldBe expectedApplicationId
|
||||
commands.getCommandId shouldBe expectedCommandId
|
||||
commands.getParty shouldBe expectedParty
|
||||
commands.getCommandsCount shouldBe 1
|
||||
signature shouldBe SignatureBytes.sign(expectedAlgorithm, key, payload)
|
||||
timestamp shouldBe expectedTimestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
load("//bazel_tools:java.bzl", "da_java_library")
|
||||
|
||||
da_java_library(
|
||||
name = "non-repudiation-core",
|
||||
srcs = glob(["src/main/java/**/*.java"]),
|
||||
tags = [
|
||||
"javadoc_root_packages=com.daml.nonrepudiation",
|
||||
"maven_coordinates=com.daml:non-repudiation-core:__VERSION__",
|
||||
],
|
||||
visibility = [
|
||||
"//:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"@maven//:io_grpc_grpc_api",
|
||||
],
|
||||
)
|
@ -1,29 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
public final class Fingerprints {
|
||||
|
||||
private Fingerprints() {}
|
||||
|
||||
private static final String ALGORITHM = "SHA-256";
|
||||
|
||||
public static byte[] compute(X509Certificate certificate) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance(ALGORITHM);
|
||||
byte[] encodedCertificate = certificate.getEncoded();
|
||||
return md.digest(encodedCertificate);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Provider for algorithm '%s' not found", ALGORITHM), e);
|
||||
} catch (CertificateEncodingException e) {
|
||||
throw new IllegalArgumentException("Unable to encode the certificate", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation;
|
||||
|
||||
import io.grpc.Metadata;
|
||||
|
||||
public final class Headers {
|
||||
|
||||
private Headers() {}
|
||||
|
||||
public static final Metadata.Key<byte[]> SIGNATURE =
|
||||
Metadata.Key.of("signature-bin", Metadata.BINARY_BYTE_MARSHALLER);
|
||||
|
||||
public static final Metadata.Key<String> ALGORITHM =
|
||||
Metadata.Key.of("algorithm", Metadata.ASCII_STRING_MARSHALLER);
|
||||
|
||||
public static final Metadata.Key<byte[]> FINGERPRINT =
|
||||
Metadata.Key.of("fingerprint-bin", Metadata.BINARY_BYTE_MARSHALLER);
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation;
|
||||
|
||||
import java.security.*;
|
||||
|
||||
public final class Signatures {
|
||||
|
||||
private Signatures() {}
|
||||
|
||||
public static byte[] sign(String algorithm, PrivateKey key, byte[] payload) {
|
||||
try {
|
||||
Signature signature = Signature.getInstance(algorithm);
|
||||
signature.initSign(key);
|
||||
signature.update(payload);
|
||||
return signature.sign();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Provider for algorithm '%s' not found", algorithm), e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("The signing key is invalid", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new IllegalArgumentException("The payload could not be signed", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
load(
|
||||
"//bazel_tools:scala.bzl",
|
||||
"da_scala_benchmark_jmh",
|
||||
)
|
||||
|
||||
da_scala_benchmark_jmh(
|
||||
name = "non-repudiation-perf",
|
||||
srcs = glob(["src/main/scala/**/*.scala"]),
|
||||
max_heap_size = "4g",
|
||||
resources = ["src/main/resources/logback.xml"],
|
||||
scala_deps = [
|
||||
"@maven//:com_chuusai_shapeless",
|
||||
"@maven//:org_tpolecat_doobie_core",
|
||||
"@maven//:org_tpolecat_doobie_free",
|
||||
"@maven//:org_tpolecat_doobie_hikari",
|
||||
"@maven//:org_typelevel_cats_core",
|
||||
"@maven//:org_typelevel_cats_effect",
|
||||
"@maven//:org_typelevel_cats_free",
|
||||
"@maven//:org_typelevel_cats_kernel",
|
||||
],
|
||||
deps = [
|
||||
"//canton:ledger_api_proto_scala",
|
||||
"//libs-scala/doobie-slf4j",
|
||||
"//libs-scala/ports",
|
||||
"//libs-scala/postgresql-testing",
|
||||
"//libs-scala/resources",
|
||||
"//libs-scala/resources-grpc",
|
||||
"//observability/metrics",
|
||||
"//runtime-components/non-repudiation",
|
||||
"//runtime-components/non-repudiation-client",
|
||||
"//runtime-components/non-repudiation-postgresql",
|
||||
"//runtime-components/non-repudiation-resources",
|
||||
"//runtime-components/non-repudiation-testing",
|
||||
"@maven//:ch_qos_logback_logback_classic",
|
||||
"@maven//:io_grpc_grpc_netty",
|
||||
"@maven//:io_grpc_grpc_services",
|
||||
"@maven//:org_postgresql_postgresql",
|
||||
"@maven//:org_slf4j_slf4j_api",
|
||||
],
|
||||
)
|
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<!-- Not logging anything on purpose -->
|
||||
<!-- If there is an error, other tests with more precise feedback will log it -->
|
||||
<appender name="nop" class="ch.qos.logback.core.helpers.NOPAppender" />
|
||||
<root>
|
||||
<appender-ref ref="nop"/>
|
||||
</root>
|
||||
</configuration>
|
@ -1,84 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.perf
|
||||
|
||||
import com.daml.doobie.logging.Slf4jLogHandler
|
||||
import com.daml.ledger.api.v1.command_submission_service.CommandSubmissionServiceGrpc.CommandSubmissionServiceBlockingStub
|
||||
import com.daml.nonrepudiation.postgresql.{Tables, createTransactor}
|
||||
import com.daml.nonrepudiation.testing._
|
||||
import com.daml.resources.Resource
|
||||
import com.daml.resources.grpc.{GrpcResourceOwnerFactories => Resources}
|
||||
import com.daml.testing.postgresql.{PostgresAround, PostgresDatabase}
|
||||
import doobie.util.log.LogHandler
|
||||
import org.openjdk.jmh.annotations._
|
||||
|
||||
import scala.concurrent.duration.DurationInt
|
||||
import scala.concurrent.{Await, ExecutionContext}
|
||||
|
||||
@State(Scope.Benchmark)
|
||||
class NonRepudiationProxyBenchmark extends PostgresAround {
|
||||
|
||||
@Param(Array("100000"))
|
||||
var commandPayloadSize: Int = _
|
||||
|
||||
@Param(Array("false"))
|
||||
var useNetworkStack: Boolean = _
|
||||
|
||||
private var stubResource: Resource[ExecutionContext, CommandSubmissionServiceBlockingStub] = _
|
||||
private var stub: CommandSubmissionServiceBlockingStub = _
|
||||
private var database: PostgresDatabase = _
|
||||
private var payload: String = _
|
||||
|
||||
@Benchmark
|
||||
def run(): Unit = {
|
||||
// Generating commands adds very little noise to substantial benchmarks
|
||||
val command = generateCommand(payload = payload)
|
||||
val _ = stub.submit(command)
|
||||
}
|
||||
|
||||
@Setup
|
||||
def setup(): Unit = {
|
||||
// The global fork-join work-stealing pool should be good enough to be used for both
|
||||
// handling resource callbacks and mock command submission calls
|
||||
implicit val executionContext: ExecutionContext = ExecutionContext.global
|
||||
implicit val logHandler: LogHandler = Slf4jLogHandler(classOf[NonRepudiationProxyBenchmark])
|
||||
connectToPostgresqlServer()
|
||||
database = createNewRandomDatabase()
|
||||
|
||||
val (key, certificate) = generateKeyAndCertificate()
|
||||
|
||||
val stubOwner =
|
||||
for {
|
||||
transactor <- createTransactor(
|
||||
database.urlWithoutCredentials,
|
||||
database.userName,
|
||||
database.password,
|
||||
maxPoolSize = 10,
|
||||
factory = Resources,
|
||||
)
|
||||
db = Tables.initialize(transactor)
|
||||
_ = db.certificates.put(certificate)
|
||||
stub <- StubOwner(
|
||||
useNetworkStack = useNetworkStack,
|
||||
key = key,
|
||||
certificate = certificate,
|
||||
certificates = db.certificates,
|
||||
signedPayloads = db.signedPayloads,
|
||||
serviceExecutionContext = executionContext,
|
||||
)
|
||||
} yield stub
|
||||
|
||||
stubResource = stubOwner.acquire()
|
||||
stub = Await.result(stubResource.asFuture, atMost = 10.seconds)
|
||||
payload = "e" * commandPayloadSize
|
||||
}
|
||||
|
||||
@TearDown
|
||||
def tearDown(): Unit = {
|
||||
Await.ready(stubResource.release(), atMost = 10.seconds)
|
||||
dropDatabase(database)
|
||||
disconnectFromPostgresqlServer()
|
||||
}
|
||||
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.perf
|
||||
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Clock
|
||||
|
||||
import com.daml.ledger.api.v1.command_submission_service.CommandSubmissionServiceGrpc
|
||||
import com.daml.ledger.api.v1.command_submission_service.CommandSubmissionServiceGrpc.CommandSubmissionServiceBlockingStub
|
||||
import com.daml.metrics.api.noop.NoOpMetricsFactory
|
||||
import com.daml.nonrepudiation.client.SigningInterceptor
|
||||
import com.daml.nonrepudiation.testing.DummyTestSetup
|
||||
import com.daml.nonrepudiation.{
|
||||
CertificateRepository,
|
||||
CommandIdString,
|
||||
Metrics,
|
||||
NonRepudiationProxy,
|
||||
SignedPayloadRepository,
|
||||
}
|
||||
import com.daml.resources.grpc.{GrpcResourceOwnerFactories => Resources}
|
||||
import com.daml.resources.{AbstractResourceOwner, Resource}
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.concurrent.duration.DurationInt
|
||||
|
||||
final class StubOwner private (
|
||||
key: PrivateKey,
|
||||
certificate: X509Certificate,
|
||||
certificates: CertificateRepository,
|
||||
signedPayloads: SignedPayloadRepository[CommandIdString],
|
||||
builders: DummyTestSetup.Builders,
|
||||
) extends AbstractResourceOwner[ExecutionContext, CommandSubmissionServiceBlockingStub] {
|
||||
|
||||
override def acquire()(implicit
|
||||
context: ExecutionContext
|
||||
): Resource[ExecutionContext, CommandSubmissionServiceBlockingStub] = {
|
||||
|
||||
val stubOwner =
|
||||
for {
|
||||
_ <- Resources.forServer(builders.participantServer, 5.seconds)
|
||||
participant <- Resources.forChannel(builders.participantChannel, 5.seconds)
|
||||
_ <- NonRepudiationProxy.owner(
|
||||
participant = participant,
|
||||
serverBuilder = builders.proxyServer,
|
||||
certificateRepository = certificates,
|
||||
signedPayloadRepository = signedPayloads,
|
||||
timestampProvider = Clock.systemUTC(),
|
||||
metrics = new Metrics(NoOpMetricsFactory),
|
||||
serviceName = CommandSubmissionServiceGrpc.SERVICE.getName,
|
||||
)
|
||||
} yield CommandSubmissionServiceGrpc
|
||||
.blockingStub(builders.proxyChannel.build())
|
||||
.withInterceptors(SigningInterceptor.signCommands(key, certificate))
|
||||
|
||||
stubOwner.acquire()(context)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object StubOwner {
|
||||
|
||||
def apply(
|
||||
useNetworkStack: Boolean,
|
||||
key: PrivateKey,
|
||||
certificate: X509Certificate,
|
||||
certificates: CertificateRepository,
|
||||
signedPayloads: SignedPayloadRepository[CommandIdString],
|
||||
serviceExecutionContext: ExecutionContext,
|
||||
): AbstractResourceOwner[ExecutionContext, CommandSubmissionServiceBlockingStub] =
|
||||
new StubOwner(
|
||||
key,
|
||||
certificate,
|
||||
certificates,
|
||||
signedPayloads,
|
||||
DummyTestSetup.Builders(useNetworkStack, serviceExecutionContext),
|
||||
)
|
||||
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
load(
|
||||
"//bazel_tools:scala.bzl",
|
||||
"da_scala_library",
|
||||
"da_scala_test",
|
||||
)
|
||||
|
||||
da_scala_library(
|
||||
name = "non-repudiation-postgresql",
|
||||
srcs = glob(["src/main/scala/**/*.scala"]),
|
||||
resource_strip_prefix = "runtime-components/non-repudiation-postgresql/src/main/resources/",
|
||||
resources = glob(["src/main/resources/com/daml/nonrepudiation/postgresql/*"]),
|
||||
scala_deps = [
|
||||
"@maven//:com_chuusai_shapeless",
|
||||
"@maven//:org_tpolecat_doobie_core",
|
||||
"@maven//:org_tpolecat_doobie_free",
|
||||
"@maven//:org_tpolecat_doobie_hikari",
|
||||
"@maven//:org_typelevel_cats_core",
|
||||
"@maven//:org_typelevel_cats_effect",
|
||||
"@maven//:org_typelevel_cats_free",
|
||||
"@maven//:org_typelevel_cats_kernel",
|
||||
],
|
||||
visibility = [
|
||||
"//:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//libs-scala/resources",
|
||||
"//runtime-components/non-repudiation",
|
||||
"//runtime-components/non-repudiation-resources",
|
||||
"@maven//:com_zaxxer_HikariCP",
|
||||
"@maven//:org_flywaydb_flyway_core",
|
||||
],
|
||||
)
|
||||
|
||||
da_scala_test(
|
||||
name = "test",
|
||||
srcs = glob(["src/test/scala/**/*.scala"]),
|
||||
resources = [
|
||||
"src/test/resources/logback-test.xml",
|
||||
],
|
||||
scala_deps = [
|
||||
"@maven//:org_tpolecat_doobie_core",
|
||||
"@maven//:org_tpolecat_doobie_hikari",
|
||||
"@maven//:org_typelevel_cats_effect",
|
||||
],
|
||||
runtime_deps = [
|
||||
"@maven//:ch_qos_logback_logback_classic",
|
||||
"@maven//:org_postgresql_postgresql",
|
||||
],
|
||||
deps = [
|
||||
":non-repudiation-postgresql",
|
||||
"//canton:ledger_api_proto_scala",
|
||||
"//libs-scala/doobie-slf4j",
|
||||
"//libs-scala/ports",
|
||||
"//libs-scala/postgresql-testing",
|
||||
"//libs-scala/resources",
|
||||
"//runtime-components/non-repudiation",
|
||||
"//runtime-components/non-repudiation-testing",
|
||||
"@maven//:org_slf4j_slf4j_api",
|
||||
],
|
||||
)
|
@ -1,19 +0,0 @@
|
||||
-- Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
-- SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
create table ${tables.prefix}_signed_payloads(
|
||||
command_id varchar(255),
|
||||
algorithm varchar(64) not null,
|
||||
fingerprint bytea not null,
|
||||
payload bytea not null,
|
||||
signature bytea not null,
|
||||
"timestamp" timestamptz not null
|
||||
);
|
||||
|
||||
-- Supports fast retrieval of signed payloads by command identifier
|
||||
create index ${tables.prefix}_signed_payloads_command_id_index on ${tables.prefix}_signed_payloads using hash(command_id);
|
||||
|
||||
create table ${tables.prefix}_certificates(
|
||||
fingerprint bytea primary key not null,
|
||||
certificate bytea not null
|
||||
);
|
@ -1,36 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.postgresql
|
||||
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
import cats.effect.IO
|
||||
import com.daml.nonrepudiation.{CertificateRepository, FingerprintBytes}
|
||||
import doobie.implicits._
|
||||
import doobie.util.fragment.Fragment
|
||||
import doobie.util.log.LogHandler
|
||||
import doobie.util.transactor.Transactor
|
||||
|
||||
final class PostgresqlCertificateRepository(transactor: Transactor[IO])(implicit
|
||||
logHandler: LogHandler
|
||||
) extends CertificateRepository {
|
||||
|
||||
private val table = Fragment.const(s"${Tables.Prefix}_certificates")
|
||||
|
||||
private def getCertificate(fingerprint: FingerprintBytes): Fragment =
|
||||
fr"select certificate from" ++ table ++ fr"where fingerprint = $fingerprint"
|
||||
|
||||
override def get(fingerprint: FingerprintBytes): Option[X509Certificate] =
|
||||
getCertificate(fingerprint).query[X509Certificate].option.transact(transactor).unsafeRunSync()
|
||||
|
||||
private def putKey(fingerprint: FingerprintBytes, certificate: X509Certificate): Fragment =
|
||||
fr"insert into" ++ table ++ fr"""(fingerprint, certificate) values ($fingerprint, $certificate) on conflict do nothing"""
|
||||
|
||||
override def put(certificate: X509Certificate): FingerprintBytes = {
|
||||
val fingerprint = FingerprintBytes.compute(certificate)
|
||||
putKey(fingerprint, certificate).update.run.transact(transactor).unsafeRunSync()
|
||||
fingerprint
|
||||
}
|
||||
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.postgresql
|
||||
|
||||
import cats.effect.IO
|
||||
import com.daml.nonrepudiation.{CommandIdString, SignedPayload, SignedPayloadRepository}
|
||||
import doobie.implicits._
|
||||
import doobie.implicits.legacy.instant._
|
||||
import doobie.util.fragment.Fragment
|
||||
import doobie.util.log.LogHandler
|
||||
import doobie.util.transactor.Transactor
|
||||
|
||||
final class PostgresqlSignedPayloadRepository(transactor: Transactor[IO])(implicit
|
||||
logHandler: LogHandler
|
||||
) extends SignedPayloadRepository[CommandIdString] {
|
||||
|
||||
private val table = Fragment.const(s"${Tables.Prefix}_signed_payloads")
|
||||
|
||||
private def getSignedPayload(key: CommandIdString): Fragment =
|
||||
fr"select algorithm, fingerprint, payload, signature, timestamp from" ++ table ++ fr"where command_id = $key"
|
||||
|
||||
override def get(key: CommandIdString): Iterable[SignedPayload] =
|
||||
getSignedPayload(key).query[SignedPayload].to[Iterable].transact(transactor).unsafeRunSync()
|
||||
|
||||
private def putSignedPayload(key: CommandIdString, signedPayload: SignedPayload): Fragment = {
|
||||
import signedPayload._
|
||||
fr"insert into" ++ table ++ fr"""(command_id, algorithm, fingerprint, payload, signature, timestamp) values($key, $algorithm, $fingerprint, $payload, $signature, $timestamp)"""
|
||||
}
|
||||
|
||||
override def put(signedPayload: SignedPayload): Unit = {
|
||||
val key = keyEncoder.encode(signedPayload.payload)
|
||||
val _ = putSignedPayload(key, signedPayload).update.run.transact(transactor).unsafeRunSync()
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.postgresql
|
||||
|
||||
import java.util.Collections
|
||||
|
||||
import cats.effect.IO
|
||||
import doobie.hikari.HikariTransactor
|
||||
import doobie.util.log.LogHandler
|
||||
import org.flywaydb.core.Flyway
|
||||
|
||||
object Tables {
|
||||
|
||||
val Prefix = "nonrepudiation"
|
||||
|
||||
def initialize(transactor: HikariTransactor[IO])(implicit logHandler: LogHandler): Tables =
|
||||
transactor
|
||||
.configure { dataSource =>
|
||||
IO {
|
||||
Flyway
|
||||
.configure()
|
||||
.dataSource(dataSource)
|
||||
.locations("classpath:com/daml/nonrepudiation/postgresql/")
|
||||
.placeholders(Collections.singletonMap("tables.prefix", Prefix))
|
||||
.table(s"${Prefix}_schema_history")
|
||||
.load()
|
||||
.migrate()
|
||||
new Tables(
|
||||
certificates = new PostgresqlCertificateRepository(transactor),
|
||||
signedPayloads = new PostgresqlSignedPayloadRepository(transactor),
|
||||
)
|
||||
}
|
||||
}
|
||||
.unsafeRunSync()
|
||||
|
||||
}
|
||||
|
||||
final class Tables private (
|
||||
val certificates: PostgresqlCertificateRepository,
|
||||
val signedPayloads: PostgresqlSignedPayloadRepository,
|
||||
)
|
@ -1,61 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.security.cert.{CertificateFactory, X509Certificate}
|
||||
|
||||
import cats.effect.{ContextShift, IO}
|
||||
import com.daml.nonrepudiation.resources.HikariTransactorResourceOwner
|
||||
import com.daml.resources.{AbstractResourceOwner, HasExecutionContext, ResourceOwnerFactories}
|
||||
import doobie.hikari.HikariTransactor
|
||||
import doobie.util.{Get, Put, Read}
|
||||
|
||||
import scala.collection.immutable.ArraySeq
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
package object postgresql {
|
||||
|
||||
def createTransactor[Context: HasExecutionContext](
|
||||
jdbcUrl: String,
|
||||
username: String,
|
||||
password: String,
|
||||
maxPoolSize: Int,
|
||||
factory: ResourceOwnerFactories[Context],
|
||||
): AbstractResourceOwner[Context, HikariTransactor[IO]] = {
|
||||
implicit val shift: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
|
||||
HikariTransactorResourceOwner(factory)(jdbcUrl, username, password, maxPoolSize)
|
||||
}
|
||||
|
||||
implicit def getBytes[Bytes <: ArraySeq.ofByte]: Get[Bytes] =
|
||||
Get[Array[Byte]].map(ArraySeq.unsafeWrapArray(_).asInstanceOf[Bytes])
|
||||
|
||||
implicit def putBytes[Bytes <: ArraySeq.ofByte]: Put[Bytes] =
|
||||
Put[Array[Byte]].contramap(_.unsafeArray)
|
||||
|
||||
implicit val getAlgorithmString: Get[AlgorithmString] =
|
||||
Get[String].map(AlgorithmString.wrap)
|
||||
|
||||
implicit val putAlgorithmString: Put[AlgorithmString] =
|
||||
Put[String].contramap(identity)
|
||||
|
||||
implicit val getCommandIdString: Get[CommandIdString] =
|
||||
Get[String].map(CommandIdString.wrap)
|
||||
|
||||
implicit val putCommandIdString: Put[CommandIdString] =
|
||||
Put[String].contramap(identity)
|
||||
|
||||
implicit val getCertificate: Get[X509Certificate] =
|
||||
Get[Array[Byte]].map { bytes =>
|
||||
val factory = CertificateFactory.getInstance("X.509")
|
||||
factory.generateCertificate(new ByteArrayInputStream(bytes)).asInstanceOf[X509Certificate]
|
||||
}
|
||||
|
||||
implicit val readCertificate: Read[X509Certificate] =
|
||||
Read.fromGet[X509Certificate]
|
||||
|
||||
implicit val putCertificate: Put[X509Certificate] =
|
||||
Put[Array[Byte]].contramap(_.getEncoded)
|
||||
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="stderr-appender" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<target>System.err</target>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>trace</level>
|
||||
</filter>
|
||||
<encoder>
|
||||
<pattern>%date{"yyyy-MM-dd'T'HH:mm:ss.SSSXXX", UTC} %-5level %logger{5}@[%-4.30thread] - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="${LOGLEVEL:-INFO}">
|
||||
<appender-ref ref="stderr-appender"/>
|
||||
</root>
|
||||
|
||||
<logger name="io.netty" level="WARN">
|
||||
<appender-ref ref="stderr-appender"/>
|
||||
</logger>
|
||||
<logger name="io.grpc.netty" level="WARN">
|
||||
<appender-ref ref="stderr-appender"/>
|
||||
</logger>
|
||||
</configuration>
|
@ -1,110 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.postgresql
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import com.daml.doobie.logging.Slf4jLogHandler
|
||||
import com.daml.nonrepudiation.testing._
|
||||
import com.daml.nonrepudiation.{
|
||||
AlgorithmString,
|
||||
FingerprintBytes,
|
||||
PayloadBytes,
|
||||
SignatureBytes,
|
||||
SignedPayload,
|
||||
}
|
||||
import com.daml.resources.{HasExecutionContext, ResourceOwnerFactories}
|
||||
import com.daml.testing.postgresql.PostgresAroundEach
|
||||
import doobie.util.log.LogHandler
|
||||
import org.scalatest.{Assertion, OptionValues}
|
||||
import org.scalatest.flatspec.AsyncFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
final class TablesSpec
|
||||
extends AsyncFlatSpec
|
||||
with Matchers
|
||||
with OptionValues
|
||||
with PostgresAroundEach {
|
||||
|
||||
behavior of "Tables"
|
||||
|
||||
implicit val logHandler: LogHandler = Slf4jLogHandler(getClass)
|
||||
|
||||
it should "correctly read and write certificates" in withDatabase { db =>
|
||||
val (_, expectedCertificate) = generateKeyAndCertificate()
|
||||
val fingerprint = db.certificates.put(expectedCertificate)
|
||||
val certificate = db.certificates.get(fingerprint)
|
||||
certificate.value.getEncoded shouldEqual expectedCertificate.getEncoded
|
||||
}
|
||||
|
||||
it should "guarantee that adding a certificate is idempotent" in withDatabase { db =>
|
||||
val (_, expectedCertificate) = generateKeyAndCertificate()
|
||||
val fingerprint1 = db.certificates.put(expectedCertificate)
|
||||
val fingerprint2 = db.certificates.put(expectedCertificate)
|
||||
fingerprint1 shouldEqual fingerprint2
|
||||
val certificate = db.certificates.get(fingerprint1)
|
||||
certificate.value.getEncoded shouldEqual expectedCertificate.getEncoded
|
||||
}
|
||||
|
||||
it should "correctly read and write signed payloads" in withDatabase { db =>
|
||||
val (privateKey, expectedCertificate) = generateKeyAndCertificate()
|
||||
|
||||
val expectedTimestamp = Instant.ofEpochMilli(42)
|
||||
|
||||
val expectedAlgorithm =
|
||||
AlgorithmString.SHA256withRSA
|
||||
|
||||
val expectedPayload =
|
||||
PayloadBytes.wrap(generateCommand().toByteArray)
|
||||
|
||||
val expectedKey =
|
||||
db.signedPayloads.keyEncoder.encode(expectedPayload)
|
||||
|
||||
val expectedFingerprint =
|
||||
FingerprintBytes.compute(expectedCertificate)
|
||||
|
||||
val expectedSignature =
|
||||
SignatureBytes.sign(
|
||||
expectedAlgorithm,
|
||||
privateKey,
|
||||
expectedPayload,
|
||||
)
|
||||
|
||||
val expectedSignedPayload =
|
||||
SignedPayload(
|
||||
expectedAlgorithm,
|
||||
expectedFingerprint,
|
||||
expectedPayload,
|
||||
expectedSignature,
|
||||
expectedTimestamp,
|
||||
)
|
||||
|
||||
db.signedPayloads.put(expectedSignedPayload)
|
||||
|
||||
val result = db.signedPayloads.get(expectedKey)
|
||||
|
||||
result should contain only expectedSignedPayload
|
||||
|
||||
}
|
||||
|
||||
private val resourceFactory = new ResourceOwnerFactories[ExecutionContext] {
|
||||
override protected implicit val hasExecutionContext: HasExecutionContext[ExecutionContext] =
|
||||
HasExecutionContext.`ExecutionContext has itself`
|
||||
}
|
||||
|
||||
private def withDatabase(test: Tables => Future[Assertion]): Future[Assertion] =
|
||||
createTransactor(
|
||||
postgresDatabase.url,
|
||||
postgresDatabase.userName,
|
||||
postgresDatabase.password,
|
||||
maxPoolSize = 10,
|
||||
resourceFactory,
|
||||
).use { transactor =>
|
||||
val db = Tables.initialize(transactor)
|
||||
test(db)
|
||||
}
|
||||
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
load(
|
||||
"//bazel_tools:scala.bzl",
|
||||
"da_scala_library",
|
||||
"da_scala_test",
|
||||
)
|
||||
|
||||
da_scala_library(
|
||||
name = "non-repudiation-resources",
|
||||
srcs = glob(["src/main/scala/**/*.scala"]),
|
||||
scala_deps = [
|
||||
"@maven//:org_tpolecat_doobie_core",
|
||||
"@maven//:org_tpolecat_doobie_hikari",
|
||||
"@maven//:org_typelevel_cats_core",
|
||||
"@maven//:org_typelevel_cats_effect",
|
||||
"@maven//:org_typelevel_cats_kernel",
|
||||
],
|
||||
visibility = [
|
||||
"//:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//libs-scala/resources",
|
||||
"@maven//:com_zaxxer_HikariCP",
|
||||
],
|
||||
)
|
@ -1,103 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.resources
|
||||
|
||||
import java.util.concurrent.{ExecutorService, Executors, ThreadFactory}
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
import HikariTransactorResourceOwner.NamedThreadFactory
|
||||
import cats.effect.{Blocker, ContextShift, IO}
|
||||
import com.daml.resources.{AbstractResourceOwner, HasExecutionContext, ResourceOwnerFactories}
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import doobie.hikari.HikariTransactor
|
||||
|
||||
import scala.annotation.nowarn
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
object HikariTransactorResourceOwner {
|
||||
|
||||
object NamedThreadFactory {
|
||||
|
||||
def cachedThreadPool(threadNamePrefix: String): ExecutorService =
|
||||
Executors.newCachedThreadPool(new NamedThreadFactory(threadNamePrefix))
|
||||
|
||||
def fixedThreadPool(size: Int, threadNamePrefix: String): ExecutorService =
|
||||
Executors.newFixedThreadPool(size, new NamedThreadFactory(threadNamePrefix))
|
||||
|
||||
}
|
||||
|
||||
private final class NamedThreadFactory(threadNamePrefix: String) extends ThreadFactory {
|
||||
|
||||
require(threadNamePrefix.nonEmpty, "The thread name prefix cannot be empty")
|
||||
|
||||
private val threadCounter = new AtomicInteger(0)
|
||||
|
||||
def newThread(r: Runnable): Thread = {
|
||||
val t = new Thread(r, s"$threadNamePrefix-${threadCounter.getAndIncrement()}")
|
||||
t.setDaemon(true)
|
||||
t
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def apply[Context: HasExecutionContext](
|
||||
factory: ResourceOwnerFactories[Context]
|
||||
)(jdbcUrl: String, username: String, password: String, maxPoolSize: Int)(implicit
|
||||
cs: ContextShift[IO]
|
||||
): AbstractResourceOwner[Context, HikariTransactor[IO]] =
|
||||
new HikariTransactorResourceOwner[Context](factory).apply(
|
||||
jdbcUrl,
|
||||
username,
|
||||
password,
|
||||
maxPoolSize,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@nowarn("msg=evidence parameter evidence.* is never used")
|
||||
final class HikariTransactorResourceOwner[Context: HasExecutionContext] private (
|
||||
resourceOwner: ResourceOwnerFactories[Context]
|
||||
) {
|
||||
|
||||
private val BlockerPoolName = "transactor-blocker-pool"
|
||||
private val ConnectorPoolName = "transactor-connector-pool"
|
||||
|
||||
private def managedBlocker: AbstractResourceOwner[Context, Blocker] =
|
||||
resourceOwner
|
||||
.forExecutorService(() => NamedThreadFactory.cachedThreadPool(BlockerPoolName))
|
||||
.map(Blocker.liftExecutorService)
|
||||
|
||||
private def managedConnector(
|
||||
size: Int
|
||||
): AbstractResourceOwner[Context, ExecutionContext] =
|
||||
resourceOwner
|
||||
.forExecutorService(() => NamedThreadFactory.fixedThreadPool(size, ConnectorPoolName))
|
||||
.map(ExecutionContext.fromExecutorService)
|
||||
|
||||
private def managedHikariDataSource(
|
||||
jdbcUrl: String,
|
||||
username: String,
|
||||
password: String,
|
||||
maxPoolSize: Int,
|
||||
): AbstractResourceOwner[Context, HikariDataSource] =
|
||||
resourceOwner.forCloseable { () =>
|
||||
val pool = new HikariDataSource()
|
||||
pool.setAutoCommit(false)
|
||||
pool.setJdbcUrl(jdbcUrl)
|
||||
pool.setUsername(username)
|
||||
pool.setPassword(password)
|
||||
pool.setMaximumPoolSize(maxPoolSize)
|
||||
pool
|
||||
}
|
||||
|
||||
def apply(jdbcUrl: String, username: String, password: String, maxPoolSize: Int)(implicit
|
||||
cs: ContextShift[IO]
|
||||
): AbstractResourceOwner[Context, HikariTransactor[IO]] =
|
||||
for {
|
||||
blocker <- managedBlocker
|
||||
connector <- managedConnector(size = maxPoolSize)
|
||||
dataSource <- managedHikariDataSource(jdbcUrl, username, password, maxPoolSize)
|
||||
} yield HikariTransactor[IO](dataSource, connector, blocker)
|
||||
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
load(
|
||||
"//bazel_tools:scala.bzl",
|
||||
"da_scala_library",
|
||||
)
|
||||
|
||||
da_scala_library(
|
||||
name = "non-repudiation-testing",
|
||||
srcs = glob(["src/main/scala/**/*.scala"]),
|
||||
visibility = [
|
||||
"//:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//canton:ledger_api_proto_scala",
|
||||
"//libs-scala/ports",
|
||||
"//libs-scala/ports:ports-testing",
|
||||
"//runtime-components/non-repudiation",
|
||||
"//runtime-components/non-repudiation-client",
|
||||
"@maven//:io_grpc_grpc_inprocess",
|
||||
"@maven//:io_grpc_grpc_netty",
|
||||
"@maven//:io_grpc_grpc_services",
|
||||
],
|
||||
)
|
@ -1,17 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.client
|
||||
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
object TestSigningInterceptors {
|
||||
|
||||
private[nonrepudiation] def signEverything(
|
||||
key: PrivateKey,
|
||||
certificate: X509Certificate,
|
||||
): SigningInterceptor =
|
||||
new SigningInterceptor(key, certificate, _ => true)
|
||||
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.testing
|
||||
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
import com.daml.nonrepudiation.{CertificateRepository, FingerprintBytes}
|
||||
|
||||
import scala.collection.concurrent.TrieMap
|
||||
|
||||
final class Certificates extends CertificateRepository {
|
||||
|
||||
private val map = TrieMap.empty[FingerprintBytes, X509Certificate]
|
||||
|
||||
override def get(fingerprint: FingerprintBytes): Option[X509Certificate] =
|
||||
map.get(fingerprint)
|
||||
|
||||
override def put(certificate: X509Certificate): FingerprintBytes = {
|
||||
val fingerprint = FingerprintBytes.compute(certificate)
|
||||
map.put(fingerprint, certificate)
|
||||
fingerprint
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.testing
|
||||
|
||||
import com.daml.ledger.api.v1.command_service.CommandServiceGrpc.CommandService
|
||||
import com.daml.ledger.api.v1.command_service.{
|
||||
CommandServiceGrpc,
|
||||
SubmitAndWaitForTransactionIdResponse,
|
||||
SubmitAndWaitForTransactionResponse,
|
||||
SubmitAndWaitForTransactionTreeResponse,
|
||||
SubmitAndWaitRequest,
|
||||
}
|
||||
import com.google.protobuf.empty.Empty
|
||||
import io.grpc.ServerServiceDefinition
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
object DummyCommandService {
|
||||
|
||||
def bind(executionContext: ExecutionContext): ServerServiceDefinition =
|
||||
CommandServiceGrpc.bindService(new DummyCommandService, executionContext)
|
||||
|
||||
}
|
||||
|
||||
final class DummyCommandService private extends CommandService {
|
||||
|
||||
override def submitAndWait(request: SubmitAndWaitRequest): Future[Empty] =
|
||||
Future.successful(Empty.defaultInstance)
|
||||
|
||||
override def submitAndWaitForTransactionId(
|
||||
request: SubmitAndWaitRequest
|
||||
): Future[SubmitAndWaitForTransactionIdResponse] =
|
||||
Future.successful(SubmitAndWaitForTransactionIdResponse.defaultInstance)
|
||||
|
||||
override def submitAndWaitForTransaction(
|
||||
request: SubmitAndWaitRequest
|
||||
): Future[SubmitAndWaitForTransactionResponse] =
|
||||
Future.successful(SubmitAndWaitForTransactionResponse.defaultInstance)
|
||||
|
||||
override def submitAndWaitForTransactionTree(
|
||||
request: SubmitAndWaitRequest
|
||||
): Future[SubmitAndWaitForTransactionTreeResponse] =
|
||||
Future.successful(SubmitAndWaitForTransactionTreeResponse.defaultInstance)
|
||||
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.testing
|
||||
|
||||
import com.daml.ledger.api.v1.command_submission_service.CommandSubmissionServiceGrpc.CommandSubmissionService
|
||||
import com.daml.ledger.api.v1.command_submission_service.{
|
||||
CommandSubmissionServiceGrpc,
|
||||
SubmitRequest,
|
||||
}
|
||||
import com.google.protobuf.empty.Empty
|
||||
import io.grpc.ServerServiceDefinition
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
object DummyCommandSubmissionService {
|
||||
|
||||
def bind(executionContext: ExecutionContext): ServerServiceDefinition =
|
||||
CommandSubmissionServiceGrpc.bindService(new DummyCommandSubmissionService, executionContext)
|
||||
|
||||
}
|
||||
|
||||
final class DummyCommandSubmissionService private extends CommandSubmissionService {
|
||||
|
||||
override def submit(request: SubmitRequest): Future[Empty] =
|
||||
Future.successful(Empty.defaultInstance)
|
||||
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.testing
|
||||
|
||||
import com.daml.ledger.api.v1.ledger_identity_service.LedgerIdentityServiceGrpc.LedgerIdentityService
|
||||
import com.daml.ledger.api.v1.ledger_identity_service.{
|
||||
GetLedgerIdentityRequest,
|
||||
GetLedgerIdentityResponse,
|
||||
LedgerIdentityServiceGrpc,
|
||||
}
|
||||
import io.grpc.ServerServiceDefinition
|
||||
|
||||
import scala.annotation.nowarn
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
object DummyLedgerIdentityService {
|
||||
|
||||
def bind(executionContext: ExecutionContext): ServerServiceDefinition =
|
||||
LedgerIdentityServiceGrpc.bindService(
|
||||
new DummyLedgerIdentityService,
|
||||
executionContext,
|
||||
): @nowarn(
|
||||
"cat=deprecation&origin=com\\.daml\\.ledger\\.api\\.v1\\.ledger_identity_service\\..*"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
final class DummyLedgerIdentityService private extends LedgerIdentityService {
|
||||
@nowarn("cat=deprecation&origin=com\\.daml\\.ledger\\.api\\.v1\\.ledger_identity_service\\..*")
|
||||
override def getLedgerIdentity(
|
||||
request: GetLedgerIdentityRequest
|
||||
): Future[GetLedgerIdentityResponse] =
|
||||
Future.successful(GetLedgerIdentityResponse.of("dummy"))
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.testing
|
||||
|
||||
import java.net.{InetAddress, InetSocketAddress, SocketAddress}
|
||||
|
||||
import com.daml.ports.FreePort
|
||||
import io.grpc.inprocess.{InProcessChannelBuilder, InProcessServerBuilder}
|
||||
import io.grpc.netty.{NettyChannelBuilder, NettyServerBuilder}
|
||||
import io.grpc.protobuf.services.ProtoReflectionService
|
||||
import io.grpc.{ManagedChannelBuilder, ServerBuilder}
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
object DummyTestSetup {
|
||||
|
||||
final class Builders(
|
||||
val participantServer: ServerBuilder[_ <: ServerBuilder[_]],
|
||||
val participantChannel: ManagedChannelBuilder[_ <: ManagedChannelBuilder[_]],
|
||||
val proxyServer: ServerBuilder[_ <: ServerBuilder[_]],
|
||||
val proxyChannel: ManagedChannelBuilder[_ <: ManagedChannelBuilder[_]],
|
||||
)
|
||||
|
||||
object Builders {
|
||||
|
||||
private def withRequiredServices[Builder <: ServerBuilder[Builder]](
|
||||
builder: Builder,
|
||||
executionContext: ExecutionContext,
|
||||
): Builder =
|
||||
builder
|
||||
.addService(DummyLedgerIdentityService.bind(executionContext))
|
||||
.addService(DummyCommandSubmissionService.bind(executionContext))
|
||||
.addService(DummyCommandService.bind(executionContext))
|
||||
.addService(ProtoReflectionService.newInstance())
|
||||
|
||||
def apply(useNetworkStack: Boolean, serviceExecutionContext: ExecutionContext): Builders = {
|
||||
if (useNetworkStack) {
|
||||
val participantPort: Int = FreePort.find().value
|
||||
val participantAddress: SocketAddress =
|
||||
new InetSocketAddress(InetAddress.getLoopbackAddress, participantPort)
|
||||
val proxyPort: Int = FreePort.find().value
|
||||
val proxyAddress: SocketAddress =
|
||||
new InetSocketAddress(InetAddress.getLoopbackAddress, proxyPort)
|
||||
new Builders(
|
||||
withRequiredServices(
|
||||
NettyServerBuilder.forPort(participantPort),
|
||||
serviceExecutionContext,
|
||||
),
|
||||
NettyChannelBuilder.forAddress(participantAddress).usePlaintext(),
|
||||
NettyServerBuilder.forPort(proxyPort),
|
||||
NettyChannelBuilder.forAddress(proxyAddress).usePlaintext(),
|
||||
)
|
||||
} else {
|
||||
val participantName: String = InProcessServerBuilder.generateName()
|
||||
val proxyName: String = InProcessServerBuilder.generateName()
|
||||
new Builders(
|
||||
withRequiredServices(
|
||||
InProcessServerBuilder.forName(participantName),
|
||||
serviceExecutionContext,
|
||||
),
|
||||
InProcessChannelBuilder.forName(participantName),
|
||||
InProcessServerBuilder.forName(proxyName),
|
||||
InProcessChannelBuilder.forName(proxyName),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation.testing
|
||||
|
||||
import com.daml.nonrepudiation.{SignedPayload, SignedPayloadRepository}
|
||||
import com.daml.nonrepudiation.SignedPayloadRepository.KeyEncoder
|
||||
|
||||
import scala.collection.concurrent.TrieMap
|
||||
|
||||
final class SignedPayloads[Key: KeyEncoder] extends SignedPayloadRepository[Key] {
|
||||
|
||||
private val map = TrieMap.empty[Key, Vector[SignedPayload]].withDefaultValue(Vector.empty)
|
||||
|
||||
override def put(signedPayload: SignedPayload): Unit = {
|
||||
val key = keyEncoder.encode(signedPayload.payload)
|
||||
val _ = map.put(key, map(key) :+ signedPayload)
|
||||
}
|
||||
|
||||
override def get(key: Key): Iterable[SignedPayload] =
|
||||
map(key)
|
||||
|
||||
def size: Int = map.size
|
||||
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation
|
||||
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.UUID
|
||||
|
||||
import com.daml.ledger.api.v1.command_submission_service.SubmitRequest
|
||||
import com.daml.ledger.api.v1.commands.Commands.DeduplicationPeriod.DeduplicationTime
|
||||
import com.daml.ledger.api.v1.commands.{Command, Commands, CreateCommand}
|
||||
import com.daml.ledger.api.v1.value.{Identifier, Record, RecordField, Value}
|
||||
import com.google.protobuf.duration.Duration
|
||||
import sun.security.tools.keytool.CertAndKeyGen
|
||||
import sun.security.x509.X500Name
|
||||
|
||||
import scala.annotation.nowarn
|
||||
import scala.concurrent.duration.DurationInt
|
||||
|
||||
package object testing {
|
||||
|
||||
def generateKeyAndCertificate(): (PrivateKey, X509Certificate) = {
|
||||
val generator = new CertAndKeyGen(AlgorithmString.RSA, AlgorithmString.SHA256withRSA)
|
||||
generator.generate(2048)
|
||||
val key = generator.getPrivateKey
|
||||
val certificate = generator.getSelfCertificate(
|
||||
new X500Name("CN=Non-Repudiation Test,O=Digital Asset,L=Zurich,C=CH"),
|
||||
1.hour.toSeconds,
|
||||
)
|
||||
(key, certificate)
|
||||
}
|
||||
|
||||
private val LedgerId = UUID.randomUUID.toString
|
||||
private val WorkflowId = UUID.randomUUID.toString
|
||||
private val ApplicationId = UUID.randomUUID.toString
|
||||
private val Party = UUID.randomUUID.toString
|
||||
|
||||
@nowarn("msg=deprecated")
|
||||
def generateCommand(
|
||||
payload: String = "hello, world",
|
||||
commandId: String = UUID.randomUUID.toString,
|
||||
): SubmitRequest =
|
||||
SubmitRequest(
|
||||
commands = Some(
|
||||
Commands(
|
||||
ledgerId = LedgerId,
|
||||
workflowId = WorkflowId,
|
||||
applicationId = ApplicationId,
|
||||
commandId = commandId,
|
||||
party = Party,
|
||||
commands = Seq(
|
||||
Command(
|
||||
Command.Command.Create(
|
||||
CreateCommand(
|
||||
templateId = Some(Identifier("Package", "Module", "Entity")),
|
||||
createArguments = Some(
|
||||
Record(
|
||||
recordId = None,
|
||||
fields = Seq(
|
||||
RecordField(
|
||||
label = "field",
|
||||
value = Some(Value(Value.Sum.Text(payload))),
|
||||
)
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
deduplicationPeriod = DeduplicationTime(Duration(seconds = 1.day.toSeconds)),
|
||||
minLedgerTimeRel = Some(Duration(seconds = 1.minute.toSeconds)),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
load(
|
||||
"//bazel_tools:scala.bzl",
|
||||
"da_scala_library",
|
||||
"da_scala_test",
|
||||
)
|
||||
|
||||
da_scala_library(
|
||||
name = "non-repudiation",
|
||||
srcs = glob(["src/main/scala/**/*.scala"]),
|
||||
visibility = [
|
||||
"//:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//canton:ledger_api_proto_scala",
|
||||
"//libs-scala/grpc-reverse-proxy",
|
||||
"//libs-scala/resources",
|
||||
"//observability/metrics",
|
||||
"//runtime-components/non-repudiation-core",
|
||||
"@maven//:com_google_guava_guava",
|
||||
"@maven//:io_opentelemetry_opentelemetry_api",
|
||||
"@maven//:io_opentelemetry_opentelemetry_sdk_common",
|
||||
"@maven//:io_opentelemetry_opentelemetry_sdk_metrics",
|
||||
"@maven//:org_slf4j_slf4j_api",
|
||||
],
|
||||
)
|
||||
|
||||
da_scala_test(
|
||||
name = "test",
|
||||
srcs = glob(["src/test/scala/**/*.scala"]),
|
||||
resources = [
|
||||
"src/test/resources/logback-test.xml",
|
||||
"//test-common:dar-files",
|
||||
],
|
||||
runtime_deps = [
|
||||
"@maven//:ch_qos_logback_logback_classic",
|
||||
],
|
||||
deps = [
|
||||
":non-repudiation",
|
||||
"//libs-scala/grpc-test-utils",
|
||||
"//libs-scala/resources",
|
||||
"//observability/metrics",
|
||||
"//runtime-components/non-repudiation-client",
|
||||
"//runtime-components/non-repudiation-testing",
|
||||
"@maven//:com_google_protobuf_protobuf_java",
|
||||
"@maven//:io_grpc_grpc_api",
|
||||
"@maven//:io_grpc_grpc_inprocess",
|
||||
"@maven//:io_grpc_grpc_services",
|
||||
],
|
||||
)
|
@ -1,32 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation
|
||||
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
import com.daml.metrics.api.MetricHandle.Timer
|
||||
|
||||
object CertificateRepository {
|
||||
|
||||
trait Read {
|
||||
|
||||
def get(fingerprint: FingerprintBytes): Option[X509Certificate]
|
||||
|
||||
}
|
||||
|
||||
trait Write {
|
||||
|
||||
/** Must guarantee idempotence. */
|
||||
def put(certificate: X509Certificate): FingerprintBytes
|
||||
|
||||
}
|
||||
|
||||
final class Timed(timer: Timer, delegate: Read) extends Read {
|
||||
override def get(fingerprint: FingerprintBytes): Option[X509Certificate] =
|
||||
timer.time(delegate.get(fingerprint))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
trait CertificateRepository extends CertificateRepository.Read with CertificateRepository.Write
|
@ -1,50 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation
|
||||
|
||||
import com.daml.metrics.api.MetricHandle.{LabeledMetricsFactory, Meter, Timer}
|
||||
import com.daml.metrics.api.MetricName
|
||||
import com.daml.resources.HasExecutionContext
|
||||
|
||||
import scala.concurrent.duration.Duration
|
||||
|
||||
class Metrics(metricsFactory: LabeledMetricsFactory) {
|
||||
|
||||
private val Prefix = MetricName("daml", "nonrepudiation")
|
||||
|
||||
private def name(suffix: String) = Prefix :+ suffix
|
||||
|
||||
// daml.nonrepudiation.processing
|
||||
// Overall time taken from interception to forwarding to the participant (or rejecting)
|
||||
val processingTimer: Timer = metricsFactory.timer(name("processing"))
|
||||
|
||||
// daml.nonrepudiation.get_key
|
||||
// Time taken to retrieve the key from the certificate store
|
||||
// Part of the time tracked in daml.nonrepudiation.processing
|
||||
val getKeyTimer: Timer = metricsFactory.timer(name("get_key"))
|
||||
|
||||
// daml.nonrepudiation.verify_signature
|
||||
// Time taken to verify the signature of a command
|
||||
// Part of the time tracked in daml.nonrepudiation.processing
|
||||
val verifySignatureTimer: Timer = metricsFactory.timer(name("verify_signature"))
|
||||
|
||||
// daml.nonrepudiation.add_signed_payload
|
||||
// Time taken to add the signed payload before ultimately forwarding the command
|
||||
// Part of the time tracked in daml.nonrepudiation.processing
|
||||
val addSignedPayloadTimer: Timer = metricsFactory.timer(name("add_signed_payload"))
|
||||
|
||||
// daml.nonrepudiation.rejections
|
||||
// Rate of calls that are being rejected before they can be forwarded to the participant
|
||||
// Historical and exponentially-weighted moving average rate over the latest 1, 5 and 15 minutes
|
||||
val rejectionsMeter: Meter = metricsFactory.meter(name("rejections"))
|
||||
|
||||
}
|
||||
|
||||
object Metrics {
|
||||
|
||||
def owner[Context: HasExecutionContext](reportingInterval: Duration) = new MetricsOwner(
|
||||
reportingInterval
|
||||
)
|
||||
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import com.daml.metrics.api.opentelemetry.{
|
||||
OpenTelemetryMetricsFactory,
|
||||
OpenTelemetryUtil,
|
||||
Slf4jMetricExporter,
|
||||
}
|
||||
import com.daml.resources.{AbstractResourceOwner, HasExecutionContext, ReleasableResource, Resource}
|
||||
import io.opentelemetry.sdk.metrics.SdkMeterProvider
|
||||
import io.opentelemetry.sdk.metrics.`export`.PeriodicMetricReader
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration.Duration
|
||||
|
||||
class MetricsOwner[Context: HasExecutionContext](interval: Duration)
|
||||
extends AbstractResourceOwner[Context, Metrics] {
|
||||
|
||||
override def acquire()(implicit
|
||||
context: Context
|
||||
): Resource[Context, Metrics] = {
|
||||
ReleasableResource(
|
||||
Future.successful(
|
||||
SdkMeterProvider
|
||||
.builder()
|
||||
.registerMetricReader(
|
||||
PeriodicMetricReader
|
||||
.builder(new Slf4jMetricExporter())
|
||||
.setInterval(interval.toMillis, TimeUnit.MILLISECONDS)
|
||||
.newMetricReaderFactory()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
)(meterProvider => OpenTelemetryUtil.completionCodeToFuture(meterProvider.shutdown()))
|
||||
.map(meterProvider =>
|
||||
new Metrics(new OpenTelemetryMetricsFactory(meterProvider.get("non-repudiation")))
|
||||
)
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation
|
||||
|
||||
import java.time.Clock
|
||||
|
||||
import com.daml.grpc.ReverseProxy
|
||||
import com.daml.resources.{AbstractResourceOwner, HasExecutionContext}
|
||||
import io.grpc.{Channel, Server, ServerBuilder}
|
||||
|
||||
object NonRepudiationProxy {
|
||||
|
||||
def owner[Context](
|
||||
participant: Channel,
|
||||
serverBuilder: ServerBuilder[_],
|
||||
certificateRepository: CertificateRepository.Read,
|
||||
signedPayloadRepository: SignedPayloadRepository.Write,
|
||||
timestampProvider: Clock,
|
||||
metrics: Metrics,
|
||||
serviceName: String,
|
||||
serviceNames: String*
|
||||
)(implicit context: HasExecutionContext[Context]): AbstractResourceOwner[Context, Server] = {
|
||||
|
||||
val signatureVerification =
|
||||
new SignatureVerificationInterceptor(
|
||||
certificateRepository,
|
||||
signedPayloadRepository,
|
||||
timestampProvider,
|
||||
metrics,
|
||||
)
|
||||
|
||||
ReverseProxy.owner(
|
||||
backend = participant,
|
||||
serverBuilder = serverBuilder,
|
||||
interceptors = Iterator(serviceName +: serviceNames: _*)
|
||||
.map(service => service -> Seq(signatureVerification))
|
||||
.toMap,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation
|
||||
|
||||
import java.security.PublicKey
|
||||
|
||||
final case class SignatureData(
|
||||
algorithm: AlgorithmString,
|
||||
fingerprint: FingerprintBytes,
|
||||
key: PublicKey,
|
||||
signature: SignatureBytes,
|
||||
)
|
@ -1,39 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation
|
||||
|
||||
import java.security.Signature
|
||||
|
||||
import com.daml.metrics.api.MetricHandle.Timer
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
object SignatureVerification {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[SignatureVerification])
|
||||
|
||||
final class Timed(timer: Timer) extends SignatureVerification {
|
||||
override def apply(payload: Array[Byte], signatureData: SignatureData): Try[Boolean] =
|
||||
timer.time(super.apply(payload, signatureData))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sealed abstract class SignatureVerification {
|
||||
|
||||
import SignatureVerification.logger
|
||||
|
||||
def apply(payload: Array[Byte], signatureData: SignatureData): Try[Boolean] =
|
||||
Try {
|
||||
logger.trace("Decoding signature bytes from Base64-encoded signature")
|
||||
logger.trace("Initializing signature verifier")
|
||||
val verifier = Signature.getInstance(signatureData.algorithm)
|
||||
verifier.initVerify(signatureData.key)
|
||||
verifier.update(payload)
|
||||
logger.trace("Verifying signature '{}'", signatureData.signature.base64)
|
||||
verifier.verify(signatureData.signature.unsafeArray)
|
||||
}
|
||||
|
||||
}
|
@ -1,197 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.security.PublicKey
|
||||
import java.time.Clock
|
||||
|
||||
import com.daml.grpc.interceptors.ForwardingServerCallListener
|
||||
import com.daml.metrics.api.MetricHandle.{Meter, Timer}
|
||||
import io.grpc.Metadata.Key
|
||||
import io.grpc.{Metadata, ServerCall, ServerCallHandler, ServerInterceptor, Status}
|
||||
import org.slf4j.{Logger, LoggerFactory}
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
final class SignatureVerificationInterceptor(
|
||||
certificateRepository: CertificateRepository.Read,
|
||||
signedPayloadRepository: SignedPayloadRepository.Write,
|
||||
timestampProvider: Clock,
|
||||
metrics: Metrics,
|
||||
) extends ServerInterceptor {
|
||||
|
||||
import SignatureVerificationInterceptor._
|
||||
|
||||
private val timedCertificateRepository =
|
||||
new CertificateRepository.Timed(metrics.getKeyTimer, certificateRepository)
|
||||
|
||||
private val timedSignedPayloadRepository =
|
||||
new SignedPayloadRepository.Timed(metrics.addSignedPayloadTimer, signedPayloadRepository)
|
||||
|
||||
private val timedSignatureVerification =
|
||||
new SignatureVerification.Timed(metrics.verifySignatureTimer)
|
||||
|
||||
override def interceptCall[ReqT, RespT](
|
||||
call: ServerCall[ReqT, RespT],
|
||||
metadata: Metadata,
|
||||
next: ServerCallHandler[ReqT, RespT],
|
||||
): ServerCall.Listener[ReqT] = {
|
||||
|
||||
val runningTimer = metrics.processingTimer.startAsync()
|
||||
|
||||
val signatureData =
|
||||
for {
|
||||
signature <- getHeader(metadata, Headers.SIGNATURE, SignatureBytes.wrap)
|
||||
algorithm <- getHeader(metadata, Headers.ALGORITHM, AlgorithmString.wrap)
|
||||
fingerprint <- getHeader(metadata, Headers.FINGERPRINT, FingerprintBytes.wrap)
|
||||
key <- getKey(timedCertificateRepository, fingerprint)
|
||||
} yield SignatureData(
|
||||
signature = signature,
|
||||
algorithm = algorithm,
|
||||
fingerprint = fingerprint,
|
||||
key = key,
|
||||
)
|
||||
|
||||
signatureData match {
|
||||
case Right(signatureData) =>
|
||||
new SignatureVerificationServerCallListener(
|
||||
call = call,
|
||||
metadata = metadata,
|
||||
next = next,
|
||||
signatureData = signatureData,
|
||||
signatureVerification = timedSignatureVerification,
|
||||
signedPayloads = timedSignedPayloadRepository,
|
||||
timestampProvider = timestampProvider,
|
||||
runningTimer = runningTimer,
|
||||
rejectionMeter = metrics.rejectionsMeter,
|
||||
)
|
||||
case Left(rejection) =>
|
||||
rejection.report(metrics.rejectionsMeter, runningTimer)
|
||||
call.close(SignatureVerificationFailed, new Metadata())
|
||||
new ServerCall.Listener[ReqT] {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object SignatureVerificationInterceptor {
|
||||
|
||||
private object Rejection {
|
||||
|
||||
def fromException(throwable: Throwable): Rejection =
|
||||
Failure("An exception was thrown while verifying the signature", throwable)
|
||||
|
||||
def missingHeader[A](header: Key[A]): Rejection =
|
||||
Error(s"Malformed request did not contain header '${header.name}'")
|
||||
|
||||
def missingCertificate(fingerprint: String): Rejection =
|
||||
Error(s"No certificate found for fingerprint $fingerprint")
|
||||
|
||||
val SignatureVerificationFailed: Rejection =
|
||||
Error("Signature verification failed")
|
||||
|
||||
final case class Error(description: String) extends Rejection
|
||||
|
||||
final case class Failure(description: String, cause: Throwable) extends Rejection
|
||||
|
||||
}
|
||||
|
||||
private trait Rejection {
|
||||
def report(rejectionMeter: Meter, timer: Timer.TimerHandle): Unit = {
|
||||
rejectionMeter.mark()
|
||||
timer.stop()
|
||||
this match {
|
||||
case Rejection.Error(reason) =>
|
||||
logger.debug(reason)
|
||||
case Rejection.Failure(reason, cause) =>
|
||||
logger.debug(reason, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val SignatureVerificationFailed: Status =
|
||||
Status.UNAUTHENTICATED.withDescription("Signature verification failed")
|
||||
|
||||
private def getKey(
|
||||
certificates: CertificateRepository.Read,
|
||||
fingerprint: FingerprintBytes,
|
||||
): Either[Rejection, PublicKey] = {
|
||||
logger.trace("Retrieving key for fingerprint '{}'", fingerprint.base64)
|
||||
certificates
|
||||
.get(fingerprint)
|
||||
.toRight(Rejection.missingCertificate(fingerprint.base64))
|
||||
.map(_.getPublicKey)
|
||||
}
|
||||
|
||||
private def getHeader[Raw, Wrapped](
|
||||
metadata: Metadata,
|
||||
key: Key[Raw],
|
||||
wrap: Raw => Wrapped,
|
||||
): Either[Rejection, Wrapped] = {
|
||||
logger.trace("Reading header '{}' from request", key.name)
|
||||
Option(metadata.get(key)).toRight(Rejection.missingHeader(key)).map(wrap)
|
||||
}
|
||||
|
||||
private val logger: Logger = LoggerFactory.getLogger(classOf[SignatureVerificationInterceptor])
|
||||
|
||||
private final class SignatureVerificationServerCallListener[ReqT, RespT](
|
||||
call: ServerCall[ReqT, RespT],
|
||||
metadata: Metadata,
|
||||
next: ServerCallHandler[ReqT, RespT],
|
||||
signatureData: SignatureData,
|
||||
signatureVerification: SignatureVerification,
|
||||
signedPayloads: SignedPayloadRepository.Write,
|
||||
timestampProvider: Clock,
|
||||
runningTimer: Timer.TimerHandle,
|
||||
rejectionMeter: Meter,
|
||||
) extends ForwardingServerCallListener(call, metadata, next) {
|
||||
|
||||
private def castToByteArray(request: ReqT): Either[Rejection, Array[Byte]] = {
|
||||
logger.trace("Casting request to byte array")
|
||||
Try(request.asInstanceOf[Array[Byte]]).toEither.left.map(Rejection.fromException)
|
||||
}
|
||||
|
||||
private def verifySignature(payload: Array[Byte]): Either[Rejection, Boolean] =
|
||||
signatureVerification(payload, signatureData).toEither.left
|
||||
.map(Rejection.fromException)
|
||||
.filterOrElse(identity, Rejection.SignatureVerificationFailed)
|
||||
|
||||
private def addSignedCommand(
|
||||
payload: Array[Byte]
|
||||
): Either[Rejection, Unit] = {
|
||||
logger.trace("Adding signed payload")
|
||||
val signedPayload = SignedPayload(
|
||||
algorithm = signatureData.algorithm,
|
||||
fingerprint = signatureData.fingerprint,
|
||||
payload = PayloadBytes.wrap(payload),
|
||||
signature = signatureData.signature,
|
||||
timestamp = timestampProvider.instant(),
|
||||
)
|
||||
Try(signedPayloads.put(signedPayload)).toEither.left.map(Rejection.fromException)
|
||||
}
|
||||
|
||||
override def onMessage(request: ReqT): Unit = {
|
||||
val result =
|
||||
for {
|
||||
payload <- castToByteArray(request)
|
||||
_ <- verifySignature(payload)
|
||||
_ <- addSignedCommand(payload)
|
||||
} yield {
|
||||
val input = new ByteArrayInputStream(payload)
|
||||
val copy = call.getMethodDescriptor.parseRequest(input)
|
||||
runningTimer.stop()
|
||||
super.onMessage(copy)
|
||||
}
|
||||
|
||||
result.left.foreach { rejection =>
|
||||
rejection.report(rejectionMeter, runningTimer)
|
||||
call.close(SignatureVerificationFailed, new Metadata())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
final case class SignedPayload(
|
||||
algorithm: AlgorithmString,
|
||||
fingerprint: FingerprintBytes,
|
||||
payload: PayloadBytes,
|
||||
signature: SignatureBytes,
|
||||
timestamp: Instant,
|
||||
)
|
@ -1,43 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation
|
||||
|
||||
import com.daml.metrics.api.MetricHandle.Timer
|
||||
import com.daml.nonrepudiation.SignedPayloadRepository.KeyEncoder
|
||||
|
||||
object SignedPayloadRepository {
|
||||
|
||||
object KeyEncoder {
|
||||
implicit object Base64EncodePayload extends KeyEncoder[String] {
|
||||
override def encode(payload: PayloadBytes): String =
|
||||
payload.base64
|
||||
}
|
||||
implicit object ParseCommandId extends KeyEncoder[CommandIdString] {
|
||||
override def encode(payload: PayloadBytes): CommandIdString =
|
||||
CommandIdString.assertFromPayload(payload)
|
||||
}
|
||||
}
|
||||
|
||||
trait KeyEncoder[Key] {
|
||||
def encode(payload: PayloadBytes): Key
|
||||
}
|
||||
|
||||
trait Read[Key] {
|
||||
def get(key: Key): Iterable[SignedPayload]
|
||||
}
|
||||
|
||||
trait Write {
|
||||
def put(signedPayload: SignedPayload): Unit
|
||||
}
|
||||
|
||||
final class Timed(timer: Timer, delegate: Write) extends Write {
|
||||
override def put(signedPayload: SignedPayload): Unit =
|
||||
timer.time[Unit](delegate.put(signedPayload))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class SignedPayloadRepository[Key](implicit val keyEncoder: KeyEncoder[Key])
|
||||
extends SignedPayloadRepository.Read[Key]
|
||||
with SignedPayloadRepository.Write
|
@ -1,85 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml
|
||||
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
import com.daml.ledger.api.v1.command_service.SubmitAndWaitRequest
|
||||
import com.daml.ledger.api.v1.command_submission_service.SubmitRequest
|
||||
import com.daml.ledger.api.v1.commands.Commands
|
||||
import com.google.common.io.BaseEncoding
|
||||
|
||||
import scala.collection.immutable.ArraySeq
|
||||
import scala.util.Try
|
||||
|
||||
package object nonrepudiation {
|
||||
|
||||
type AlgorithmString <: String
|
||||
|
||||
object AlgorithmString {
|
||||
def wrap(string: String): AlgorithmString = string.asInstanceOf[AlgorithmString]
|
||||
|
||||
val RSA: AlgorithmString = wrap("RSA")
|
||||
val SHA256withRSA: AlgorithmString = wrap("SHA256withRSA")
|
||||
}
|
||||
|
||||
type CommandIdString <: String
|
||||
|
||||
object CommandIdString {
|
||||
def wrap(string: String): CommandIdString = string.asInstanceOf[CommandIdString]
|
||||
|
||||
private val BadInput = "The mandatory `commands` field is absent"
|
||||
|
||||
private def throwBadInput: Nothing = throw new IllegalArgumentException(BadInput)
|
||||
|
||||
private def getCommandsOrThrow(maybeCommands: Option[Commands]): Commands =
|
||||
maybeCommands.getOrElse(throwBadInput)
|
||||
|
||||
private def parseFromSubmit(payload: Array[Byte]): Try[Option[Commands]] =
|
||||
Try(SubmitRequest.parseFrom(payload).commands)
|
||||
|
||||
private def parseFromSubmitAndWait(payload: Array[Byte]): Try[Option[Commands]] =
|
||||
Try(SubmitAndWaitRequest.parseFrom(payload).commands)
|
||||
|
||||
private def commands(payload: Array[Byte]): Try[Commands] =
|
||||
parseFromSubmit(payload).orElse(parseFromSubmitAndWait(payload)).map(getCommandsOrThrow)
|
||||
|
||||
def fromPayload(payload: PayloadBytes): Try[CommandIdString] =
|
||||
commands(payload.unsafeArray).map(_.commandId).map(wrap)
|
||||
|
||||
def assertFromPayload(payload: PayloadBytes): CommandIdString =
|
||||
fromPayload(payload).get
|
||||
}
|
||||
|
||||
type FingerprintBytes <: ArraySeq.ofByte
|
||||
|
||||
object FingerprintBytes {
|
||||
def wrap(bytes: Array[Byte]): FingerprintBytes =
|
||||
ArraySeq.unsafeWrapArray(bytes).asInstanceOf[FingerprintBytes]
|
||||
def compute(certificate: X509Certificate): FingerprintBytes =
|
||||
wrap(Fingerprints.compute(certificate))
|
||||
}
|
||||
|
||||
type PayloadBytes <: ArraySeq.ofByte
|
||||
|
||||
object PayloadBytes {
|
||||
def wrap(bytes: Array[Byte]): PayloadBytes =
|
||||
new ArraySeq.ofByte(bytes).asInstanceOf[PayloadBytes]
|
||||
}
|
||||
|
||||
type SignatureBytes <: ArraySeq.ofByte
|
||||
|
||||
object SignatureBytes {
|
||||
def wrap(bytes: Array[Byte]): SignatureBytes =
|
||||
new ArraySeq.ofByte(bytes).asInstanceOf[SignatureBytes]
|
||||
def sign(algorithm: AlgorithmString, key: PrivateKey, payload: PayloadBytes): SignatureBytes =
|
||||
wrap(Signatures.sign(algorithm, key, payload.unsafeArray))
|
||||
}
|
||||
|
||||
implicit final class BytesToBase64[Bytes <: ArraySeq.ofByte](val bytes: Bytes) extends AnyVal {
|
||||
def base64: String = BaseEncoding.base64().encode(bytes.unsafeArray)
|
||||
}
|
||||
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="stderr-appender" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<target>System.err</target>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>trace</level>
|
||||
</filter>
|
||||
<encoder>
|
||||
<pattern>%date{"yyyy-MM-dd'T'HH:mm:ss.SSSXXX", UTC} %-5level %logger{5}@[%-4.30thread] - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="${LOGLEVEL:-INFO}">
|
||||
<appender-ref ref="stderr-appender"/>
|
||||
</root>
|
||||
|
||||
<logger name="io.netty" level="WARN">
|
||||
<appender-ref ref="stderr-appender"/>
|
||||
</logger>
|
||||
<logger name="io.grpc.netty" level="WARN">
|
||||
<appender-ref ref="stderr-appender"/>
|
||||
</logger>
|
||||
</configuration>
|
@ -1,176 +0,0 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.nonrepudiation
|
||||
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.{Clock, Instant, ZoneId}
|
||||
|
||||
import com.daml.grpc.test.GrpcServer
|
||||
import com.daml.metrics.api.noop.NoOpMetricsFactory
|
||||
import com.daml.nonrepudiation.SignedPayloadRepository.KeyEncoder
|
||||
import com.daml.nonrepudiation.client.TestSigningInterceptors
|
||||
import io.grpc.inprocess.{InProcessChannelBuilder, InProcessServerBuilder}
|
||||
import io.grpc.{Channel, StatusRuntimeException}
|
||||
import org.scalatest.Inside
|
||||
import org.scalatest.flatspec.AsyncFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
final class NonRepudiationProxySpec
|
||||
extends AsyncFlatSpec
|
||||
with Matchers
|
||||
with Inside
|
||||
with GrpcServer {
|
||||
|
||||
import NonRepudiationProxySpec._
|
||||
import Services._
|
||||
import SignatureVerificationInterceptor.SignatureVerificationFailed
|
||||
|
||||
behavior of "NonRepudiationProxy"
|
||||
|
||||
it should "accept requests signed with a known key and add the correct signature" in withServices(
|
||||
Health.newInstance,
|
||||
Reflection.newInstance,
|
||||
) { channel =>
|
||||
val Setup(certificates, signedPayloads, privateKey, certificate, proxyBuilder, proxyChannel) =
|
||||
Setup.newInstance[String]
|
||||
val expectedTimestamp = Instant.ofEpochMilli(42)
|
||||
val timestampProvider = Clock.fixed(expectedTimestamp, ZoneId.systemDefault())
|
||||
|
||||
NonRepudiationProxy
|
||||
.owner(
|
||||
participant = channel,
|
||||
serverBuilder = proxyBuilder,
|
||||
certificateRepository = certificates,
|
||||
signedPayloadRepository = signedPayloads,
|
||||
timestampProvider = timestampProvider,
|
||||
metrics = new Metrics(NoOpMetricsFactory),
|
||||
Health.Name,
|
||||
)
|
||||
.use { _ =>
|
||||
val expectedAlgorithm =
|
||||
AlgorithmString.SHA256withRSA
|
||||
|
||||
val expectedPayload =
|
||||
PayloadBytes.wrap(Health.Requests.Check.toByteArray)
|
||||
|
||||
val expectedKey =
|
||||
signedPayloads.keyEncoder.encode(expectedPayload)
|
||||
|
||||
val expectedFingerprint =
|
||||
FingerprintBytes.compute(certificate)
|
||||
|
||||
val expectedSignature =
|
||||
SignatureBytes.sign(
|
||||
expectedAlgorithm,
|
||||
privateKey,
|
||||
expectedPayload,
|
||||
)
|
||||
|
||||
val expectedSignedPayload =
|
||||
SignedPayload(
|
||||
expectedAlgorithm,
|
||||
expectedFingerprint,
|
||||
expectedPayload,
|
||||
expectedSignature,
|
||||
expectedTimestamp,
|
||||
)
|
||||
|
||||
val result =
|
||||
Health.getHealthStatus(
|
||||
proxyChannel,
|
||||
TestSigningInterceptors.signEverything(privateKey, certificate),
|
||||
)
|
||||
|
||||
result shouldEqual Health.getHealthStatus(channel)
|
||||
|
||||
inside(signedPayloads.get(expectedKey)) { case signedPayloads =>
|
||||
signedPayloads should contain only expectedSignedPayload
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
it should "reject unsigned requests" in withServices(
|
||||
Health.newInstance,
|
||||
Reflection.newInstance,
|
||||
) { channel =>
|
||||
val Setup(certificates, signatures, _, _, proxyBuilder, proxyChannel) =
|
||||
Setup.newInstance[String]
|
||||
|
||||
NonRepudiationProxy
|
||||
.owner(
|
||||
participant = channel,
|
||||
serverBuilder = proxyBuilder,
|
||||
certificateRepository = certificates,
|
||||
signedPayloadRepository = signatures,
|
||||
timestampProvider = Clock.systemUTC(),
|
||||
metrics = new Metrics(NoOpMetricsFactory),
|
||||
Health.Name,
|
||||
)
|
||||
.use { _ =>
|
||||
the[StatusRuntimeException] thrownBy {
|
||||
Health.getHealthStatus(proxyChannel)
|
||||
} should have message SignatureVerificationFailed.asRuntimeException.getMessage
|
||||
}
|
||||
}
|
||||
|
||||
it should "reject requests signed with an unknown key" in withServices(
|
||||
Health.newInstance,
|
||||
Reflection.newInstance,
|
||||
) { channel =>
|
||||
val Setup(certificates, signatures, _, _, proxyBuilder, proxyChannel) =
|
||||
Setup.newInstance[String]
|
||||
|
||||
val (privateKey, certificate) = testing.generateKeyAndCertificate()
|
||||
|
||||
NonRepudiationProxy
|
||||
.owner(
|
||||
channel,
|
||||
proxyBuilder,
|
||||
certificates,
|
||||
signatures,
|
||||
Clock.systemUTC(),
|
||||
new Metrics(NoOpMetricsFactory),
|
||||
Health.Name,
|
||||
)
|
||||
.use { _ =>
|
||||
the[StatusRuntimeException] thrownBy {
|
||||
Health.getHealthStatus(
|
||||
proxyChannel,
|
||||
TestSigningInterceptors.signEverything(privateKey, certificate),
|
||||
)
|
||||
} should have message SignatureVerificationFailed.asRuntimeException.getMessage
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object NonRepudiationProxySpec {
|
||||
|
||||
final case class Setup[Key](
|
||||
certificates: CertificateRepository,
|
||||
signedPayloads: SignedPayloadRepository[Key],
|
||||
privateKey: PrivateKey,
|
||||
certificate: X509Certificate,
|
||||
proxyBuilder: InProcessServerBuilder,
|
||||
proxyChannel: Channel,
|
||||
)
|
||||
|
||||
object Setup {
|
||||
|
||||
def newInstance[Key: KeyEncoder]: Setup[Key] = {
|
||||
val certificates = new testing.Certificates
|
||||
val signatures = new testing.SignedPayloads
|
||||
val proxyName = InProcessServerBuilder.generateName()
|
||||
val proxyBuilder = InProcessServerBuilder.forName(proxyName)
|
||||
val proxyChannel = InProcessChannelBuilder.forName(proxyName).build()
|
||||
val (privateKey, certificate) = testing.generateKeyAndCertificate()
|
||||
certificates.put(certificate)
|
||||
Setup(certificates, signatures, privateKey, certificate, proxyBuilder, proxyChannel)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user