Introduce a Noise-over-WebSocket client connection manager

This commit is contained in:
Jon Chambers
2024-03-22 15:20:55 -04:00
committed by GitHub
parent 075a08884b
commit aec6ac019f
53 changed files with 1818 additions and 933 deletions

View File

@@ -0,0 +1,77 @@
package org.whispersystems.textsecuregcm.auth.grpc;
import static org.mockito.Mockito.mock;
import io.grpc.ManagedChannel;
import io.grpc.Server;
import io.grpc.netty.NettyChannelBuilder;
import io.grpc.netty.NettyServerBuilder;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.local.LocalServerChannel;
import java.io.IOException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.signal.chat.rpc.GetAuthenticatedDeviceRequest;
import org.signal.chat.rpc.GetAuthenticatedDeviceResponse;
import org.signal.chat.rpc.RequestAttributesGrpc;
import org.whispersystems.textsecuregcm.grpc.RequestAttributesServiceImpl;
import org.whispersystems.textsecuregcm.grpc.net.ClientConnectionManager;
abstract class AbstractAuthenticationInterceptorTest {
private static DefaultEventLoopGroup eventLoopGroup;
private ClientConnectionManager clientConnectionManager;
private Server server;
private ManagedChannel managedChannel;
@BeforeAll
static void setUpBeforeAll() {
eventLoopGroup = new DefaultEventLoopGroup();
}
@BeforeEach
void setUp() throws IOException {
final LocalAddress serverAddress = new LocalAddress("test-authentication-interceptor-server");
clientConnectionManager = mock(ClientConnectionManager.class);
// `RequestAttributesInterceptor` operates on `LocalAddresses`, so we need to do some slightly fancy plumbing to make
// sure that we're using local channels and addresses
server = NettyServerBuilder.forAddress(serverAddress)
.channelType(LocalServerChannel.class)
.bossEventLoopGroup(eventLoopGroup)
.workerEventLoopGroup(eventLoopGroup)
.intercept(getInterceptor())
.addService(new RequestAttributesServiceImpl())
.build()
.start();
managedChannel = NettyChannelBuilder.forAddress(serverAddress)
.channelType(LocalChannel.class)
.eventLoopGroup(eventLoopGroup)
.usePlaintext()
.build();
}
@AfterEach
void tearDown() {
managedChannel.shutdown();
server.shutdown();
}
protected abstract AbstractAuthenticationInterceptor getInterceptor();
protected ClientConnectionManager getClientConnectionManager() {
return clientConnectionManager;
}
protected GetAuthenticatedDeviceResponse getAuthenticatedDevice() {
return RequestAttributesGrpc.newBlockingStub(managedChannel)
.getAuthenticatedDevice(GetAuthenticatedDeviceRequest.newBuilder().build());
}
}

View File

@@ -1,135 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth.grpc;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import io.grpc.CallCredentials;
import io.grpc.ManagedChannel;
import io.grpc.Metadata;
import io.grpc.Server;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.stream.Stream;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.signal.chat.rpc.EchoRequest;
import org.signal.chat.rpc.EchoServiceGrpc;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.grpc.EchoServiceImpl;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.HeaderUtils;
import org.whispersystems.textsecuregcm.util.Pair;
class BasicCredentialAuthenticationInterceptorTest {
private Server server;
private ManagedChannel managedChannel;
private AccountAuthenticator accountAuthenticator;
@BeforeEach
void setUp() throws IOException {
accountAuthenticator = mock(AccountAuthenticator.class);
final BasicCredentialAuthenticationInterceptor authenticationInterceptor =
new BasicCredentialAuthenticationInterceptor(accountAuthenticator);
final String serverName = InProcessServerBuilder.generateName();
server = InProcessServerBuilder.forName(serverName)
.directExecutor()
.intercept(authenticationInterceptor)
.addService(new EchoServiceImpl())
.build()
.start();
managedChannel = InProcessChannelBuilder.forName(serverName)
.directExecutor()
.build();
}
@AfterEach
void tearDown() {
managedChannel.shutdown();
server.shutdown();
}
@ParameterizedTest
@MethodSource
void interceptCall(final Metadata headers, final boolean acceptCredentials, final boolean expectAuthentication) {
if (acceptCredentials) {
final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(UUID.randomUUID());
final Device device = mock(Device.class);
when(device.getId()).thenReturn(Device.PRIMARY_ID);
when(accountAuthenticator.authenticate(any()))
.thenReturn(Optional.of(new AuthenticatedAccount(account, device)));
} else {
when(accountAuthenticator.authenticate(any()))
.thenReturn(Optional.empty());
}
final EchoServiceGrpc.EchoServiceBlockingStub stub = EchoServiceGrpc.newBlockingStub(managedChannel)
.withCallCredentials(new CallCredentials() {
@Override
public void applyRequestMetadata(final RequestInfo requestInfo, final Executor appExecutor, final MetadataApplier applier) {
applier.apply(headers);
}
@Override
public void thisUsesUnstableApi() {
}
});
if (expectAuthentication) {
assertDoesNotThrow(() -> stub.echo(EchoRequest.newBuilder().build()));
} else {
final StatusRuntimeException exception =
assertThrows(StatusRuntimeException.class, () -> stub.echo(EchoRequest.newBuilder().build()));
assertEquals(Status.UNAUTHENTICATED.getCode(), exception.getStatus().getCode());
}
}
private static Stream<Arguments> interceptCall() {
final Metadata malformedCredentialHeaders = new Metadata();
malformedCredentialHeaders.put(BasicCredentialAuthenticationInterceptor.BASIC_CREDENTIALS, "Incorrect");
final Metadata structurallyValidCredentialHeaders = new Metadata();
structurallyValidCredentialHeaders.put(
BasicCredentialAuthenticationInterceptor.BASIC_CREDENTIALS,
HeaderUtils.basicAuthHeader(UUID.randomUUID().toString(), RandomStringUtils.randomAlphanumeric(16))
);
return Stream.of(
Arguments.of(new Metadata(), true, false),
Arguments.of(malformedCredentialHeaders, true, false),
Arguments.of(structurallyValidCredentialHeaders, false, false),
Arguments.of(structurallyValidCredentialHeaders, true, true)
);
}
}

