Skip to content

Commit

Permalink
Switch from public keys to certificates for non-repudiation (digital-…
Browse files Browse the repository at this point in the history
…asset#8739)

changelog_begin
changelog_end

Un-butchering the original design document
  • Loading branch information
stefanobaghino-da authored Feb 4, 2021
1 parent 6673efe commit 5a08c52
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import com.daml.nonrepudiation.Headers;
import com.daml.nonrepudiation.Signatures;
import io.grpc.*;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;

/**
* A gRPC client-side interceptor that uses a key pair to sign a payload and adds it as metadata to
Expand All @@ -20,11 +20,11 @@ public final class SigningInterceptor implements ClientInterceptor {
private final byte[] fingerprint;
private final String algorithm;

public SigningInterceptor(KeyPair keyPair, String signingAlgorithm) {
public SigningInterceptor(PrivateKey key, X509Certificate certificate) {
super();
this.key = keyPair.getPrivate();
this.algorithm = signingAlgorithm;
this.fingerprint = Fingerprints.compute(keyPair.getPublic());
this.key = key;
this.algorithm = certificate.getSigAlgName();
this.fingerprint = Fingerprints.compute(certificate);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,25 @@

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
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(PublicKey key) {
public static byte[] compute(X509Certificate certificate) {
try {
MessageDigest md = MessageDigest.getInstance(ALGORITHM);
byte[] encodedKey = key.getEncoded();
return md.digest(encodedKey);
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) 2021 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

object CertificateRepository {

trait Read {
def get(fingerprint: FingerprintBytes): Option[X509Certificate]
}

trait Write {
def put(certificate: X509Certificate): FingerprintBytes
}

}

trait CertificateRepository extends CertificateRepository.Read with CertificateRepository.Write

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ object NonRepudiationProxy {
def owner[Context: HasExecutionContext](
participant: Channel,
serverBuilder: ServerBuilder[_],
keyRepository: KeyRepository.Read,
certificateRepository: CertificateRepository.Read,
signedPayloadRepository: SignedPayloadRepository.Write,
timestampProvider: Clock,
serviceName: String,
serviceNames: String*
): AbstractResourceOwner[Context, Server] = {
val signatureVerification =
new SignatureVerificationInterceptor(
keyRepository,
certificateRepository,
signedPayloadRepository,
timestampProvider,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import org.slf4j.{Logger, LoggerFactory}
import scala.util.Try

final class SignatureVerificationInterceptor(
keyRepository: KeyRepository.Read,
certificateRepository: CertificateRepository.Read,
signedPayloads: SignedPayloadRepository.Write,
timestampProvider: Clock,
) extends ServerInterceptor {
Expand All @@ -33,7 +33,7 @@ final class SignatureVerificationInterceptor(
signature <- getHeader(metadata, Headers.SIGNATURE, SignatureBytes.wrap)
algorithm <- getHeader(metadata, Headers.ALGORITHM, AlgorithmString.wrap)
fingerprint <- getHeader(metadata, Headers.FINGERPRINT, FingerprintBytes.wrap)
key <- getKey(keyRepository, fingerprint)
key <- getKey(certificateRepository, fingerprint)
} yield SignatureData(
signature = signature,
algorithm = algorithm,
Expand Down Expand Up @@ -71,11 +71,11 @@ object SignatureVerificationInterceptor {
def missingHeader[A](header: Key[A]): Rejection =
Error(s"Malformed request did not contain header '${header.name}'")

def missingKey(fingerprint: String): Rejection =
Error(s"No key found for fingerprint $fingerprint")
def missingCertificate(fingerprint: String): Rejection =
Error(s"No certificate found for fingerprint $fingerprint")

val KeyVerificationFailed: Rejection =
Error("Key verification failed")
val SignatureVerificationFailed: Rejection =
Error("Signature verification failed")

final case class Error(description: String) extends Rejection

Expand All @@ -98,11 +98,14 @@ object SignatureVerificationInterceptor {
Status.UNAUTHENTICATED.withDescription("Signature verification failed")

private def getKey(
keys: KeyRepository.Read,
certificates: CertificateRepository.Read,
fingerprint: FingerprintBytes,
): Either[Rejection, PublicKey] = {
logger.trace("Retrieving key for fingerprint '{}'", fingerprint.base64)
keys.get(fingerprint).toRight(Rejection.missingKey(fingerprint.base64))
certificates
.get(fingerprint)
.toRight(Rejection.missingCertificate(fingerprint.base64))
.map(_.getPublicKey)
}

private def getHeader[Raw, Wrapped](
Expand Down Expand Up @@ -141,7 +144,7 @@ object SignatureVerificationInterceptor {
verifier.verify(signatureData.signature.unsafeArray)
}.toEither.left
.map(Rejection.fromException)
.filterOrElse(identity, Rejection.KeyVerificationFailed)
.filterOrElse(identity, Rejection.SignatureVerificationFailed)

private def addSignedCommand(
payload: Array[Byte]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

package com.daml

import java.security.{PrivateKey, PublicKey}
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
Expand All @@ -20,6 +21,7 @@ package object nonrepudiation {
object AlgorithmString {
def wrap(string: String): AlgorithmString = string.asInstanceOf[AlgorithmString]

val RSA: AlgorithmString = wrap("RSA")
val SHA256withRSA: AlgorithmString = wrap("SHA256withRSA")
}

Expand Down Expand Up @@ -56,8 +58,8 @@ package object nonrepudiation {
object FingerprintBytes {
def wrap(bytes: Array[Byte]): FingerprintBytes =
ArraySeq.unsafeWrapArray(bytes).asInstanceOf[FingerprintBytes]
def compute(key: PublicKey): FingerprintBytes =
wrap(Fingerprints.compute(key))
def compute(certificate: X509Certificate): FingerprintBytes =
wrap(Fingerprints.compute(certificate))
}

type PayloadBytes <: ArraySeq[Byte]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

package com.daml.nonrepudiation

import java.security.KeyPairGenerator
import java.time.Clock

import com.daml.ledger.api.testtool.infrastructure.{
Expand All @@ -29,17 +28,16 @@ import scala.concurrent.duration.DurationInt

final class NonRepudiationProxyConformance extends AsyncFlatSpec with Matchers with EitherValues {

import NonRepudiationProxySpec._
import NonRepudiationProxyConformance.ConformanceTestCases
import NonRepudiationProxySpec._

behavior of "NonRepudiationProxy"

it should "pass all conformance tests" in {
implicit val context: ResourceContext = ResourceContext(executionContext)
val config = SandboxConfig.defaultConfig.copy(port = Port.Dynamic)
val Setup(keys, signedPayloads, proxyBuilder, proxyChannel) = Setup.newInstance[CommandIdString]
val keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair()
keys.put(keyPair.getPublic)
val Setup(certificates, signedPayloads, privateKey, certificate, proxyBuilder, proxyChannel) =
Setup.newInstance[CommandIdString]

val proxy =
for {
Expand All @@ -54,7 +52,7 @@ final class NonRepudiationProxyConformance extends AsyncFlatSpec with Matchers w
proxy <- NonRepudiationProxy.owner[ResourceContext](
sandboxChannel,
proxyBuilder,
keys,
certificates,
signedPayloads,
Clock.systemUTC(),
CommandService.scalaDescriptor.fullName,
Expand All @@ -67,7 +65,7 @@ final class NonRepudiationProxyConformance extends AsyncFlatSpec with Matchers w
testCases = ConformanceTestCases,
participants = Vector(proxyChannel),
commandInterceptors = Seq(
new SigningInterceptor(keyPair, AlgorithmString.SHA256withRSA)
new SigningInterceptor(privateKey, certificate)
),
)

Expand Down
Loading

0 comments on commit 5a08c52

Please sign in to comment.