Allow use of the new CDSH service in staging.

This commit is contained in:
Greyson Parrelli
2021-09-27 16:28:24 -04:00
committed by Cody Henthorne
parent e72be42eff
commit cc99febe32
13 changed files with 353 additions and 17 deletions

View File

@@ -38,6 +38,7 @@ import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.services.CdshService;
import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageModels;
@@ -97,6 +98,8 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import io.reactivex.rxjava3.core.Single;
import static org.whispersystems.signalservice.internal.push.ProvisioningProtos.ProvisionMessage;
import static org.whispersystems.signalservice.internal.push.ProvisioningProtos.ProvisioningVersion;
@@ -110,10 +113,11 @@ public class SignalServiceAccountManager {
private static final String TAG = SignalServiceAccountManager.class.getSimpleName();
private final PushServiceSocket pushServiceSocket;
private final CredentialsProvider credentials;
private final String userAgent;
private final GroupsV2Operations groupsV2Operations;
private final PushServiceSocket pushServiceSocket;
private final CredentialsProvider credentials;
private final String userAgent;
private final GroupsV2Operations groupsV2Operations;
private final SignalServiceConfiguration configuration;
/**
* Construct a SignalServiceAccountManager.
@@ -145,6 +149,7 @@ public class SignalServiceAccountManager {
this.pushServiceSocket = new PushServiceSocket(configuration, credentialsProvider, signalAgent, groupsV2Operations.getProfileOperations(), automaticNetworkRetry);
this.credentials = credentialsProvider;
this.userAgent = signalAgent;
this.configuration = configuration;
}
public byte[] getSenderCertificate() throws IOException {
@@ -437,6 +442,7 @@ public class SignalServiceAccountManager {
return activeTokens;
}
@SuppressWarnings("SameParameterValue")
public Map<String, UUID> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String mrenclave)
throws IOException, Quote.InvalidQuoteFormatException, UnauthenticatedQuoteException, SignatureException, UnauthenticatedResponseException, InvalidKeyException
{
@@ -481,6 +487,31 @@ public class SignalServiceAccountManager {
}
}
public Map<String, UUID> getRegisteredUsersWithCdsh(Set<String> e164numbers, String hexPublicKey, String hexCodeHash)
throws IOException
{
CdshService service = new CdshService(configuration, hexPublicKey, hexCodeHash);
Single<ServiceResponse<Map<String, UUID>>> result = service.getRegisteredUsers(e164numbers);
ServiceResponse<Map<String, UUID>> response;
try {
response = result.blockingGet();
} catch (Exception e) {
throw new RuntimeException("Unexpected exception when retrieving registered users!", e);
}
if (response.getResult().isPresent()) {
return response.getResult().get();
} else if (response.getApplicationError().isPresent()) {
throw new IOException(response.getApplicationError().get());
} else if (response.getExecutionError().isPresent()) {
throw new IOException(response.getExecutionError().get());
} else {
throw new IOException("Missing result!");
}
}
public Optional<SignalStorageManifest> getStorageManifest(StorageKey storageKey) throws IOException {
try {
String authToken = this.pushServiceSocket.getStorageAuth();

View File

@@ -0,0 +1,184 @@
package org.whispersystems.signalservice.api.services;
import org.signal.libsignal.hsmenclave.HsmEnclaveClient;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.api.util.Tls12SocketFactory;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import io.reactivex.rxjava3.annotations.NonNull;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.core.SingleEmitter;
import io.reactivex.rxjava3.core.SingleOnSubscribe;
import io.reactivex.rxjava3.subjects.PublishSubject;
import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
/**
* Handles network interactions with CDSH, the HSM-backed CDS service.
*/
public final class CdshService {
private static final String TAG = CdshService.class.getSimpleName();
private static final int VERSION = 1;
private final OkHttpClient client;
private final HsmEnclaveClient enclave;
private final String baseUrl;
private final String hexPublicKey;
private final String hexCodeHash;
public CdshService(SignalServiceConfiguration configuration, String hexPublicKey, String hexCodeHash) {
this.baseUrl = configuration.getSignalCdshUrls()[0].getUrl();
this.hexPublicKey = hexPublicKey;
this.hexCodeHash = hexCodeHash;
Pair<SSLSocketFactory, X509TrustManager> socketFactory = createTlsSocketFactory(configuration.getSignalCdshUrls()[0].getTrustStore());
this.client = new OkHttpClient.Builder().sslSocketFactory(new Tls12SocketFactory(socketFactory.first()),
socketFactory.second())
.connectionSpecs(Util.immutableList(ConnectionSpec.RESTRICTED_TLS))
.readTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.build();
try {
this.enclave = new HsmEnclaveClient(Hex.fromStringCondensed(hexPublicKey),
Collections.singletonList(Hex.fromStringCondensed(hexCodeHash)));
} catch (IOException e) {
throw new IllegalArgumentException("Badly-formatted public key or code hash!", e);
}
}
public Single<ServiceResponse<Map<String, UUID>>> getRegisteredUsers(Set<String> e164Numbers) {
return Single.create(emitter -> {
AtomicReference<Stage> stage = new AtomicReference<>(Stage.WAITING_TO_INITIALIZE);
List<String> addressBook = e164Numbers.stream().map(e -> e.substring(1)).collect(Collectors.toList());
String url = String.format("%s/discovery/%s/%s", baseUrl, hexPublicKey, hexCodeHash);
Request request = new Request.Builder().url(url).build();
WebSocket webSocket = client.newWebSocket(request, new WebSocketListener() {
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
switch (stage.get()) {
case WAITING_TO_INITIALIZE:
enclave.completeHandshake(bytes.toByteArray());
byte[] request = enclave.establishedSend(buildPlaintextRequest(addressBook));
stage.set(Stage.WAITING_FOR_RESPONSE);
webSocket.send(ByteString.of(request));
break;
case WAITING_FOR_RESPONSE:
byte[] response = enclave.establishedRecv(bytes.toByteArray());
try {
Map<String, UUID> out = parseResponse(addressBook, response);
emitter.onSuccess(ServiceResponse.forResult(out, 200, null));
} catch (IOException e) {
emitter.onSuccess(ServiceResponse.forUnknownError(e));
} finally {
webSocket.close(1000, "OK");
}
break;
case FAILURE:
Log.w(TAG, "Received a message after we entered the failure state! Ignoring.");
webSocket.close(1000, "OK");
break;
}
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
emitter.onSuccess(ServiceResponse.forApplicationError(t, response != null ? response.code() : 0, null));
stage.set(Stage.FAILURE);
webSocket.close(1000, "OK");
}
});
webSocket.send(ByteString.of(enclave.initialRequest()));
});
}
private static byte[] buildPlaintextRequest(List<String> addressBook) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
outputStream.write(VERSION);
for (String e164 : addressBook) {
outputStream.write(ByteUtil.longToByteArray(Long.parseLong(e164)));
}
return outputStream.toByteArray();
} catch (IOException e) {
throw new AssertionError("Failed to write bytes to the output stream?");
}
}
private static Map<String, UUID> parseResponse(List<String> addressBook, byte[] plaintextResponse) throws IOException {
Map<String, UUID> results = new HashMap<>();
try (DataInputStream uuidInputStream = new DataInputStream(new ByteArrayInputStream(plaintextResponse))) {
for (String candidate : addressBook) {
long candidateUuidHigh = uuidInputStream.readLong();
long candidateUuidLow = uuidInputStream.readLong();
if (candidateUuidHigh != 0 || candidateUuidLow != 0) {
results.put('+' + candidate, new UUID(candidateUuidHigh, candidateUuidLow));
}
}
}
return results;
}
private static Pair<SSLSocketFactory, X509TrustManager> createTlsSocketFactory(TrustStore trustStore) {
try {
SSLContext context = SSLContext.getInstance("TLS");
TrustManager[] trustManagers = BlacklistingTrustManager.createFor(trustStore);
context.init(null, trustManagers, null);
return new Pair<>(context.getSocketFactory(), (X509TrustManager) trustManagers[0]);
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new AssertionError(e);
}
}
private enum Stage {
WAITING_TO_INITIALIZE, WAITING_FOR_RESPONSE, FAILURE
}
}

