Implement new CDS changes.

This commit is contained in:
Greyson Parrelli
2020-07-02 10:38:52 -07:00
parent 1752972be9
commit 2791790bf5
22 changed files with 908 additions and 561 deletions

View File

@@ -73,6 +73,8 @@ import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.util.Base64;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
@@ -363,29 +365,38 @@ public class SignalServiceAccountManager {
return activeTokens;
}
public List<String> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String enclaveId)
public Map<String, UUID> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String mrenclave)
throws IOException, Quote.InvalidQuoteFormatException, UnauthenticatedQuoteException, SignatureException, UnauthenticatedResponseException
{
try {
String authorization = pushServiceSocket.getContactDiscoveryAuthorization();
RemoteAttestation remoteAttestation = RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.ContactDiscovery, iasKeyStore, enclaveId, enclaveId, authorization);
List<String> addressBook = new LinkedList<>();
String authorization = this.pushServiceSocket.getContactDiscoveryAuthorization();
Map<String, RemoteAttestation> attestations = RemoteAttestationUtil.getAndVerifyMultiRemoteAttestation(pushServiceSocket,
PushServiceSocket.ClientSet.ContactDiscovery,
iasKeyStore,
mrenclave,
mrenclave,
authorization);
List<String> addressBook = new ArrayList<>(e164numbers.size());
for (String e164number : e164numbers) {
addressBook.add(e164number.substring(1));
}
DiscoveryRequest request = ContactDiscoveryCipher.createDiscoveryRequest(addressBook, remoteAttestation);
DiscoveryResponse response = pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, remoteAttestation.getCookies(), enclaveId);
byte[] data = ContactDiscoveryCipher.getDiscoveryResponseData(response, remoteAttestation);
List<String> cookies = attestations.values().iterator().next().getCookies();
DiscoveryRequest request = ContactDiscoveryCipher.createDiscoveryRequest(addressBook, attestations);
DiscoveryResponse response = this.pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, cookies, mrenclave);
byte[] data = ContactDiscoveryCipher.getDiscoveryResponseData(response, attestations.values());
Iterator<String> addressBookIterator = addressBook.iterator();
List<String> results = new LinkedList<>();
HashMap<String, UUID> results = new HashMap<>(addressBook.size());
DataInputStream uuidInputStream = new DataInputStream(new ByteArrayInputStream(data));
for (byte aData : data) {
String candidate = addressBookIterator.next();
if (aData != 0) results.add('+' + candidate);
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;
@@ -394,38 +405,6 @@ public class SignalServiceAccountManager {
}
}
public void reportContactDiscoveryServiceMatch() {
try {
this.pushServiceSocket.reportContactDiscoveryServiceMatch();
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery result match failed. Ignoring.", e);
}
}
public void reportContactDiscoveryServiceMismatch() {
try {
this.pushServiceSocket.reportContactDiscoveryServiceMismatch();
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery result mismatch failed. Ignoring.", e);
}
}
public void reportContactDiscoveryServiceAttestationError(String reason) {
try {
this.pushServiceSocket.reportContactDiscoveryServiceAttestationError(reason);
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery attestation error failed. Ignoring.", e);
}
}
public void reportContactDiscoveryServiceUnexpectedError(String reason) {
try {
this.pushServiceSocket.reportContactDiscoveryServiceUnexpectedError(reason);
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery unexpected error failed. Ignoring.", e);
}
}
public Optional<SignalStorageManifest> getStorageManifest(StorageKey storageKey) throws IOException {
try {
String authToken = this.pushServiceSocket.getStorageAuth();

View File

@@ -1,6 +1,7 @@
package org.whispersystems.signalservice.api.crypto;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
@@ -22,4 +23,13 @@ public final class CryptoUtil {
throw new AssertionError(e);
}
}
public static byte[] sha256(byte[] data) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(data);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -39,7 +39,9 @@ final class AESCipher {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, iv));
cipher.updateAAD(aad);
if (aad != null) {
cipher.updateAAD(aad);
}
byte[] cipherText = cipher.doFinal(requestData);
byte[][] parts = ByteUtil.split(cipherText, cipherText.length - TAG_LENGTH_BYTES, TAG_LENGTH_BYTES);

View File

