Skip to content

Commit

Permalink
Support mutual TLS for block eventing (#685)
Browse files Browse the repository at this point in the history
Fabric requires a SHA-256 hash of the client certificate used for mutual TLS authentication to be included in a block events request. This is to avoid replay attacks by ensuring that no TLS proxy (or man-in-the-middle) exists between the client and the Fabric Deliver service. Fabric checks that the hash of the client certificate included in the request matches the hash of the client certificate used to establish the TLS connection.

This change adds a Gateway connect option to specify the hash of the TLS client certificate, which is then included in the ChannelHeader for any block events request. This option is required only if using block eventing over a gRPC connection that uses mutual TLS authentication.

Signed-off-by: Mark S. Lewis <[email protected]>
  • Loading branch information
bestbeforetoday authored Mar 6, 2024
1 parent b0893c4 commit d278dc6
Show file tree
Hide file tree
Showing 18 changed files with 300 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,27 @@

package org.hyperledger.fabric.client;

import java.util.Objects;

import com.google.protobuf.ByteString;
import org.hyperledger.fabric.protos.common.Envelope;

import java.util.Objects;

final class BlockAndPrivateDataEventsBuilder implements BlockAndPrivateDataEventsRequest.Builder {
private final GatewayClient client;
private final SigningIdentity signingIdentity;
private final BlockEventsEnvelopeBuilder envelopeBuilder;

BlockAndPrivateDataEventsBuilder(final GatewayClient client, final SigningIdentity signingIdentity, final String channelName) {
BlockAndPrivateDataEventsBuilder(
final GatewayClient client,
final SigningIdentity signingIdentity,
final String channelName,
final ByteString tlsCertificateHash
) {
Objects.requireNonNull(channelName, "channel name");

this.client = client;
this.signingIdentity = signingIdentity;
this.envelopeBuilder = new BlockEventsEnvelopeBuilder(signingIdentity, channelName);
this.envelopeBuilder = new BlockEventsEnvelopeBuilder(signingIdentity, channelName, tlsCertificateHash);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,27 @@

package org.hyperledger.fabric.client;

import java.util.Objects;

import com.google.protobuf.ByteString;
import org.hyperledger.fabric.protos.common.Envelope;

import java.util.Objects;

final class BlockEventsBuilder implements BlockEventsRequest.Builder {
private final GatewayClient client;
private final SigningIdentity signingIdentity;
private final BlockEventsEnvelopeBuilder envelopeBuilder;

BlockEventsBuilder(final GatewayClient client, final SigningIdentity signingIdentity, final String channelName) {
BlockEventsBuilder(
final GatewayClient client,
final SigningIdentity signingIdentity,
final String channelName,
final ByteString tlsCertificateHash
) {
Objects.requireNonNull(channelName, "channel name");

this.client = client;
this.signingIdentity = signingIdentity;
this.envelopeBuilder = new BlockEventsEnvelopeBuilder(signingIdentity, channelName);
this.envelopeBuilder = new BlockEventsEnvelopeBuilder(signingIdentity, channelName, tlsCertificateHash);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@
final class BlockEventsEnvelopeBuilder {
private final SigningIdentity signingIdentity;
private final String channelName;
private final ByteString tlsCertificateHash;
private final StartPositionBuilder startPositionBuilder = new StartPositionBuilder();

BlockEventsEnvelopeBuilder(final SigningIdentity signingIdentity, final String channelName) {
BlockEventsEnvelopeBuilder(
final SigningIdentity signingIdentity,
final String channelName,
final ByteString tlsCertificateHash
) {
this.signingIdentity = signingIdentity;
this.channelName = channelName;
this.tlsCertificateHash = tlsCertificateHash;
}

public BlockEventsEnvelopeBuilder startBlock(final long blockNumber) {
Expand Down Expand Up @@ -56,6 +62,7 @@ private ChannelHeader newChannelHeader() {
.setEpoch(0)
.setTimestamp(GatewayUtils.getCurrentTimestamp())
.setType(HeaderType.DELIVER_SEEK_INFO_VALUE)
.setTlsCertHash(tlsCertificateHash)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,27 @@

package org.hyperledger.fabric.client;

import java.util.Objects;

import com.google.protobuf.ByteString;
import org.hyperledger.fabric.protos.common.Envelope;

import java.util.Objects;

final class FilteredBlockEventsBuilder implements FilteredBlockEventsRequest.Builder {
private final GatewayClient client;
private final SigningIdentity signingIdentity;
private final BlockEventsEnvelopeBuilder envelopeBuilder;

FilteredBlockEventsBuilder(final GatewayClient client, final SigningIdentity signingIdentity, final String channelName) {
FilteredBlockEventsBuilder(
final GatewayClient client,
final SigningIdentity signingIdentity,
final String channelName,
final ByteString tlsCertificateHash
) {
Objects.requireNonNull(channelName, "channel name");

this.client = client;
this.signingIdentity = signingIdentity;
this.envelopeBuilder = new BlockEventsEnvelopeBuilder(signingIdentity, channelName);
this.envelopeBuilder = new BlockEventsEnvelopeBuilder(signingIdentity, channelName, tlsCertificateHash);
}

@Override
Expand Down
9 changes: 9 additions & 0 deletions java/src/main/java/org/hyperledger/fabric/client/Gateway.java
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,15 @@ interface Builder {
*/
Builder hash(Function<byte[], byte[]> hash);

/**
* Specify the SHA-256 hash of the TLS client certificate. This option is required only if mutual TLS
* authentication is used for the gRPC connection to the Gateway peer.
*
* @param certificateHash A SHA-256 hash.
* @return The builder instance, allowing multiple configuration options to be chained.
*/
Builder tlsClientCertificateHash(byte[] certificateHash);

/**
* Specify the default call options for evaluating transactions.
* <p>A call of:</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package org.hyperledger.fabric.client;

import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import io.grpc.CallOptions;
import io.grpc.Channel;
Expand Down Expand Up @@ -34,6 +35,7 @@ public static final class Builder implements Gateway.Builder {
private Identity identity;
private Signer signer = UNDEFINED_SIGNER; // No signer implementation is required if only offline signing is used
private Function<byte[], byte[]> hash = Hash.SHA256;
private ByteString tlsCertificateHash = ByteString.empty();
private final DefaultCallOptions.Builder optionsBuilder = DefaultCallOptions.newBuiler();

@Override
Expand Down Expand Up @@ -64,6 +66,13 @@ public Builder hash(final Function<byte[], byte[]> hash) {
return this;
}

@Override
public Builder tlsClientCertificateHash(final byte[] certificateHash) {
Objects.requireNonNull(certificateHash, "certificateHash");
tlsCertificateHash = ByteString.copyFrom(certificateHash);
return this;
}

@Override
public Builder evaluateOptions(final UnaryOperator<CallOptions> options) {
Objects.requireNonNull(options, "evaluateOptions");
Expand Down Expand Up @@ -128,10 +137,12 @@ public GatewayImpl connect() {

private final GatewayClient client;
private final SigningIdentity signingIdentity;
private final ByteString tlsCertificateHash;

private GatewayImpl(final Builder builder) {
signingIdentity = new SigningIdentity(builder.identity, builder.hash, builder.signer);
client = new GatewayClient(builder.grpcChannel, builder.optionsBuilder.build());
tlsCertificateHash = builder.tlsCertificateHash;
}

@Override
Expand All @@ -145,7 +156,7 @@ public void close() {

@Override
public Network getNetwork(final String networkName) {
return new NetworkImpl(client, signingIdentity, networkName);
return new NetworkImpl(client, signingIdentity, networkName, tlsCertificateHash);
}

@Override
Expand Down
24 changes: 16 additions & 8 deletions java/src/main/java/org/hyperledger/fabric/client/NetworkImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,33 @@

package org.hyperledger.fabric.client;

import java.util.Objects;
import java.util.function.UnaryOperator;

import com.google.protobuf.ByteString;
import io.grpc.CallOptions;
import org.hyperledger.fabric.protos.common.Block;
import org.hyperledger.fabric.protos.peer.BlockAndPrivateData;
import org.hyperledger.fabric.protos.peer.FilteredBlock;

import java.util.Objects;
import java.util.function.UnaryOperator;

final class NetworkImpl implements Network {
private final GatewayClient client;
private final SigningIdentity signingIdentity;
private final String channelName;

NetworkImpl(final GatewayClient client, final SigningIdentity signingIdentity, final String channelName) {
private final ByteString tlsCertificateHash;

NetworkImpl(
final GatewayClient client,
final SigningIdentity signingIdentity,
final String channelName,
final ByteString tlsCertificateHash
) {
Objects.requireNonNull(channelName, "network name");

this.client = client;
this.signingIdentity = signingIdentity;
this.channelName = channelName;
this.tlsCertificateHash = tlsCertificateHash;
}

@Override
Expand Down Expand Up @@ -59,7 +67,7 @@ public CloseableIterator<Block> getBlockEvents(final UnaryOperator<CallOptions>

@Override
public BlockEventsRequest.Builder newBlockEventsRequest() {
return new BlockEventsBuilder(client, signingIdentity, channelName);
return new BlockEventsBuilder(client, signingIdentity, channelName, tlsCertificateHash);
}

@Override
Expand All @@ -69,7 +77,7 @@ public CloseableIterator<FilteredBlock> getFilteredBlockEvents(final UnaryOperat

@Override
public FilteredBlockEventsRequest.Builder newFilteredBlockEventsRequest() {
return new FilteredBlockEventsBuilder(client, signingIdentity, channelName);
return new FilteredBlockEventsBuilder(client, signingIdentity, channelName, tlsCertificateHash);
}

@Override
Expand All @@ -79,6 +87,6 @@ public CloseableIterator<BlockAndPrivateData> getBlockAndPrivateDataEvents(final

@Override
public BlockAndPrivateDataEventsRequest.Builder newBlockAndPrivateDataEventsRequest() {
return new BlockAndPrivateDataEventsBuilder(client, signingIdentity, channelName);
return new BlockAndPrivateDataEventsBuilder(client, signingIdentity, channelName, tlsCertificateHash);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,6 @@

package org.hyperledger.fabric.client;

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.protobuf.InvalidProtocolBufferException;
import io.grpc.CallOptions;
import io.grpc.Deadline;
Expand All @@ -35,12 +27,22 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.catchThrowableOfType;

public abstract class CommonBlockEventsTest<E> {
private static final Deadline defaultDeadline = Deadline.after(1, TimeUnit.DAYS);
private static final String tlsCertificateHash = "TLS_CLIENT_CERTIFICATE_HASH";

protected GatewayMocker mocker;
protected DeliverServiceStub stub;
Expand All @@ -54,6 +56,7 @@ void beforeEach() {
stub = mocker.getDeliverServiceStubSpy();

Gateway.Builder builder = mocker.getGatewayBuilder();
builder.tlsClientCertificateHash(tlsCertificateHash.getBytes(StandardCharsets.UTF_8));
setEventsOptions(builder, options -> options.withDeadline(defaultDeadline));
gateway = builder.connect();
network = gateway.getNetwork("NETWORK");
Expand Down Expand Up @@ -360,4 +363,19 @@ void uses_default_call_options() {
.extracting(CallOptions::getDeadline)
.isEqualTo(defaultDeadline);
}

@Test
void sends_request_with_tls_client_certificate_hash() throws Exception {
try (CloseableIterator<?> iter = getEvents()) {
iter.hasNext(); // Interact with iterator before asserting to ensure async request has been made
}

Envelope request = captureEvents().findFirst().get();
Payload payload = Payload.parseFrom(request.getPayload());
ChannelHeader channelHeader = ChannelHeader.parseFrom(payload.getHeader().getChannelHeader());

String actual = channelHeader.getTlsCertHash().toStringUtf8();
assertThat(actual).isEqualTo(tlsCertificateHash);
}

}
17 changes: 17 additions & 0 deletions node/src/blockevents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('Block Events', () => {
details: 'DETAILS',
metadata: new Metadata(),
});
const tlsClientCertificateHash = Uint8Array.from(Buffer.from('TLS_CLIENT_CERTIFICATE_HASH'));

let defaultOptions: () => CallOptions;
let client: MockGatewayGrpcClient;
Expand Down Expand Up @@ -83,6 +84,7 @@ describe('Block Events', () => {
signer,
hash,
client,
tlsClientCertificateHash,
blockEventsOptions: defaultOptions,
filteredBlockEventsOptions: defaultOptions,
blockAndPrivateDataEventsOptions: defaultOptions,
Expand Down Expand Up @@ -459,6 +461,21 @@ describe('Block Events', () => {

expect(stream.cancel).toHaveBeenCalled();
});

it('sends request with TLS client certificate hash', async () => {
const stream = newDuplexStreamResponse<common.Envelope, peer.DeliverResponse>([]);
testCase.mockResponse(stream);

await testCase.getEvents();

expect(stream.write.mock.calls.length).toBe(1);
const request = stream.write.mock.calls[0][0];

const payload = common.Payload.deserializeBinary(request.getPayload_asU8());
const header = assertDefined(payload.getHeader(), 'header');
const channelHeader = common.ChannelHeader.deserializeBinary(header.getChannelHeader_asU8());
expect(channelHeader.getTlsCertHash_asU8()).toEqual(tlsClientCertificateHash);
});
});
});
});
4 changes: 4 additions & 0 deletions node/src/blockeventsbuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface BlockEventsBuilderOptions extends EventsOptions {
client: GatewayClient;
signingIdentity: SigningIdentity;
channelName: string;
tlsCertificateHash?: Uint8Array;
}

export class BlockEventsBuilder {
Expand Down Expand Up @@ -133,6 +134,9 @@ class BlockEventsEnvelopeBuilder {
result.setEpoch(0);
result.setTimestamp(Timestamp.fromDate(new Date()));
result.setType(common.HeaderType.DELIVER_SEEK_INFO);
if (this.#options.tlsCertificateHash) {
result.setTlsCertHash(this.#options.tlsCertificateHash);
}
return result;
}

Expand Down
Loading

0 comments on commit d278dc6

Please sign in to comment.