View File

@@ -13,15 +13,14 @@ import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import java.util.UUID;
import javax.annotation.Nullable;
import org.whispersystems.textsecuregcm.util.Pair;
public class MockAuthenticationInterceptor implements ServerInterceptor {
@Nullable
private Pair<UUID, Byte> authenticatedDevice;
private AuthenticatedDevice authenticatedDevice;
public void setAuthenticatedDevice(final UUID accountIdentifier, final byte deviceId) {
authenticatedDevice = new Pair<>(accountIdentifier, deviceId);
authenticatedDevice = new AuthenticatedDevice(accountIdentifier, deviceId);
}
public void clearAuthenticatedDevice() {
@@ -33,14 +32,10 @@ public class MockAuthenticationInterceptor implements ServerInterceptor {
final Metadata headers,
final ServerCallHandler<ReqT, RespT> next) {
if (authenticatedDevice != null) {
final Context context = Context.current()
.withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY, authenticatedDevice.first())
.withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY, authenticatedDevice.second());
return Contexts.interceptCall(context, call, headers, next);
}
return next.startCall(call, headers);
return authenticatedDevice != null
? Contexts.interceptCall(
Context.current().withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_DEVICE, authenticatedDevice),
call, headers, next)
: next.startCall(call, headers);
}
}

View File

@@ -0,0 +1,40 @@
package org.whispersystems.textsecuregcm.auth.grpc;
import io.grpc.Status;
import org.junit.jupiter.api.Test;
import org.signal.chat.rpc.GetAuthenticatedDeviceResponse;
import org.whispersystems.textsecuregcm.grpc.GrpcTestUtils;
import org.whispersystems.textsecuregcm.grpc.net.ClientConnectionManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
class ProhibitAuthenticationInterceptorTest extends AbstractAuthenticationInterceptorTest {
@Override
protected AbstractAuthenticationInterceptor getInterceptor() {
return new ProhibitAuthenticationInterceptor(getClientConnectionManager());
}
@Test
void interceptCall() {
final ClientConnectionManager clientConnectionManager = getClientConnectionManager();
when(clientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.empty());
final GetAuthenticatedDeviceResponse response = getAuthenticatedDevice();
assertTrue(response.getAccountIdentifier().isEmpty());
assertEquals(0, response.getDeviceId());
final AuthenticatedDevice authenticatedDevice = new AuthenticatedDevice(UUID.randomUUID(), Device.PRIMARY_ID);
when(clientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.of(authenticatedDevice));
GrpcTestUtils.assertStatusException(Status.UNAUTHENTICATED, this::getAuthenticatedDevice);
}
}

View File

@@ -0,0 +1,39 @@
package org.whispersystems.textsecuregcm.auth.grpc;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import io.grpc.Status;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.signal.chat.rpc.GetAuthenticatedDeviceResponse;
import org.whispersystems.textsecuregcm.grpc.GrpcTestUtils;
import org.whispersystems.textsecuregcm.grpc.net.ClientConnectionManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
class RequireAuthenticationInterceptorTest extends AbstractAuthenticationInterceptorTest {
@Override
protected AbstractAuthenticationInterceptor getInterceptor() {
return new RequireAuthenticationInterceptor(getClientConnectionManager());
}
@Test
void interceptCall() {
final ClientConnectionManager clientConnectionManager = getClientConnectionManager();
when(clientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.empty());
GrpcTestUtils.assertStatusException(Status.UNAUTHENTICATED, this::getAuthenticatedDevice);
final AuthenticatedDevice authenticatedDevice = new AuthenticatedDevice(UUID.randomUUID(), Device.PRIMARY_ID);
when(clientConnectionManager.getAuthenticatedDevice(any())).thenReturn(Optional.of(authenticatedDevice));
final GetAuthenticatedDeviceResponse response = getAuthenticatedDevice();
assertEquals(UUIDUtil.toByteString(authenticatedDevice.accountIdentifier()), response.getAccountIdentifier());
assertEquals(authenticatedDevice.deviceId(), response.getDeviceId());
}
}