View File

@@ -0,0 +1,17 @@
package org.whispersystems.signalservice.internal.configuration;
import org.whispersystems.signalservice.api.push.TrustStore;
import okhttp3.ConnectionSpec;
public class SignalCdshUrl extends SignalUrl {
public SignalCdshUrl(String url, TrustStore trustStore) {
super(url, trustStore);
}
public SignalCdshUrl(String url, String hostHeader, TrustStore trustStore, ConnectionSpec connectionSpec) {
super(url, hostHeader, trustStore, connectionSpec);
}
}

View File

@@ -13,6 +13,7 @@ public final class SignalServiceConfiguration {
private final SignalServiceUrl[] signalServiceUrls;
private final Map<Integer, SignalCdnUrl[]> signalCdnUrlMap;
private final SignalContactDiscoveryUrl[] signalContactDiscoveryUrls;
private final SignalCdshUrl[] signalCdshUrls;
private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls;
private final SignalStorageUrl[] signalStorageUrls;
private final List<Interceptor> networkInterceptors;
@@ -25,6 +26,7 @@ public final class SignalServiceConfiguration {
SignalContactDiscoveryUrl[] signalContactDiscoveryUrls,
SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls,
SignalStorageUrl[] signalStorageUrls,
SignalCdshUrl[] signalCdshUrls,
List<Interceptor> networkInterceptors,
Optional<Dns> dns,
Optional<SignalProxy> proxy,
@@ -33,6 +35,7 @@ public final class SignalServiceConfiguration {
this.signalServiceUrls = signalServiceUrls;
this.signalCdnUrlMap = signalCdnUrlMap;
this.signalContactDiscoveryUrls = signalContactDiscoveryUrls;
this.signalCdshUrls = signalCdshUrls;
this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls;
this.signalStorageUrls = signalStorageUrls;
this.networkInterceptors = networkInterceptors;
@@ -53,6 +56,10 @@ public final class SignalServiceConfiguration {
return signalContactDiscoveryUrls;
}
public SignalCdshUrl[] getSignalCdshUrls() {
return signalCdshUrls;
}
public SignalKeyBackupServiceUrl[] getSignalKeyBackupServiceUrls() {
return signalKeyBackupServiceUrls;
}

View File

@@ -36,7 +36,7 @@ dependencyVerification {
['org.threeten:threetenbp:1.3.6',
'f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7'],
['org.whispersystems:signal-client-java:0.9.4',
'629fb84dbbf5663cbfda0cb87420b0f64ad9902088c575478b04009cce9cbf8a'],
['org.whispersystems:signal-client-java:0.9.5',
'f00784bf49e75744e1d6c128136a8849b42551b2075cc392f7fe237793a13f7d'],
]
}