remove non-repudiation (#18089)

run-all-tests: true
This commit is contained in:
mziolekda 2024-01-10 21:23:04 +01:00 committed by GitHub
parent 36fe0abb09
commit 576560428f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 5 additions and 3882 deletions

View File

@ -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/

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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,

View File

@ -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),

View File

@ -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,
)

View File

@ -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]

View File

@ -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")
}

View File

@ -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",
],

View File

@ -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))

View File

@ -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))
}
}

View File

@ -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) =>

View File

@ -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(

View File

@ -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",
],

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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(

View File

@ -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]}" +
")"
)

View File

@ -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 senders 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
}

View File

@ -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",
],
)

View File

@ -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 ()
}
}

View File

@ -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))
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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),
)
}

View File

@ -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,
)
}
}

View File

@ -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",
],
)

View File

@ -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,
)

View File

@ -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
}
}

View File

@ -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"),
)
}

View File

@ -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
}
}

View File

@ -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",
],
)

View File

@ -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);
}
}

View File

@ -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) {};
}
}
}

View File

@ -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>

View File

@ -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
}
}
}
}

View File

@ -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",
],
)

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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",
],
)

View File

@ -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>

View File

@ -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()
}
}

View File

@ -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),
)
}

View File

@ -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",
],
)

View File

@ -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
);

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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,
)

View File

@ -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)
}

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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",
],
)

View File

@ -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)
}

View File

@ -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",
],
)

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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"))
}

View File

@ -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),
)
}
}
}
}

View File

@ -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
}

View File

@ -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)),
)
)
)
}

View File

@ -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",
],
)

View File

@ -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

View File

@ -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
)
}

View File

@ -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")))
)
}
}

View File

@ -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,
)
}
}

View File

@ -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,
)

View File

@ -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)
}
}

View File

@ -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())
}
}
}
}

View File

@ -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,
)

View File

@ -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

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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)
}
}
}