Introduce a helper to limit signing to commands (#9050)

This allows the signing interceptor to be generic for now (which
is useful to test), while allowing the users to use a simple
static method to only sign commands directed to the command and
command submittion services.

This is intended to address https://github.com/digital-asset/daml/pull/9036#discussion_r588332093

https://github.com/digital-asset/daml/pull/9036 should be rebased
on the main branch after this has been merged.

changelog_begin
changelog_end
This commit is contained in:
Stefano Baghino 2021-03-08 16:26:02 +01:00 committed by GitHub
parent 0c4d8ac19c
commit 45b33757b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 85 additions and 37 deletions

View File

@ -13,6 +13,7 @@ da_java_library(
"//:__subpackages__",
],
deps = [
"//language-support/java/bindings:bindings-java",
"//runtime-components/non-repudiation-core",
"@maven//:com_google_guava_guava",
"@maven//:io_grpc_grpc_api",

View File

@ -3,12 +3,19 @@
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
@ -19,50 +26,72 @@ public final class SigningInterceptor implements ClientInterceptor {
private final PrivateKey key;
private final byte[] fingerprint;
private final String algorithm;
private final Predicate<MethodDescriptor<?, ?>> signingPredicate;
public SigningInterceptor(PrivateKey key, X509Certificate certificate) {
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 <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
public final <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
ClientCall<ReqT, RespT> call = next.newCall(method, callOptions);
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(call) {
private Listener<RespT> responseListener = null;
private Metadata headers = null;
private int requested = 0;
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 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 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);
}
};
@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

@ -49,7 +49,7 @@ final class StubOwner private (
)
} yield CommandSubmissionServiceGrpc
.blockingStub(builders.proxyChannel.build())
.withInterceptors(new SigningInterceptor(key, certificate))
.withInterceptors(SigningInterceptor.signCommands(key, certificate))
stubOwner.acquire()(context)

View File

@ -87,7 +87,7 @@ final class NonRepudiationProxyConformance
testCases = ConformanceTestCases,
participants = Vector(proxyChannel),
commandInterceptors = Seq(
new SigningInterceptor(key, certificate)
SigningInterceptor.signCommands(key, certificate)
),
)

View File

@ -29,6 +29,7 @@ da_scala_library(
"//libs-scala/resources-akka",
"//libs-scala/resources-grpc",
"//runtime-components/non-repudiation",
"//runtime-components/non-repudiation-client",
"//runtime-components/non-repudiation-postgresql",
"//runtime-components/non-repudiation-resources",
"@maven//:com_zaxxer_HikariCP",

View File

@ -0,0 +1,17 @@
// Copyright (c) 2021 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

@ -9,7 +9,7 @@ import java.time.{Clock, Instant, ZoneId}
import com.daml.grpc.test.GrpcServer
import com.daml.nonrepudiation.SignedPayloadRepository.KeyEncoder
import com.daml.nonrepudiation.client.SigningInterceptor
import com.daml.nonrepudiation.client.TestSigningInterceptors
import io.grpc.inprocess.{InProcessChannelBuilder, InProcessServerBuilder}
import io.grpc.{Channel, StatusRuntimeException}
import org.scalatest.Inside
@ -78,7 +78,7 @@ final class NonRepudiationProxySpec
val result =
Health.getHealthStatus(
proxyChannel,
new SigningInterceptor(privateKey, certificate),
TestSigningInterceptors.signEverything(privateKey, certificate),
)
result shouldEqual Health.getHealthStatus(channel)
@ -135,7 +135,7 @@ final class NonRepudiationProxySpec
the[StatusRuntimeException] thrownBy {
Health.getHealthStatus(
proxyChannel,
new SigningInterceptor(privateKey, certificate),
TestSigningInterceptors.signEverything(privateKey, certificate),
)
} should have message SignatureVerificationFailed.asRuntimeException.getMessage
}