Skip to content

Commit d278dc6

Browse files
Support mutual TLS for block eventing (#685)
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 <Mark.S.Lewis@outlook.com>
1 parent b0893c4 commit d278dc6

18 files changed

+300
-72
lines changed

java/src/main/java/org/hyperledger/fabric/client/BlockAndPrivateDataEventsBuilder.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,27 @@
66

77
package org.hyperledger.fabric.client;
88

9-
import java.util.Objects;
10-
9+
import com.google.protobuf.ByteString;
1110
import org.hyperledger.fabric.protos.common.Envelope;
1211

12+
import java.util.Objects;
13+
1314
final class BlockAndPrivateDataEventsBuilder implements BlockAndPrivateDataEventsRequest.Builder {
1415
private final GatewayClient client;
1516
private final SigningIdentity signingIdentity;
1617
private final BlockEventsEnvelopeBuilder envelopeBuilder;
1718

18-
BlockAndPrivateDataEventsBuilder(final GatewayClient client, final SigningIdentity signingIdentity, final String channelName) {
19+
BlockAndPrivateDataEventsBuilder(
20+
final GatewayClient client,
21+
final SigningIdentity signingIdentity,
22+
final String channelName,
23+
final ByteString tlsCertificateHash
24+
) {
1925
Objects.requireNonNull(channelName, "channel name");
2026

2127
this.client = client;
2228
this.signingIdentity = signingIdentity;
23-
this.envelopeBuilder = new BlockEventsEnvelopeBuilder(signingIdentity, channelName);
29+
this.envelopeBuilder = new BlockEventsEnvelopeBuilder(signingIdentity, channelName, tlsCertificateHash);
2430
}
2531

2632
@Override

java/src/main/java/org/hyperledger/fabric/client/BlockEventsBuilder.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,27 @@
66

77
package org.hyperledger.fabric.client;
88

9-
import java.util.Objects;
10-
9+
import com.google.protobuf.ByteString;
1110
import org.hyperledger.fabric.protos.common.Envelope;
1211

12+
import java.util.Objects;
13+
1314
final class BlockEventsBuilder implements BlockEventsRequest.Builder {
1415
private final GatewayClient client;
1516
private final SigningIdentity signingIdentity;
1617
private final BlockEventsEnvelopeBuilder envelopeBuilder;
1718

18-
BlockEventsBuilder(final GatewayClient client, final SigningIdentity signingIdentity, final String channelName) {
19+
BlockEventsBuilder(
20+
final GatewayClient client,
21+
final SigningIdentity signingIdentity,
22+
final String channelName,
23+
final ByteString tlsCertificateHash
24+
) {
1925
Objects.requireNonNull(channelName, "channel name");
2026

2127
this.client = client;
2228
this.signingIdentity = signingIdentity;
23-
this.envelopeBuilder = new BlockEventsEnvelopeBuilder(signingIdentity, channelName);
29+
this.envelopeBuilder = new BlockEventsEnvelopeBuilder(signingIdentity, channelName, tlsCertificateHash);
2430
}
2531

2632
@Override

java/src/main/java/org/hyperledger/fabric/client/BlockEventsEnvelopeBuilder.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@
1818
final class BlockEventsEnvelopeBuilder {
1919
private final SigningIdentity signingIdentity;
2020
private final String channelName;
21+
private final ByteString tlsCertificateHash;
2122
private final StartPositionBuilder startPositionBuilder = new StartPositionBuilder();
2223

23-
BlockEventsEnvelopeBuilder(final SigningIdentity signingIdentity, final String channelName) {
24+
BlockEventsEnvelopeBuilder(
25+
final SigningIdentity signingIdentity,
26+
final String channelName,
27+
final ByteString tlsCertificateHash
28+
) {
2429
this.signingIdentity = signingIdentity;
2530
this.channelName = channelName;
31+
this.tlsCertificateHash = tlsCertificateHash;
2632
}
2733

2834
public BlockEventsEnvelopeBuilder startBlock(final long blockNumber) {
@@ -56,6 +62,7 @@ private ChannelHeader newChannelHeader() {
5662
.setEpoch(0)
5763
.setTimestamp(GatewayUtils.getCurrentTimestamp())
5864
.setType(HeaderType.DELIVER_SEEK_INFO_VALUE)
65+
.setTlsCertHash(tlsCertificateHash)
5966
.build();
6067
}
6168

java/src/main/java/org/hyperledger/fabric/client/FilteredBlockEventsBuilder.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,27 @@
66

77
package org.hyperledger.fabric.client;
88

9-
import java.util.Objects;
10-
9+
import com.google.protobuf.ByteString;
1110
import org.hyperledger.fabric.protos.common.Envelope;
1211

12+
import java.util.Objects;
13+
1314
final class FilteredBlockEventsBuilder implements FilteredBlockEventsRequest.Builder {
1415
private final GatewayClient client;
1516
private final SigningIdentity signingIdentity;
1617
private final BlockEventsEnvelopeBuilder envelopeBuilder;
1718

18-
FilteredBlockEventsBuilder(final GatewayClient client, final SigningIdentity signingIdentity, final String channelName) {
19+
FilteredBlockEventsBuilder(
20+
final GatewayClient client,
21+
final SigningIdentity signingIdentity,
22+
final String channelName,
23+
final ByteString tlsCertificateHash
24+
) {
1925
Objects.requireNonNull(channelName, "channel name");
2026

2127
this.client = client;
2228
this.signingIdentity = signingIdentity;
23-
this.envelopeBuilder = new BlockEventsEnvelopeBuilder(signingIdentity, channelName);
29+
this.envelopeBuilder = new BlockEventsEnvelopeBuilder(signingIdentity, channelName, tlsCertificateHash);
2430
}
2531

2632
@Override

java/src/main/java/org/hyperledger/fabric/client/Gateway.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,15 @@ interface Builder {
236236
*/
237237
Builder hash(Function<byte[], byte[]> hash);
238238

239+
/**
240+
* Specify the SHA-256 hash of the TLS client certificate. This option is required only if mutual TLS
241+
* authentication is used for the gRPC connection to the Gateway peer.
242+
*
243+
* @param certificateHash A SHA-256 hash.
244+
* @return The builder instance, allowing multiple configuration options to be chained.
245+
*/
246+
Builder tlsClientCertificateHash(byte[] certificateHash);
247+
239248
/**
240249
* Specify the default call options for evaluating transactions.
241250
* <p>A call of:</p>

java/src/main/java/org/hyperledger/fabric/client/GatewayImpl.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
package org.hyperledger.fabric.client;
88

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

3941
@Override
@@ -64,6 +66,13 @@ public Builder hash(final Function<byte[], byte[]> hash) {
6466
return this;
6567
}
6668

69+
@Override
70+
public Builder tlsClientCertificateHash(final byte[] certificateHash) {
71+
Objects.requireNonNull(certificateHash, "certificateHash");
72+
tlsCertificateHash = ByteString.copyFrom(certificateHash);
73+
return this;
74+
}
75+
6776
@Override
6877
public Builder evaluateOptions(final UnaryOperator<CallOptions> options) {
6978
Objects.requireNonNull(options, "evaluateOptions");
@@ -128,10 +137,12 @@ public GatewayImpl connect() {
128137

129138
private final GatewayClient client;
130139
private final SigningIdentity signingIdentity;
140+
private final ByteString tlsCertificateHash;
131141

132142
private GatewayImpl(final Builder builder) {
133143
signingIdentity = new SigningIdentity(builder.identity, builder.hash, builder.signer);
134144
client = new GatewayClient(builder.grpcChannel, builder.optionsBuilder.build());
145+
tlsCertificateHash = builder.tlsCertificateHash;
135146
}
136147

137148
@Override
@@ -145,7 +156,7 @@ public void close() {
145156

146157
@Override
147158
public Network getNetwork(final String networkName) {
148-
return new NetworkImpl(client, signingIdentity, networkName);
159+
return new NetworkImpl(client, signingIdentity, networkName, tlsCertificateHash);
149160
}
150161

151162
@Override

java/src/main/java/org/hyperledger/fabric/client/NetworkImpl.java

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,33 @@
66

77
package org.hyperledger.fabric.client;
88

9-
import java.util.Objects;
10-
import java.util.function.UnaryOperator;
11-
9+
import com.google.protobuf.ByteString;
1210
import io.grpc.CallOptions;
1311
import org.hyperledger.fabric.protos.common.Block;
1412
import org.hyperledger.fabric.protos.peer.BlockAndPrivateData;
1513
import org.hyperledger.fabric.protos.peer.FilteredBlock;
1614

15+
import java.util.Objects;
16+
import java.util.function.UnaryOperator;
17+
1718
final class NetworkImpl implements Network {
1819
private final GatewayClient client;
1920
private final SigningIdentity signingIdentity;
2021
private final String channelName;
21-
22-
NetworkImpl(final GatewayClient client, final SigningIdentity signingIdentity, final String channelName) {
22+
private final ByteString tlsCertificateHash;
23+
24+
NetworkImpl(
25+
final GatewayClient client,
26+
final SigningIdentity signingIdentity,
27+
final String channelName,
28+
final ByteString tlsCertificateHash
29+
) {
2330
Objects.requireNonNull(channelName, "network name");
2431

2532
this.client = client;
2633
this.signingIdentity = signingIdentity;
2734
this.channelName = channelName;
35+
this.tlsCertificateHash = tlsCertificateHash;
2836
}
2937

3038
@Override
@@ -59,7 +67,7 @@ public CloseableIterator<Block> getBlockEvents(final UnaryOperator<CallOptions>
5967

6068
@Override
6169
public BlockEventsRequest.Builder newBlockEventsRequest() {
62-
return new BlockEventsBuilder(client, signingIdentity, channelName);
70+
return new BlockEventsBuilder(client, signingIdentity, channelName, tlsCertificateHash);
6371
}
6472

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

7078
@Override
7179
public FilteredBlockEventsRequest.Builder newFilteredBlockEventsRequest() {
72-
return new FilteredBlockEventsBuilder(client, signingIdentity, channelName);
80+
return new FilteredBlockEventsBuilder(client, signingIdentity, channelName, tlsCertificateHash);
7381
}
7482

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

8088
@Override
8189
public BlockAndPrivateDataEventsRequest.Builder newBlockAndPrivateDataEventsRequest() {
82-
return new BlockAndPrivateDataEventsBuilder(client, signingIdentity, channelName);
90+
return new BlockAndPrivateDataEventsBuilder(client, signingIdentity, channelName, tlsCertificateHash);
8391
}
8492
}

java/src/test/java/org/hyperledger/fabric/client/CommonBlockEventsTest.java

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,6 @@
66

77
package org.hyperledger.fabric.client;
88

9-
import java.security.cert.CertificateException;
10-
import java.security.cert.X509Certificate;
11-
import java.util.List;
12-
import java.util.concurrent.TimeUnit;
13-
import java.util.function.UnaryOperator;
14-
import java.util.stream.Collectors;
15-
import java.util.stream.Stream;
16-
179
import com.google.protobuf.InvalidProtocolBufferException;
1810
import io.grpc.CallOptions;
1911
import io.grpc.Deadline;
@@ -35,12 +27,22 @@
3527
import org.junit.jupiter.api.BeforeEach;
3628
import org.junit.jupiter.api.Test;
3729

30+
import java.nio.charset.StandardCharsets;
31+
import java.security.cert.CertificateException;
32+
import java.security.cert.X509Certificate;
33+
import java.util.List;
34+
import java.util.concurrent.TimeUnit;
35+
import java.util.function.UnaryOperator;
36+
import java.util.stream.Collectors;
37+
import java.util.stream.Stream;
38+
3839
import static org.assertj.core.api.Assertions.assertThat;
3940
import static org.assertj.core.api.Assertions.assertThatThrownBy;
4041
import static org.assertj.core.api.Assertions.catchThrowableOfType;
4142

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

4547
protected GatewayMocker mocker;
4648
protected DeliverServiceStub stub;
@@ -54,6 +56,7 @@ void beforeEach() {
5456
stub = mocker.getDeliverServiceStubSpy();
5557

5658
Gateway.Builder builder = mocker.getGatewayBuilder();
59+
builder.tlsClientCertificateHash(tlsCertificateHash.getBytes(StandardCharsets.UTF_8));
5760
setEventsOptions(builder, options -> options.withDeadline(defaultDeadline));
5861
gateway = builder.connect();
5962
network = gateway.getNetwork("NETWORK");
@@ -360,4 +363,19 @@ void uses_default_call_options() {
360363
.extracting(CallOptions::getDeadline)
361364
.isEqualTo(defaultDeadline);
362365
}
366+
367+
@Test
368+
void sends_request_with_tls_client_certificate_hash() throws Exception {
369+
try (CloseableIterator<?> iter = getEvents()) {
370+
iter.hasNext(); // Interact with iterator before asserting to ensure async request has been made
371+
}
372+
373+
Envelope request = captureEvents().findFirst().get();
374+
Payload payload = Payload.parseFrom(request.getPayload());
375+
ChannelHeader channelHeader = ChannelHeader.parseFrom(payload.getHeader().getChannelHeader());
376+
377+
String actual = channelHeader.getTlsCertHash().toStringUtf8();
378+
assertThat(actual).isEqualTo(tlsCertificateHash);
379+
}
380+
363381
}

node/src/blockevents.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe('Block Events', () => {
5252
details: 'DETAILS',
5353
metadata: new Metadata(),
5454
});
55+
const tlsClientCertificateHash = Uint8Array.from(Buffer.from('TLS_CLIENT_CERTIFICATE_HASH'));
5556

5657
let defaultOptions: () => CallOptions;
5758
let client: MockGatewayGrpcClient;
@@ -83,6 +84,7 @@ describe('Block Events', () => {
8384
signer,
8485
hash,
8586
client,
87+
tlsClientCertificateHash,
8688
blockEventsOptions: defaultOptions,
8789
filteredBlockEventsOptions: defaultOptions,
8890
blockAndPrivateDataEventsOptions: defaultOptions,
@@ -459,6 +461,21 @@ describe('Block Events', () => {
459461

460462
expect(stream.cancel).toHaveBeenCalled();
461463
});
464+
465+
it('sends request with TLS client certificate hash', async () => {
466+
const stream = newDuplexStreamResponse<common.Envelope, peer.DeliverResponse>([]);
467+
testCase.mockResponse(stream);
468+
469+
await testCase.getEvents();
470+
471+
expect(stream.write.mock.calls.length).toBe(1);
472+
const request = stream.write.mock.calls[0][0];
473+
474+
const payload = common.Payload.deserializeBinary(request.getPayload_asU8());
475+
const header = assertDefined(payload.getHeader(), 'header');
476+
const channelHeader = common.ChannelHeader.deserializeBinary(header.getChannelHeader_asU8());
477+
expect(channelHeader.getTlsCertHash_asU8()).toEqual(tlsClientCertificateHash);
478+
});
462479
});
463480
});
464481
});

node/src/blockeventsbuilder.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface BlockEventsBuilderOptions extends EventsOptions {
3737
client: GatewayClient;
3838
signingIdentity: SigningIdentity;
3939
channelName: string;
40+
tlsCertificateHash?: Uint8Array;
4041
}
4142

4243
export class BlockEventsBuilder {
@@ -133,6 +134,9 @@ class BlockEventsEnvelopeBuilder {
133134
result.setEpoch(0);
134135
result.setTimestamp(Timestamp.fromDate(new Date()));
135136
result.setType(common.HeaderType.DELIVER_SEEK_INFO);
137+
if (this.#options.tlsCertificateHash) {
138+
result.setTlsCertHash(this.#options.tlsCertificateHash);
139+
}
136140
return result;
137141
}
138142

0 commit comments

Comments
 (0)