@@ -1,40 +1,81 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.api.crypto.CryptoUtil;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.internal.contacts.crypto.AESCipher.AESEncryptedResult;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
import org.whispersystems.signalservice.internal.contacts.entities.QueryEnvelope;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class ContactDiscoveryCipher {
private ContactDiscoveryCipher() {
}
public static DiscoveryRequest createDiscoveryRequest(List<String> addressBook, RemoteAttestation remoteAttestation) {
public static DiscoveryRequest createDiscoveryRequest(List<String> addressBook, Map<String, RemoteAttestation> remoteAttestations) {
byte[] queryDataKey = Util.getSecretBytes(32);
byte[] queryData = buildQueryData(addressBook);
AESEncryptedResult encryptedQueryData = AESCipher.encrypt(queryDataKey, null, queryData);
byte[] commitment = CryptoUtil.sha256(queryData);
Map<String, QueryEnvelope> envelopes = new HashMap<>(remoteAttestations.size());
for (Map.Entry<String, RemoteAttestation> entry : remoteAttestations.entrySet()) {
envelopes.put(entry.getKey(),
buildQueryEnvelope(entry.getValue().getRequestId(),
entry.getValue().getKeys().getClientKey(),
queryDataKey));
}
return new DiscoveryRequest(addressBook.size(),
commitment,
encryptedQueryData.iv,
encryptedQueryData.data,
encryptedQueryData.mac,
envelopes);
}
public static byte[] getDiscoveryResponseData(DiscoveryResponse response, Collection<RemoteAttestation> attestations) throws InvalidCiphertextException, IOException {
for (RemoteAttestation attestation : attestations) {
if (Arrays.equals(response.getRequestId(), attestation.getRequestId())) {
return AESCipher.decrypt(attestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
}
}
throw new NoMatchingRequestIdException();
}
private static byte[] buildQueryData(List<String> addresses) {
try {
byte[] nonce = Util.getSecretBytes(32);
ByteArrayOutputStream requestDataStream = new ByteArrayOutputStream();
for (String address : addressBook) {
requestDataStream.write(nonce);
for (String address : addresses) {
requestDataStream.write(ByteUtil.longToByteArray(Long.parseLong(address)));
}
byte[] clientKey = remoteAttestation.getKeys().getClientKey();
byte[] requestData = requestDataStream.toByteArray();
byte[] aad = remoteAttestation.getRequestId();
AESCipher.AESEncryptedResult aesEncryptedResult = AESCipher.encrypt(clientKey, aad, requestData);
return new DiscoveryRequest(addressBook.size(), aesEncryptedResult.aad, aesEncryptedResult.iv, aesEncryptedResult.data, aesEncryptedResult.mac);
return requestDataStream.toByteArray();
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static byte[] getDiscoveryResponseData(DiscoveryResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
return AESCipher.decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
private static QueryEnvelope buildQueryEnvelope(byte[] requestId, byte[] clientKey, byte[] queryDataKey) {
AESEncryptedResult result = AESCipher.encrypt(clientKey, requestId, queryDataKey);
return new QueryEnvelope(requestId, result.iv, result.data, result.mac);
}
static class NoMatchingRequestIdException extends IOException {
}
}

View File

@@ -18,8 +18,8 @@ package org.whispersystems.signalservice.internal.contacts.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.internal.util.Hex;
import java.util.List;
import java.util.Map;
public class DiscoveryRequest {
@@ -27,7 +27,7 @@ public class DiscoveryRequest {
private int addressCount;
@JsonProperty
private byte[] requestId;
private byte[] commitment;
@JsonProperty
private byte[] iv;
@@ -38,20 +38,22 @@ public class DiscoveryRequest {
@JsonProperty
private byte[] mac;
public DiscoveryRequest() {
@JsonProperty
private Map<String, QueryEnvelope> envelopes;
}
public DiscoveryRequest() { }
public DiscoveryRequest(int addressCount, byte[] requestId, byte[] iv, byte[] data, byte[] mac) {
public DiscoveryRequest(int addressCount, byte[] commitment, byte[] iv, byte[] data, byte[] mac, Map<String, QueryEnvelope> envelopes) {
this.addressCount = addressCount;
this.requestId = requestId;
this.commitment = commitment;
this.iv = iv;
this.data = data;
this.mac = mac;
this.envelopes = envelopes;
}
public byte[] getRequestId() {
return requestId;
public byte[] getCommitment() {
return commitment;
}
public byte[] getIv() {
@@ -70,8 +72,8 @@ public class DiscoveryRequest {
return addressCount;
}
@Override
public String toString() {
return "{ addressCount: " + addressCount + ", ticket: " + Hex.toString(requestId) + ", iv: " + Hex.toString(iv) + ", data: " + Hex.toString(data) + ", mac: " + Hex.toString(mac) + "}";
return "{ addressCount: " + addressCount + ", envelopes: " + envelopes.size() + " }";
}
}

View File

@@ -22,6 +22,9 @@ import org.whispersystems.signalservice.internal.util.Hex;
public class DiscoveryResponse {
@JsonProperty
private byte[] requestId;
@JsonProperty
private byte[] iv;
@@ -33,10 +36,8 @@ public class DiscoveryResponse {
public DiscoveryResponse() {}
public DiscoveryResponse(byte[] iv, byte[] data, byte[] mac) {
this.iv = iv;
this.data = data;
this.mac = mac;
public byte[] getRequestId() {
return requestId;
}
public byte[] getIv() {

View File

@@ -0,0 +1,16 @@
package org.whispersystems.signalservice.internal.contacts.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
public class MultiRemoteAttestationResponse {
@JsonProperty
private Map<String, RemoteAttestationResponse> attestations;
public Map<String, RemoteAttestationResponse> getAttestations() {
return attestations;
}
}

View File

@@ -0,0 +1,27 @@
package org.whispersystems.signalservice.internal.contacts.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class QueryEnvelope {
@JsonProperty
private byte[] requestId;
@JsonProperty
private byte[] iv;
@JsonProperty
private byte[] data;
@JsonProperty
private byte[] mac;
public QueryEnvelope() { }
public QueryEnvelope(byte[] requestId, byte[] iv, byte[] data, byte[] mac) {
this.requestId = requestId;
this.iv = iv;
this.data = data;
this.mac = mac;
}
}

View File

@@ -852,24 +852,6 @@ public class PushServiceSocket {
}
}
public void reportContactDiscoveryServiceMatch() throws IOException {
makeServiceRequest(String.format(DIRECTORY_FEEDBACK_PATH, "ok"), "PUT", "");
}
public void reportContactDiscoveryServiceMismatch() throws IOException {
makeServiceRequest(String.format(DIRECTORY_FEEDBACK_PATH, "mismatch"), "PUT", "");
}
public void reportContactDiscoveryServiceAttestationError(String reason) throws IOException {
ContactDiscoveryFailureReason failureReason = new ContactDiscoveryFailureReason(reason);
makeServiceRequest(String.format(DIRECTORY_FEEDBACK_PATH, "attestation-error"), "PUT", JsonUtil.toJson(failureReason));
}
public void reportContactDiscoveryServiceUnexpectedError(String reason) throws IOException {
ContactDiscoveryFailureReason failureReason = new ContactDiscoveryFailureReason(reason);
makeServiceRequest(String.format(DIRECTORY_FEEDBACK_PATH, "unexpected-error"), "PUT", JsonUtil.toJson(failureReason));
}
public TurnServerInfo getTurnServerInfo() throws IOException {
String response = makeServiceRequest(TURN_SERVER_INFO, "GET", null);
return JsonUtil.fromJson(response, TurnServerInfo.class);

View File

@@ -10,6 +10,7 @@ import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestati
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationCipher;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationKeys;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.entities.MultiRemoteAttestationResponse;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.util.JsonUtil;
@@ -17,8 +18,11 @@ import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.io.IOException;
import java.security.KeyStore;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import okhttp3.Response;
import okhttp3.ResponseBody;
@@ -36,33 +40,66 @@ public final class RemoteAttestationUtil {
String authorization)
throws IOException, Quote.InvalidQuoteFormatException, InvalidCiphertextException, UnauthenticatedQuoteException, SignatureException
{
Curve25519 curve = Curve25519.getInstance(Curve25519.BEST);
Curve25519KeyPair keyPair = curve.generateKeyPair();
RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey());
Pair<RemoteAttestationResponse, List<String>> attestationResponsePair = getRemoteAttestation(socket, clientSet, authorization, attestationRequest, enclaveName);
RemoteAttestationResponse attestationResponse = attestationResponsePair.first();
List<String> attestationCookies = attestationResponsePair.second();
Curve25519KeyPair keyPair = buildKeyPair();
ResponsePair result = makeAttestationRequest(socket, clientSet, authorization, enclaveName, keyPair);
RemoteAttestationResponse response = JsonUtil.fromJson(result.body, RemoteAttestationResponse.class);
RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, attestationResponse.getServerEphemeralPublic(), attestationResponse.getServerStaticPublic());
Quote quote = new Quote(attestationResponse.getQuote());
byte[] requestId = RemoteAttestationCipher.getRequestId(keys, attestationResponse);
RemoteAttestationCipher.verifyServerQuote(quote, attestationResponse.getServerStaticPublic(), mrenclave);
RemoteAttestationCipher.verifyIasSignature(iasKeyStore, attestationResponse.getCertificates(), attestationResponse.getSignatureBody(), attestationResponse.getSignature(), quote);
return new RemoteAttestation(requestId, keys, attestationCookies);
return validateAndBuildRemoteAttestation(response, result.cookies, iasKeyStore, keyPair, mrenclave);
}
private static Pair<RemoteAttestationResponse, List<String>> getRemoteAttestation(PushServiceSocket socket,
PushServiceSocket.ClientSet clientSet,
String authorization,
RemoteAttestationRequest request,
String enclaveName)
throws IOException
public static Map<String, RemoteAttestation> getAndVerifyMultiRemoteAttestation(PushServiceSocket socket,
PushServiceSocket.ClientSet clientSet,
KeyStore iasKeyStore,
String enclaveName,
String mrenclave,
String authorization)
throws IOException, Quote.InvalidQuoteFormatException, InvalidCiphertextException, UnauthenticatedQuoteException, SignatureException
{
Response response = socket.makeRequest(clientSet, authorization, new LinkedList<String>(), "/v1/attestation/" + enclaveName, "PUT", JsonUtil.toJson(request));
ResponseBody body = response.body();
Curve25519KeyPair keyPair = buildKeyPair();
ResponsePair result = makeAttestationRequest(socket, clientSet, authorization, enclaveName, keyPair);
MultiRemoteAttestationResponse response = JsonUtil.fromJson(result.body, MultiRemoteAttestationResponse.class);
Map<String, RemoteAttestation> attestations = new HashMap<>();
if (response.getAttestations().isEmpty() || response.getAttestations().size() > 3) {
throw new NonSuccessfulResponseCodeException("Incorrect number of attestations: " + response.getAttestations().size());
}
for (Map.Entry<String, RemoteAttestationResponse> entry : response.getAttestations().entrySet()) {
attestations.put(entry.getKey(),
validateAndBuildRemoteAttestation(entry.getValue(),
result.cookies,
iasKeyStore,
keyPair,
mrenclave));
}
return attestations;
}
private static Curve25519KeyPair buildKeyPair() {
Curve25519 curve = Curve25519.getInstance(Curve25519.BEST);
return curve.generateKeyPair();
}
private static ResponsePair makeAttestationRequest(PushServiceSocket socket,
PushServiceSocket.ClientSet clientSet,
String authorization,
String enclaveName,
Curve25519KeyPair keyPair)
throws IOException
{
RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey());
Response response = socket.makeRequest(clientSet, authorization, new LinkedList<String>(), "/v1/attestation/" + enclaveName, "PUT", JsonUtil.toJson(attestationRequest));
ResponseBody body = response.body();
if (body == null) {
throw new NonSuccessfulResponseCodeException("Empty response!");
}
return new ResponsePair(body.string(), parseCookies(response));
}
private static List<String> parseCookies(Response response) {
List<String> rawCookies = response.headers("Set-Cookie");
List<String> cookies = new LinkedList<>();
@@ -70,10 +107,34 @@ public final class RemoteAttestationUtil {
cookies.add(cookie.split(";")[0]);
}
if (body != null) {
return new Pair<>(JsonUtil.fromJson(body.string(), RemoteAttestationResponse.class), cookies);
} else {
throw new NonSuccessfulResponseCodeException("Empty response!");
return cookies;
}
private static RemoteAttestation validateAndBuildRemoteAttestation(RemoteAttestationResponse response,
List<String> cookies,
KeyStore iasKeyStore,
Curve25519KeyPair keyPair,
String mrenclave)
throws Quote.InvalidQuoteFormatException, InvalidCiphertextException, UnauthenticatedQuoteException, SignatureException
{
RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, response.getServerEphemeralPublic(), response.getServerStaticPublic());
Quote quote = new Quote(response.getQuote());
byte[] requestId = RemoteAttestationCipher.getRequestId(keys, response);
RemoteAttestationCipher.verifyServerQuote(quote, response.getServerStaticPublic(), mrenclave);
RemoteAttestationCipher.verifyIasSignature(iasKeyStore, response.getCertificates(), response.getSignatureBody(), response.getSignature(), quote);
return new RemoteAttestation(requestId, keys, cookies);
}
private static class ResponsePair {
final String body;
final List<String> cookies;
private ResponsePair(String body, List<String> cookies) {
this.body = body;
this.cookies = cookies;
}
}
}