Skip to content

Commit 542422b

Browse files
committed
Replace XX/NX handshakes with IK/NK
1 parent c835d85 commit 542422b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1901
-1610
lines changed

service/config/sample.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,6 @@ noiseTunnel:
478478
tlsKeyStoreEntryAlias: example.com
479479
tlsKeyStorePassword: secret://noiseTunnel.tlsKeyStorePassword
480480
noiseStaticPrivateKey: secret://noiseTunnel.noiseStaticPrivateKey
481-
noiseRootPublicKeySignature: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
482481
recognizedProxySecret: secret://noiseTunnel.recognizedProxySecret
483482

484483
externalRequestFilter:

service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,7 +933,6 @@ protected void configureServer(final ServerBuilder<?> serverBuilder) {
933933
clientConnectionManager,
934934
clientPublicKeysManager,
935935
config.getNoiseWebSocketTunnelConfiguration().noiseStaticKeyPair(),
936-
config.getNoiseWebSocketTunnelConfiguration().noiseRootPublicKeySignature(),
937936
authenticatedGrpcServerAddress,
938937
anonymousGrpcServerAddress,
939938
config.getNoiseWebSocketTunnelConfiguration().recognizedProxySecret().value());

service/src/main/java/org/whispersystems/textsecuregcm/configuration/NoiseWebSocketTunnelConfiguration.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ public record NoiseWebSocketTunnelConfiguration(@Positive int port,
1515
@Nullable String tlsKeyStoreEntryAlias,
1616
@Nullable SecretString tlsKeyStorePassword,
1717
@NotNull SecretBytes noiseStaticPrivateKey,
18-
@NotNull byte[] noiseRootPublicKeySignature,
1918
@NotNull SecretString recognizedProxySecret) {
2019

2120
public ECKeyPair noiseStaticKeyPair() throws InvalidKeyException {

service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/AbstractNoiseHandshakeHandler.java

Lines changed: 0 additions & 124 deletions
This file was deleted.

service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/ErrorHandler.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import javax.crypto.BadPaddingException;
1010
import org.slf4j.Logger;
1111
import org.slf4j.LoggerFactory;
12+
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
1213

1314
/**
1415
* An error handler serves as a general backstop for exceptions elsewhere in the pipeline. If the client has completed a
@@ -38,7 +39,7 @@ protected void setWebsocketHandshakeComplete() {
3839
@Override
3940
public void exceptionCaught(final ChannelHandlerContext context, final Throwable cause) {
4041
if (websocketHandshakeComplete) {
41-
final WebSocketCloseStatus webSocketCloseStatus = switch (cause) {
42+
final WebSocketCloseStatus webSocketCloseStatus = switch (ExceptionUtils.unwrap(cause)) {
4243
case NoiseHandshakeException e -> ApplicationWebSocketCloseReason.NOISE_HANDSHAKE_ERROR.toWebSocketCloseStatus(e.getMessage());
4344
case ClientAuthenticationException ignored -> ApplicationWebSocketCloseReason.CLIENT_AUTHENTICATION_ERROR.toWebSocketCloseStatus("Not authenticated");
4445
case BadPaddingException ignored -> ApplicationWebSocketCloseReason.NOISE_ENCRYPTION_ERROR.toWebSocketCloseStatus("Noise encryption error");
@@ -51,6 +52,7 @@ public void exceptionCaught(final ChannelHandlerContext context, final Throwable
5152
context.writeAndFlush(new CloseWebSocketFrame(webSocketCloseStatus))
5253
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
5354
} else {
55+
log.debug("Error occurred before websocket handshake complete", cause);
5456
// We haven't completed a websocket handshake, so we can't really communicate errors in a semantically-meaningful
5557
// way; just close the connection instead.
5658
context.close();

service/src/main/java/org/whispersystems/textsecuregcm/grpc/net/EstablishLocalGrpcConnectionHandler.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,12 @@ public void channelRead(final ChannelHandlerContext context, final Object messag
4848

4949
@Override
5050
public void userEventTriggered(final ChannelHandlerContext remoteChannelContext, final Object event) {
51-
if (event instanceof NoiseHandshakeCompleteEvent noiseHandshakeCompleteEvent) {
51+
if (event instanceof NoiseIdentityDeterminedEvent noiseIdentityDeterminedEvent) {
5252
// We assume that we'll only get a completed handshake event if the handshake met all authentication requirements
5353
// for the requested service. If the handshake doesn't have an authenticated device, we assume we're trying to
5454
// connect to the anonymous service. If it does have an authenticated device, we assume we're aiming for the
5555
// authenticated service.
56-
final LocalAddress grpcServerAddress = noiseHandshakeCompleteEvent.authenticatedDevice().isPresent()
56+
final LocalAddress grpcServerAddress = noiseIdentityDeterminedEvent.authenticatedDevice().isPresent()
5757
? authenticatedGrpcServerAddress
5858
: anonymousGrpcServerAddress;
5959

@@ -72,7 +72,7 @@ protected void initChannel(final LocalChannel localChannel) {
7272
if (localChannelFuture.isSuccess()) {
7373
clientConnectionManager.handleConnectionEstablished((LocalChannel) localChannelFuture.channel(),
7474
remoteChannelContext.channel(),
75-
noiseHandshakeCompleteEvent.authenticatedDevice());
75+
noiseIdentityDeterminedEvent.authenticatedDevice());
7676

7777
// Close the local connection if the remote channel closes and vice versa
7878
remoteChannelContext.channel().closeFuture().addListener(closeFuture -> localChannelFuture.channel().close());
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2024 Signal Messenger, LLC
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
package org.whispersystems.textsecuregcm.grpc.net;
6+
7+
enum HandshakePattern {
8+
NK("Noise_NK_25519_ChaChaPoly_BLAKE2b"),
9+
IK("Noise_IK_25519_ChaChaPoly_BLAKE2b");
10+
11+
private final String protocol;
12+
13+
public String protocol() {
14+
return protocol;
15+
}
16+
17+
18+
HandshakePattern(String protocol) {
19+
this.protocol = protocol;
20+
}
21+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.whispersystems.textsecuregcm.grpc.net;
2+
3+
import io.netty.buffer.ByteBuf;
4+
import io.netty.channel.ChannelHandlerContext;
5+
import java.util.Optional;
6+
import java.util.concurrent.CompletableFuture;
7+
import org.signal.libsignal.protocol.ecc.ECKeyPair;
8+
9+
/**
10+
* A NoiseAnonymousHandler is a netty pipeline element that handles the responder side of an unauthenticated handshake
11+
* and noise encryption/decryption.
12+
* <p>
13+
* A noise NK handshake must be used for unauthenticated connections. Optionally, the initiator can also include an
14+
* initial request in their payload. If provided, this allows the server to begin processing the request without an
15+
* initial message delay (fast open).
16+
* <p>
17+
* Once the handler receives the handshake initiator message, it will fire a {@link NoiseIdentityDeterminedEvent}
18+
* indicating that initiator connected anonymously.
19+
*/
20+
class NoiseAnonymousHandler extends NoiseHandler {
21+
22+
public NoiseAnonymousHandler(final ECKeyPair ecKeyPair) {
23+
super(new NoiseHandshakeHelper(HandshakePattern.NK, ecKeyPair));
24+
}
25+
26+
@Override
27+
CompletableFuture<HandshakeResult> handleHandshakePayload(final ChannelHandlerContext context,
28+
final Optional<byte[]> initiatorPublicKey, final ByteBuf handshakePayload) {
29+
return CompletableFuture.completedFuture(new HandshakeResult(
30+
handshakePayload,
31+
Optional.empty()
32+
));
33+
}
34+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package org.whispersystems.textsecuregcm.grpc.net;
2+
3+
import io.netty.buffer.ByteBuf;
4+
import io.netty.channel.ChannelHandlerContext;
5+
import io.netty.util.ReferenceCountUtil;
6+
import java.security.MessageDigest;
7+
import java.util.Optional;
8+
import java.util.UUID;
9+
import java.util.concurrent.CompletableFuture;
10+
import org.signal.libsignal.protocol.ecc.ECKeyPair;
11+
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
12+
import org.whispersystems.textsecuregcm.storage.ClientPublicKeysManager;
13+
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
14+
15+
/**
16+
* A NoiseAuthenticatedHandler is a netty pipeline element that handles the responder side of an authenticated handshake
17+
* and noise encryption/decryption. Authenticated handshakes are noise IK handshakes where the initiator's static public
18+
* key is authenticated by the responder.
19+
* <p>
20+
* The authenticated handshake requires the initiator to provide a payload with their first handshake message that
21+
* includes their account identifier and device id in network byte-order. Optionally, the initiator can also include an
22+
* initial request in their payload. If provided, this allows the server to begin processing the request without an
23+
* initial message delay (fast open).
24+
* <pre>
25+
* +-----------------+----------------+------------------------+
26+
* | UUID (16) | deviceId (1) | request bytes (N) |
27+
* +-----------------+----------------+------------------------+
28+
* </pre>
29+
* <p>
30+
* For a successful handshake, the static key provided in the handshake message must match the server's stored public
31+
* key for the device identified by the provided ACI and deviceId.
32+
* <p>
33+
* As soon as the handler authenticates the caller, it will fire a {@link NoiseIdentityDeterminedEvent}.
34+
*/
35+
class NoiseAuthenticatedHandler extends NoiseHandler {
36+
37+
private final ClientPublicKeysManager clientPublicKeysManager;
38+
39+
NoiseAuthenticatedHandler(final ClientPublicKeysManager clientPublicKeysManager,
40+
final ECKeyPair ecKeyPair) {
41+
super(new NoiseHandshakeHelper(HandshakePattern.IK, ecKeyPair));
42+
this.clientPublicKeysManager = clientPublicKeysManager;
43+
}
44+
45+
@Override
46+
CompletableFuture<HandshakeResult> handleHandshakePayload(
47+
final ChannelHandlerContext context,
48+
final Optional<byte[]> initiatorPublicKey,
49+
final ByteBuf handshakePayload) throws NoiseHandshakeException {
50+
if (handshakePayload.readableBytes() < 17) {
51+
throw new NoiseHandshakeException("Invalid handshake payload");
52+
}
53+
54+
final byte[] publicKeyFromClient = initiatorPublicKey
55+
.orElseThrow(() -> new IllegalStateException("No remote public key"));
56+
57+
// Advances the read index by 16 bytes
58+
final UUID accountIdentifier = parseUUID(handshakePayload);
59+
60+
// Advances the read index by 1 byte
61+
final byte deviceId = handshakePayload.readByte();
62+
63+
final ByteBuf fastOpenRequest = handshakePayload.slice();
64+
return clientPublicKeysManager
65+
.findPublicKey(accountIdentifier, deviceId)
66+
.handleAsync((storedPublicKey, throwable) -> {
67+
if (throwable != null) {
68+
ReferenceCountUtil.release(fastOpenRequest);
69+
throw ExceptionUtils.wrap(throwable);
70+
}
71+
final boolean valid = storedPublicKey
72+
.map(spk -> MessageDigest.isEqual(publicKeyFromClient, spk.getPublicKeyBytes()))
73+
.orElse(false);
74+
if (!valid) {
75+
throw ExceptionUtils.wrap(new ClientAuthenticationException());
76+
}
77+
return new HandshakeResult(
78+
fastOpenRequest,
79+
Optional.of(new AuthenticatedDevice(accountIdentifier, deviceId)));
80+
}, context.executor());
81+
}
82+
83+
/**
84+
* Parse a {@link UUID} out of bytes, advancing the readerIdx by 16
85+
*
86+
* @param bytes The {@link ByteBuf} to read from
87+
* @return The parsed UUID
88+
* @throws NoiseHandshakeException If a UUID could not be parsed from bytes
89+
*/
90+
private UUID parseUUID(final ByteBuf bytes) throws NoiseHandshakeException {
91+
if (bytes.readableBytes() < 16) {
92+
throw new NoiseHandshakeException("Could not parse account identifier");
93+
}
94+
return new UUID(bytes.readLong(), bytes.readLong());
95+
}
96+
}

0 commit comments

Comments
 (0)