Add internal pre-alpha support for Registration Lock v2.

This commit is contained in:
Alan Evans
2019-12-03 12:31:23 -05:00
committed by Greyson Parrelli
parent 058c25808b
commit 7f8ca58762
50 changed files with 2313 additions and 340 deletions

View File

@@ -35,7 +35,7 @@ dependencies {
api 'com.squareup.okhttp3:okhttp:3.12.1'
implementation 'org.threeten:threetenbp:1.3.6'
testImplementation 'junit:junit:3.8.2'
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:1.7.1'
testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.0.0'
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (C) 2019 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
package textsecure;
option java_package = "org.whispersystems.signalservice.internal.keybackup.protos";
option java_multiple_files = true;
message Request {
optional BackupRequest backup = 1;
optional RestoreRequest restore = 2;
optional DeleteRequest delete = 3;
}
message Response {
optional BackupResponse backup = 1;
optional RestoreResponse restore = 2;
optional DeleteResponse delete = 3;
}
message BackupRequest {
optional bytes service_id = 1;
optional bytes backup_id = 2;
optional bytes token = 3;
optional uint64 valid_from = 4;
optional bytes data = 5;
optional bytes pin = 6;
optional uint32 tries = 7;
}
message BackupResponse {
enum Status {
OK = 1;
ALREADY_EXISTS = 2;
NOT_YET_VALID = 3;
}
optional Status status = 1;
optional bytes token = 2;
}
message RestoreRequest {
optional bytes service_id = 1;
optional bytes backup_id = 2;
optional bytes token = 3;
optional uint64 valid_from = 4;
optional bytes pin = 5;
}
message RestoreResponse {
enum Status {
OK = 1;
TOKEN_MISMATCH = 2;
NOT_YET_VALID = 3;
MISSING = 4;
PIN_MISMATCH = 5;
}
optional Status status = 1;
optional bytes token = 2;
optional bytes data = 3;
optional uint32 tries = 4;
}
message DeleteRequest {
optional bytes service_id = 1;
optional bytes backup_id = 2;
}
message DeleteResponse {
}

View File

@@ -0,0 +1,281 @@
package org.whispersystems.signalservice.api;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.internal.contacts.crypto.KeyBackupCipher;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse;
import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.IOException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.util.Locale;
public final class KeyBackupService {
private static final String TAG = KeyBackupService.class.getSimpleName();
private final KeyStore iasKeyStore;
private final String enclaveName;
private final String mrenclave;
private final PushServiceSocket pushServiceSocket;
private final int maxTries;
KeyBackupService(KeyStore iasKeyStore,
String enclaveName,
String mrenclave,
PushServiceSocket pushServiceSocket,
int maxTries)
{
this.iasKeyStore = iasKeyStore;
this.enclaveName = enclaveName;
this.mrenclave = mrenclave;
this.pushServiceSocket = pushServiceSocket;
this.maxTries = maxTries;
}
/**
* Use this if you don't want to validate that the server has not changed since you last set the pin.
*/
public PinChangeSession newPinChangeSession()
throws IOException
{
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), null);
}
/**
* Use this if you want to validate that the server has not changed since you last set the pin.
* The supplied token will have to match for the change to be successful.
*/
public PinChangeSession newPinChangeSession(TokenResponse currentToken)
throws IOException
{
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken);
}
/**
* Use this to validate that the pin is still set on the server with the current token.
* Additionally this validates that no one has used any tries.
*/
public RestoreSession newRestoreSession(TokenResponse currentToken)
throws IOException
{
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken);
}
/**
* Only call before registration, to see how many tries are left.
* <p>
* Pass the token to the newRegistrationSession.
*/
public TokenResponse getToken(String authAuthorization) throws IOException {
return pushServiceSocket.getKeyBackupServiceToken(authAuthorization, enclaveName);
}
/**
* Use this during registration, good for one try, on subsequent attempts, pass the token from the previous attempt.
*
* @param tokenResponse Supplying a token response from a failed previous attempt prevents certain attacks.
*/
public RestoreSession newRegistrationSession(String authAuthorization, TokenResponse tokenResponse)
throws IOException
{
return newSession(authAuthorization, tokenResponse);
}
private Session newSession(String authorization, TokenResponse currentToken)
throws IOException
{
TokenResponse token = currentToken != null ? currentToken : pushServiceSocket.getKeyBackupServiceToken(authorization, enclaveName);
return new Session(authorization, token);
}
private class Session implements RestoreSession, PinChangeSession {
private final String authorization;
private final TokenResponse currentToken;
Session(String authorization, TokenResponse currentToken) {
this.authorization = authorization;
this.currentToken = currentToken;
}
@Override
public RegistrationLockData restorePin(String pin)
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException
{
int attempt = 0;
SecureRandom random = new SecureRandom();
TokenResponse token = currentToken;
while (true) {
attempt++;
try {
return restorePin(pin, token);
} catch (TokenException tokenException) {
token = tokenException.getToken();
if (tokenException instanceof KeyBackupServicePinException) {
throw (KeyBackupServicePinException) tokenException;
}
if (tokenException.isCanAutomaticallyRetry() && attempt < 5) {
// back off randomly, between 250 and 8000 ms
int backoffMs = 250 * (1 << (attempt - 1));
Util.sleep(backoffMs + random.nextInt(backoffMs));
} else {
throw new UnauthenticatedResponseException("Token mismatch, expended all automatic retries");
}
}
}
}
private RegistrationLockData restorePin(String pin, TokenResponse token)
throws UnauthenticatedResponseException, IOException, TokenException, InvalidPinException
{
PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin);
try {
final int remainingTries = token.getTries();
final RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(stretchedPin.getKbsAccessKey(), token, remoteAttestation, Hex.fromStringCondensed(enclaveName));
final KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
final RestoreResponse status = KeyBackupCipher.getKeyRestoreResponse(response, remoteAttestation);
TokenResponse nextToken = status.hasToken()
? new TokenResponse(token.getBackupId(), status.getToken().toByteArray(), status.getTries())
: token;
Log.i(TAG, "Restore " + status.getStatus());
switch (status.getStatus()) {
case OK:
Log.i(TAG, String.format(Locale.US,"Restore OK! data: %s tries: %d", Hex.toStringCondensed(status.getData().toByteArray()), status.getTries()));
PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(status.getData().toByteArray());
return new RegistrationLockData(masterKey, nextToken);
case PIN_MISMATCH:
Log.i(TAG, "Restore PIN_MISMATCH");
throw new KeyBackupServicePinException(nextToken);
case TOKEN_MISMATCH:
Log.i(TAG, "Restore TOKEN_MISMATCH");
// if the number of tries has not fallen, the pin is correct we're just using an out of date token
boolean canRetry = remainingTries == status.getTries();
Log.i(TAG, String.format(Locale.US, "Token MISMATCH %d %d", remainingTries, status.getTries()));
Log.i(TAG, String.format("Last token %s", Hex.toStringCondensed(token.getToken())));
Log.i(TAG, String.format("Next token %s", Hex.toStringCondensed(nextToken.getToken())));
throw new TokenException(nextToken, canRetry);
case MISSING:
Log.i(TAG, "Restore OK! No data though");
return null;
case NOT_YET_VALID:
throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
}
} catch (InvalidCiphertextException e) {
throw new UnauthenticatedResponseException(e);
}
return null;
}
private RemoteAttestation getAndVerifyRemoteAttestation() throws UnauthenticatedResponseException, IOException {
try {
return RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.KeyBackup, iasKeyStore, enclaveName, mrenclave, authorization);
} catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | InvalidCiphertextException | SignatureException e) {
throw new UnauthenticatedResponseException(e);
}
}
@Override
public RegistrationLockData setPin(String pin) throws IOException, UnauthenticatedResponseException, InvalidPinException {
PinStretcher.MasterKey masterKey = PinStretcher.stretchPin(pin)
.withNewSecurePinKey2();
TokenResponse tokenResponse = putKbsData(masterKey.getKbsAccessKey(),
masterKey.getPinKey2(),
enclaveName,
currentToken);
pushServiceSocket.setRegistrationLock(masterKey.getRegistrationLock());
return new RegistrationLockData(masterKey, tokenResponse);
}
@Override
public void removePin() throws IOException, UnauthenticatedResponseException {
deleteKbsData();
pushServiceSocket.removePinV2();
}
private TokenResponse putKbsData(byte[] kbsAccessKey, byte[] kbsData, String enclaveName, TokenResponse token)
throws IOException, UnauthenticatedResponseException
{
try {
RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
KeyBackupRequest request = KeyBackupCipher.createKeyBackupRequest(kbsAccessKey, kbsData, token, remoteAttestation, Hex.fromStringCondensed(enclaveName), maxTries);
KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
BackupResponse backupResponse = KeyBackupCipher.getKeyBackupResponse(response, remoteAttestation);
BackupResponse.Status status = backupResponse.getStatus();
switch (status) {
case OK:
return backupResponse.hasToken() ? new TokenResponse(token.getBackupId(), backupResponse.getToken().toByteArray(), maxTries) : token;
case ALREADY_EXISTS:
throw new UnauthenticatedResponseException("Already exists");
case NOT_YET_VALID:
throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
default:
throw new AssertionError("Unknown response status " + status);
}
} catch (InvalidCiphertextException e) {
throw new UnauthenticatedResponseException(e);
}
}
private void deleteKbsData()
throws IOException, UnauthenticatedResponseException
{
try {
RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
KeyBackupRequest request = KeyBackupCipher.createKeyDeleteRequest(currentToken, remoteAttestation, Hex.fromStringCondensed(enclaveName));
KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
KeyBackupCipher.getKeyDeleteResponseStatus(response, remoteAttestation);
} catch (InvalidCiphertextException e) {
throw new UnauthenticatedResponseException(e);
}
}
}
public interface RestoreSession {
RegistrationLockData restorePin(String pin)
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException;
}
public interface PinChangeSession {
RegistrationLockData setPin(String pin)
throws IOException, UnauthenticatedResponseException, InvalidPinException;
void removePin()
throws IOException, UnauthenticatedResponseException;
}
}

View File

@@ -0,0 +1,17 @@
package org.whispersystems.signalservice.api;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
public final class KeyBackupServicePinException extends TokenException {
private final int triesRemaining;
public KeyBackupServicePinException(TokenResponse nextToken) {
super(nextToken, false);
this.triesRemaining = nextToken.getTries();
}
public int getTriesRemaining() {
return triesRemaining;
}
}

View File

@@ -0,0 +1,27 @@
package org.whispersystems.signalservice.api;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
public final class RegistrationLockData {
private final PinStretcher.MasterKey masterKey;
private final TokenResponse tokenResponse;
RegistrationLockData(PinStretcher.MasterKey masterKey, TokenResponse tokenResponse) {
this.masterKey = masterKey;
this.tokenResponse = tokenResponse;
}
public PinStretcher.MasterKey getMasterKey() {
return masterKey;
}
public TokenResponse getTokenResponse() {
return tokenResponse;
}
public int getRemainingTries() {
return tokenResponse.getTries();
}
}

View File

@@ -9,8 +9,6 @@ package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import org.whispersystems.curve25519.Curve25519;
import org.whispersystems.curve25519.Curve25519KeyPair;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
@@ -18,7 +16,6 @@ import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
@@ -33,23 +30,19 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
import org.whispersystems.signalservice.internal.contacts.crypto.ContactDiscoveryCipher;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationKeys;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.crypto.ProvisioningCipher;
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
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.security.KeyStore;
import java.security.MessageDigest;
@@ -117,18 +110,34 @@ public class SignalServiceAccountManager {
return this.pushServiceSocket.getSenderCertificateLegacy();
}
public void setPin(Optional<String> pin) throws IOException {
if (pin.isPresent()) {
this.pushServiceSocket.setPin(pin.get());
} else {
this.pushServiceSocket.removePin();
}
/**
* @deprecated Remove this method once KBS is live.
*/
@Deprecated
public void setPin(String pin) throws IOException {
this.pushServiceSocket.setPin(pin);
}
/**
* V1 Pin setting has been replaced by KeyBackupService.
* Now you can only remove the old pin but there is no need to remove the old pin if setting a KBS Pin.
*/
public void removeV1Pin() throws IOException {
this.pushServiceSocket.removePin();
}
public UUID getOwnUuid() throws IOException {
return this.pushServiceSocket.getOwnUuid();
}
public KeyBackupService getKeyBackupService(KeyStore iasKeyStore,
String enclaveName,
String mrenclave,
int tries)
{
return new KeyBackupService(iasKeyStore, enclaveName, mrenclave, pushServiceSocket, tries);
}
/**
* Register/Unregister a Google Cloud Messaging registration ID.
*
@@ -193,16 +202,20 @@ public class SignalServiceAccountManager {
* This value should remain consistent across registrations for the
* same install, but probabilistically differ across registrations
* for separate installs.
* @param pin Deprecated, only supply the pin if you did not find a registrationLock on KBS.
* @param registrationLock Only supply if found on KBS.
* @return The UUID of the user that was registered.
* @throws IOException
*/
public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey,
signalProtocolRegistrationId,
fetchesMessages, pin,
fetchesMessages,
pin, registrationLock,
unidentifiedAccessKey,
unrestrictedUnidentifiedAccess);
}
@@ -215,14 +228,18 @@ public class SignalServiceAccountManager {
* This value should remain consistent across registrations for the same
* install, but probabilistically differ across registrations for
* separate installs.
* @param pin Only supply if pin has not yet been migrated to KBS.
* @param registrationLock Only supply if found on KBS.
*
* @throws IOException
*/
public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin,
public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages, pin,
this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages,
pin, registrationLock,
unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
}
@@ -306,35 +323,21 @@ public class SignalServiceAccountManager {
return activeTokens;
}
public List<String> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String mrenclave)
public List<String> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String enclaveId)
throws IOException, Quote.InvalidQuoteFormatException, UnauthenticatedQuoteException, SignatureException, UnauthenticatedResponseException
{
try {
String authorization = this.pushServiceSocket.getContactDiscoveryAuthorization();
Curve25519 curve = Curve25519.getInstance(Curve25519.BEST);
Curve25519KeyPair keyPair = curve.generateKeyPair();
ContactDiscoveryCipher cipher = new ContactDiscoveryCipher();
RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey());
Pair<RemoteAttestationResponse, List<String>> attestationResponse = this.pushServiceSocket.getContactDiscoveryRemoteAttestation(authorization, attestationRequest, mrenclave);
RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, attestationResponse.first().getServerEphemeralPublic(), attestationResponse.first().getServerStaticPublic());
Quote quote = new Quote(attestationResponse.first().getQuote());
byte[] requestId = cipher.getRequestId(keys, attestationResponse.first());
cipher.verifyServerQuote(quote, attestationResponse.first().getServerStaticPublic(), mrenclave);
cipher.verifyIasSignature(iasKeyStore, attestationResponse.first().getCertificates(), attestationResponse.first().getSignatureBody(), attestationResponse.first().getSignature(), quote);
RemoteAttestation remoteAttestation = new RemoteAttestation(requestId, keys);
List<String> addressBook = new LinkedList<>();
String authorization = pushServiceSocket.getContactDiscoveryAuthorization();
RemoteAttestation remoteAttestation = RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.ContactDiscovery, iasKeyStore, enclaveId, enclaveId, authorization);
List<String> addressBook = new LinkedList<>();
for (String e164number : e164numbers) {
addressBook.add(e164number.substring(1));
}
DiscoveryRequest request = cipher.createDiscoveryRequest(addressBook, remoteAttestation);
DiscoveryResponse response = this.pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, attestationResponse.second(), mrenclave);
byte[] data = cipher.getDiscoveryResponseData(response, remoteAttestation);
DiscoveryRequest request = ContactDiscoveryCipher.createDiscoveryRequest(addressBook, remoteAttestation);
DiscoveryResponse response = pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, remoteAttestation.getCookies(), enclaveId);
byte[] data = ContactDiscoveryCipher.getDiscoveryResponseData(response, remoteAttestation);
Iterator<String> addressBookIterator = addressBook.iterator();
List<String> results = new LinkedList<>();

View File

@@ -0,0 +1,22 @@
package org.whispersystems.signalservice.api;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
class TokenException extends Exception {
private final TokenResponse nextToken;
private final boolean canAutomaticallyRetry;
TokenException(TokenResponse nextToken, boolean canAutomaticallyRetry) {
this.nextToken = nextToken;
this.canAutomaticallyRetry = canAutomaticallyRetry;
}
public TokenResponse getToken() {
return nextToken;
}
public boolean isCanAutomaticallyRetry() {
return canAutomaticallyRetry;
}
}

View File

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

View File

@@ -6,11 +6,16 @@ public class SignalServiceConfiguration {
private final SignalServiceUrl[] signalServiceUrls;
private final SignalCdnUrl[] signalCdnUrls;
private final SignalContactDiscoveryUrl[] signalContactDiscoveryUrls;
private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls;
public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls, SignalCdnUrl[] signalCdnUrls, SignalContactDiscoveryUrl[] signalContactDiscoveryUrls) {
public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls,
SignalCdnUrl[] signalCdnUrls,
SignalContactDiscoveryUrl[] signalContactDiscoveryUrls,
SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls) {
this.signalServiceUrls = signalServiceUrls;
this.signalCdnUrls = signalCdnUrls;
this.signalContactDiscoveryUrls = signalContactDiscoveryUrls;
this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls;
}
public SignalServiceUrl[] getSignalServiceUrls() {
@@ -24,4 +29,8 @@ public class SignalServiceConfiguration {
public SignalContactDiscoveryUrl[] getSignalContactDiscoveryUrls() {
return signalContactDiscoveryUrls;
}
public SignalKeyBackupServiceUrl[] getSignalKeyBackupServiceUrls() {
return signalKeyBackupServiceUrls;
}
}

View File

@@ -0,0 +1,69 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.internal.util.Util;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
final class AESCipher {
private static final int TAG_LENGTH_BYTES = 16;
private static final int TAG_LENGTH_BITS = TAG_LENGTH_BYTES * 8;
static byte[] decrypt(byte[] key, byte[] iv, byte[] ciphertext, byte[] tag) throws InvalidCiphertextException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, iv));
return cipher.doFinal(ByteUtil.combine(ciphertext, tag));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (InvalidKeyException | BadPaddingException e) {
throw new InvalidCiphertextException(e);
}
}
static AESEncryptedResult encrypt(byte[] key, byte[] aad, byte[] requestData) {
try {
byte[] iv = Util.getSecretBytes(12);
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);
byte[] cipherText = cipher.doFinal(requestData);
byte[][] parts = ByteUtil.split(cipherText, cipherText.length - TAG_LENGTH_BYTES, TAG_LENGTH_BYTES);
byte[] mac = parts[1];
byte[] data = parts[0];
return new AESEncryptedResult(iv, data, mac, aad);
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
static class AESEncryptedResult {
final byte[] iv;
final byte[] data;
final byte[] mac;
final byte[] aad;
private AESEncryptedResult(byte[] iv, byte[] data, byte[] mac, byte[] aad) {
this.iv = iv;
this.data = data;
this.mac = mac;
this.aad = aad;
}
}
}

View File

@@ -1,47 +1,20 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import org.threeten.bp.Instant;
import org.threeten.bp.LocalDateTime;
import org.threeten.bp.Period;
import org.threeten.bp.ZoneId;
import org.threeten.bp.ZonedDateTime;
import org.threeten.bp.format.DateTimeFormatter;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public final class ContactDiscoveryCipher {
public class ContactDiscoveryCipher {
private ContactDiscoveryCipher() {
}
private static final int TAG_LENGTH_BYTES = 16;
private static final int TAG_LENGTH_BITS = TAG_LENGTH_BYTES * 8;
private static final long SIGNATURE_BODY_VERSION = 3L;
public DiscoveryRequest createDiscoveryRequest(List<String> addressBook, RemoteAttestation remoteAttestation) {
public static DiscoveryRequest createDiscoveryRequest(List<String> addressBook, RemoteAttestation remoteAttestation) {
try {
ByteArrayOutputStream requestDataStream = new ByteArrayOutputStream();
@@ -49,100 +22,19 @@ public class ContactDiscoveryCipher {
requestDataStream.write(ByteUtil.longToByteArray(Long.parseLong(address)));
}
byte[] requestData = requestDataStream.toByteArray();
byte[] nonce = Util.getSecretBytes(12);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] clientKey = remoteAttestation.getKeys().getClientKey();
byte[] requestData = requestDataStream.toByteArray();
byte[] aad = remoteAttestation.getRequestId();
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(remoteAttestation.getKeys().getClientKey(), "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, nonce));
cipher.updateAAD(remoteAttestation.getRequestId());
AESCipher.AESEncryptedResult aesEncryptedResult = AESCipher.encrypt(clientKey, aad, requestData);
byte[] cipherText = cipher.doFinal(requestData);
byte[][] parts = ByteUtil.split(cipherText, cipherText.length - TAG_LENGTH_BYTES, TAG_LENGTH_BYTES);
return new DiscoveryRequest(addressBook.size(), remoteAttestation.getRequestId(), nonce, parts[0], parts[1]);
} catch (IOException | NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
public byte[] getDiscoveryResponseData(DiscoveryResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
return decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
}
public byte[] getRequestId(RemoteAttestationKeys keys, RemoteAttestationResponse response) throws InvalidCiphertextException {
return decrypt(keys.getServerKey(), response.getIv(), response.getCiphertext(), response.getTag());
}
public void verifyServerQuote(Quote quote, byte[] serverPublicStatic, String mrenclave)
throws UnauthenticatedQuoteException
{
try {
byte[] theirServerPublicStatic = new byte[serverPublicStatic.length];
System.arraycopy(quote.getReportData(), 0, theirServerPublicStatic, 0, theirServerPublicStatic.length);
if (!MessageDigest.isEqual(theirServerPublicStatic, serverPublicStatic)) {
throw new UnauthenticatedQuoteException("Response quote has unauthenticated report data!");
}
if (!MessageDigest.isEqual(Hex.fromStringCondensed(mrenclave), quote.getMrenclave())) {
throw new UnauthenticatedQuoteException("The response quote has the wrong mrenclave value in it: " + Hex.toStringCondensed(quote.getMrenclave()));
}
if (quote.isDebugQuote()) {
throw new UnauthenticatedQuoteException("Received quote for debuggable enclave");
}
return new DiscoveryRequest(addressBook.size(), aesEncryptedResult.aad, aesEncryptedResult.iv, aesEncryptedResult.data, aesEncryptedResult.mac);
} catch (IOException e) {
throw new UnauthenticatedQuoteException(e);
}
}
public void verifyIasSignature(KeyStore trustStore, String certificates, String signatureBody, String signature, Quote quote)
throws SignatureException
{
if (certificates == null || certificates.isEmpty()) {
throw new SignatureException("No certificates.");
}
try {
SigningCertificate signingCertificate = new SigningCertificate(certificates, trustStore);
signingCertificate.verifySignature(signatureBody, signature);
SignatureBodyEntity signatureBodyEntity = JsonUtil.fromJson(signatureBody, SignatureBodyEntity.class);
if (signatureBodyEntity.getVersion() != SIGNATURE_BODY_VERSION) {
throw new SignatureException("Unexpected signed quote version " + signatureBodyEntity.getVersion());
}
if (!MessageDigest.isEqual(ByteUtil.trim(signatureBodyEntity.getIsvEnclaveQuoteBody(), 432), ByteUtil.trim(quote.getQuoteBytes(), 432))) {
throw new SignatureException("Signed quote is not the same as RA quote: " + Hex.toStringCondensed(signatureBodyEntity.getIsvEnclaveQuoteBody()) + " vs " + Hex.toStringCondensed(quote.getQuoteBytes()));
}
if (!"OK".equals(signatureBodyEntity.getIsvEnclaveQuoteStatus())) {
throw new SignatureException("Quote status is: " + signatureBodyEntity.getIsvEnclaveQuoteStatus());
}
if (Instant.from(ZonedDateTime.of(LocalDateTime.from(DateTimeFormatter.ofPattern("yyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(signatureBodyEntity.getTimestamp())), ZoneId.of("UTC")))
.plus(Period.ofDays(1))
.isBefore(Instant.now()))
{
throw new SignatureException("Signature is expired");
}
} catch (CertificateException | CertPathValidatorException | IOException e) {
throw new SignatureException(e);
}
}
private byte[] decrypt(byte[] key, byte[] iv, byte[] ciphertext, byte[] tag) throws InvalidCiphertextException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
return cipher.doFinal(ByteUtil.combine(ciphertext, tag));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (InvalidKeyException | BadPaddingException e) {
throw new InvalidCiphertextException(e);
}
}
public static byte[] getDiscoveryResponseData(DiscoveryResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
return AESCipher.decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
}
}

View File

@@ -0,0 +1,128 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.keybackup.protos.BackupRequest;
import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse;
import org.whispersystems.signalservice.internal.keybackup.protos.DeleteRequest;
import org.whispersystems.signalservice.internal.keybackup.protos.DeleteResponse;
import org.whispersystems.signalservice.internal.keybackup.protos.Request;
import org.whispersystems.signalservice.internal.keybackup.protos.Response;
import org.whispersystems.signalservice.internal.keybackup.protos.RestoreRequest;
import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse;
import java.util.concurrent.TimeUnit;
public final class KeyBackupCipher {
private KeyBackupCipher() {
}
private static final long VALID_FROM_BUFFER_MS = TimeUnit.DAYS.toMillis(1);
public static KeyBackupRequest createKeyBackupRequest(byte[] kbsAccessKey,
byte[] kbsData,
TokenResponse token,
RemoteAttestation remoteAttestation,
byte[] serviceId,
int tries)
{
long now = System.currentTimeMillis();
BackupRequest backupRequest = BackupRequest.newBuilder()
.setServiceId(ByteString.copyFrom(serviceId))
.setBackupId(ByteString.copyFrom(token.getBackupId()))
.setToken(ByteString.copyFrom(token.getToken()))
.setValidFrom(getValidFromSeconds(now))
.setData(ByteString.copyFrom(kbsData))
.setPin(ByteString.copyFrom(kbsAccessKey))
.setTries(tries)
.build();
Request requestData = Request.newBuilder().setBackup(backupRequest).build();
return createKeyBackupRequest(requestData, remoteAttestation);
}
public static KeyBackupRequest createKeyRestoreRequest(byte[] kbsAccessKey,
TokenResponse token,
RemoteAttestation remoteAttestation,
byte[] serviceId)
{
long now = System.currentTimeMillis();
RestoreRequest restoreRequest = RestoreRequest.newBuilder()
.setServiceId(ByteString.copyFrom(serviceId))
.setBackupId(ByteString.copyFrom(token.getBackupId()))
.setToken(ByteString.copyFrom(token.getToken()))
.setValidFrom(getValidFromSeconds(now))
.setPin(ByteString.copyFrom(kbsAccessKey))
.build();
Request request = Request.newBuilder().setRestore(restoreRequest).build();
return createKeyBackupRequest(request, remoteAttestation);
}
public static KeyBackupRequest createKeyDeleteRequest(TokenResponse token,
RemoteAttestation remoteAttestation,
byte[] serviceId)
{
DeleteRequest deleteRequest = DeleteRequest.newBuilder()
.setServiceId(ByteString.copyFrom(serviceId))
.setBackupId(ByteString.copyFrom(token.getBackupId()))
.build();
Request request = Request.newBuilder().setDelete(deleteRequest).build();
return createKeyBackupRequest(request, remoteAttestation);
}
public static BackupResponse getKeyBackupResponse(KeyBackupResponse response, RemoteAttestation remoteAttestation)
throws InvalidCiphertextException, InvalidProtocolBufferException
{
byte[] data = decryptData(response, remoteAttestation);
Response backupResponse = Response.parseFrom(data);
return backupResponse.getBackup();
}
public static RestoreResponse getKeyRestoreResponse(KeyBackupResponse response, RemoteAttestation remoteAttestation)
throws InvalidCiphertextException, InvalidProtocolBufferException
{
byte[] data = decryptData(response, remoteAttestation);
return Response.parseFrom(data).getRestore();
}
public static DeleteResponse getKeyDeleteResponseStatus(KeyBackupResponse response, RemoteAttestation remoteAttestation)
throws InvalidCiphertextException, InvalidProtocolBufferException
{
byte[] data = decryptData(response, remoteAttestation);
return DeleteResponse.parseFrom(data);
}
private static KeyBackupRequest createKeyBackupRequest(Request requestData, RemoteAttestation remoteAttestation) {
byte[] clientKey = remoteAttestation.getKeys().getClientKey();
byte[] aad = remoteAttestation.getRequestId();
AESCipher.AESEncryptedResult aesEncryptedResult = AESCipher.encrypt(clientKey, aad, requestData.toByteArray());
return new KeyBackupRequest(aesEncryptedResult.aad, aesEncryptedResult.iv, aesEncryptedResult.data, aesEncryptedResult.mac);
}
private static byte[] decryptData(KeyBackupResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
return AESCipher.decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
}
private static long getValidFromSeconds(long nowMs) {
return TimeUnit.MILLISECONDS.toSeconds(nowMs - VALID_FROM_BUFFER_MS);
}
}

View File

@@ -1,13 +1,17 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import java.util.List;
public class RemoteAttestation {
private final byte[] requestId;
private final RemoteAttestationKeys keys;
private final List<String> cookies;
public RemoteAttestation(byte[] requestId, RemoteAttestationKeys keys) {
public RemoteAttestation(byte[] requestId, RemoteAttestationKeys keys, List<String> cookies) {
this.requestId = requestId;
this.keys = keys;
this.cookies = cookies;
}
public byte[] getRequestId() {
@@ -17,4 +21,8 @@ public class RemoteAttestation {
public RemoteAttestationKeys getKeys() {
return keys;
}
public List<String> getCookies() {
return cookies;
}
}

View File

@@ -0,0 +1,92 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import org.threeten.bp.Instant;
import org.threeten.bp.LocalDateTime;
import org.threeten.bp.Period;
import org.threeten.bp.ZoneId;
import org.threeten.bp.ZonedDateTime;
import org.threeten.bp.format.DateTimeFormatter;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.io.IOException;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.SignatureException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
public final class RemoteAttestationCipher {
private RemoteAttestationCipher() {
}
private static final long SIGNATURE_BODY_VERSION = 3L;
public static byte[] getRequestId(RemoteAttestationKeys keys, RemoteAttestationResponse response) throws InvalidCiphertextException {
return AESCipher.decrypt(keys.getServerKey(), response.getIv(), response.getCiphertext(), response.getTag());
}
public static void verifyServerQuote(Quote quote, byte[] serverPublicStatic, String mrenclave)
throws UnauthenticatedQuoteException
{
try {
byte[] theirServerPublicStatic = new byte[serverPublicStatic.length];
System.arraycopy(quote.getReportData(), 0, theirServerPublicStatic, 0, theirServerPublicStatic.length);
if (!MessageDigest.isEqual(theirServerPublicStatic, serverPublicStatic)) {
throw new UnauthenticatedQuoteException("Response quote has unauthenticated report data!");
}
if (!MessageDigest.isEqual(Hex.fromStringCondensed(mrenclave), quote.getMrenclave())) {
throw new UnauthenticatedQuoteException("The response quote has the wrong mrenclave value in it: " + Hex.toStringCondensed(quote.getMrenclave()));
}
if (quote.isDebugQuote()) {
throw new UnauthenticatedQuoteException("Received quote for debuggable enclave");
}
} catch (IOException e) {
throw new UnauthenticatedQuoteException(e);
}
}
public static void verifyIasSignature(KeyStore trustStore, String certificates, String signatureBody, String signature, Quote quote)
throws SignatureException
{
if (certificates == null || certificates.isEmpty()) {
throw new SignatureException("No certificates.");
}
try {
SigningCertificate signingCertificate = new SigningCertificate(certificates, trustStore);
signingCertificate.verifySignature(signatureBody, signature);
SignatureBodyEntity signatureBodyEntity = JsonUtil.fromJson(signatureBody, SignatureBodyEntity.class);
if (signatureBodyEntity.getVersion() != SIGNATURE_BODY_VERSION) {
throw new SignatureException("Unexpected signed quote version " + signatureBodyEntity.getVersion());
}
if (!MessageDigest.isEqual(ByteUtil.trim(signatureBodyEntity.getIsvEnclaveQuoteBody(), 432), ByteUtil.trim(quote.getQuoteBytes(), 432))) {
throw new SignatureException("Signed quote is not the same as RA quote: " + Hex.toStringCondensed(signatureBodyEntity.getIsvEnclaveQuoteBody()) + " vs " + Hex.toStringCondensed(quote.getQuoteBytes()));
}
if (!"OK".equals(signatureBodyEntity.getIsvEnclaveQuoteStatus())) {
throw new SignatureException("Quote status is: " + signatureBodyEntity.getIsvEnclaveQuoteStatus());
}
if (Instant.from(ZonedDateTime.of(LocalDateTime.from(DateTimeFormatter.ofPattern("yyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(signatureBodyEntity.getTimestamp())), ZoneId.of("UTC")))
.plus(Period.ofDays(1))
.isBefore(Instant.now()))
{
throw new SignatureException("Signature is expired");
}
} catch (CertificateException | CertPathValidatorException | IOException e) {
throw new SignatureException(e);
}
}
}

View File

@@ -31,11 +31,15 @@ public class SigningCertificate {
{
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<X509Certificate> certificatesCollection = (Collection<X509Certificate>) certificateFactory.generateCertificates(new ByteArrayInputStream(URLDecoder.decode(certificateChain).getBytes()));
Collection<X509Certificate> certificatesCollection = (Collection<X509Certificate>) certificateFactory.generateCertificates(new ByteArrayInputStream(certificateChain.getBytes()));
List<X509Certificate> certificates = new LinkedList<>(certificatesCollection);
PKIXParameters pkixParameters = new PKIXParameters(trustStore);
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
if (certificates.isEmpty()) {
throw new CertificateException("No certificates available! Badly-formatted cert chain?");
}
this.path = certificateFactory.generateCertPath(certificates);
pkixParameters.setRevocationEnabled(false);

View File

@@ -47,7 +47,7 @@ public class DiscoveryRequest {
this.requestId = requestId;
this.iv = iv;
this.data = data;
this. mac = mac;
this.mac = mac;
}
public byte[] getRequestId() {

View File

@@ -0,0 +1,51 @@
package org.whispersystems.signalservice.internal.contacts.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.internal.util.Hex;
public class KeyBackupRequest {
@JsonProperty
private byte[] requestId;
@JsonProperty
private byte[] iv;
@JsonProperty
private byte[] data;
@JsonProperty
private byte[] mac;
public KeyBackupRequest() {
}
public KeyBackupRequest(byte[] requestId, byte[] iv, byte[] data, byte[] mac) {
this.requestId = requestId;
this.iv = iv;
this.data = data;
this.mac = mac;
}
public byte[] getRequestId() {
return requestId;
}
public byte[] getIv() {
return iv;
}
public byte[] getData() {
return data;
}
public byte[] getMac() {
return mac;
}
public String toString() {
return "{ requestId: " + Hex.toString(requestId) + ", iv: " + Hex.toString(iv) + ", data: " + Hex.toString(data) + ", mac: " + Hex.toString(mac) + "}";
}
}

View File

@@ -0,0 +1,41 @@
package org.whispersystems.signalservice.internal.contacts.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.internal.util.Hex;
public class KeyBackupResponse {
@JsonProperty
private byte[] iv;
@JsonProperty
private byte[] data;
@JsonProperty
private byte[] mac;
public KeyBackupResponse() {}
public KeyBackupResponse(byte[] iv, byte[] data, byte[] mac) {
this.iv = iv;
this.data = data;
this.mac = mac;
}
public byte[] getIv() {
return iv;
}
public byte[] getData() {
return data;
}
public byte[] getMac() {
return mac;
}
public String toString() {
return "{iv: " + (iv == null ? null : Hex.toString(iv)) + ", data: " + (data == null ? null: Hex.toString(data)) + ", mac: " + (mac == null ? null : Hex.toString(mac)) + "}";
}
}

View File

@@ -0,0 +1,38 @@
package org.whispersystems.signalservice.internal.contacts.entities;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class TokenResponse {
@JsonProperty
private byte[] backupId;
@JsonProperty
private byte[] token;
@JsonProperty
private int tries;
@JsonCreator
public TokenResponse() {
}
public TokenResponse(byte[] backupId, byte[] token, int tries) {
this.backupId = backupId;
this.token = token;
this.tries = tries;
}
public byte[] getBackupId() {
return backupId;
}
public byte[] getToken() {
return token;
}
public int getTries() {
return tries;
}
}

View File

@@ -28,19 +28,23 @@ public class AccountAttributes {
@JsonProperty
private String pin;
@JsonProperty
private String registrationLock;
@JsonProperty
private byte[] unidentifiedAccessKey;
@JsonProperty
private boolean unrestrictedUnidentifiedAccess;
public AccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess) {
public AccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin, String registrationLock, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess) {
this.signalingKey = signalingKey;
this.registrationId = registrationId;
this.voice = true;
this.video = true;
this.fetchesMessages = fetchesMessages;
this.pin = pin;
this.registrationLock = registrationLock;
this.unidentifiedAccessKey = unidentifiedAccessKey;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
}
@@ -71,6 +75,10 @@ public class AccountAttributes {
return pin;
}
public String getRegistrationLock() {
return registrationLock;
}
public byte[] getUnidentifiedAccessKey() {
return unidentifiedAccessKey;
}

View File

@@ -0,0 +1,18 @@
package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
import okhttp3.Credentials;
public class AuthCredentials {
@JsonProperty
private String username;
@JsonProperty
private String password;
public String asBasic() {
return Credentials.basic(username, password);
}
}

View File

@@ -1,28 +0,0 @@
package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ContactDiscoveryCredentials {
@JsonProperty
private String username;
@JsonProperty
private String password;
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}

View File

@@ -3,14 +3,16 @@ package org.whispersystems.signalservice.internal.push;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
public class LockedException extends NonSuccessfulResponseCodeException {
public final class LockedException extends NonSuccessfulResponseCodeException {
private int length;
private long timeRemaining;
private final int length;
private final long timeRemaining;
private final String basicStorageCredentials;
LockedException(int length, long timeRemaining) {
this.length = length;
this.timeRemaining = timeRemaining;
LockedException(int length, long timeRemaining, String basicStorageCredentials) {
this.length = length;
this.timeRemaining = timeRemaining;
this.basicStorageCredentials = basicStorageCredentials;
}
public int getLength() {
@@ -20,4 +22,8 @@ public class LockedException extends NonSuccessfulResponseCodeException {
public long getTimeRemaining() {
return timeRemaining;
}
public String getBasicStorageCredentials() {
return basicStorageCredentials;
}
}

View File

@@ -43,8 +43,9 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
import org.whispersystems.signalservice.internal.configuration.SignalUrl;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody;
@@ -78,14 +79,15 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.Call;
import okhttp3.ConnectionSpec;
import okhttp3.Credentials;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
@@ -108,6 +110,7 @@ public class PushServiceSocket {
private static final String TURN_SERVER_INFO = "/v1/accounts/turn";
private static final String SET_ACCOUNT_ATTRIBUTES = "/v1/accounts/attributes/";
private static final String PIN_PATH = "/v1/accounts/pin/";
private static final String REGISTRATION_LOCK_PATH = "/v1/accounts/registration_lock";
private static final String REQUEST_PUSH_CHALLENGE = "/v1/accounts/fcm/preauth/%s/%s";
private static final String WHO_AM_I = "/v1/accounts/whoami";
private static final String SET_USERNAME_PATH = "/v1/accounts/username/%s";
@@ -137,6 +140,8 @@ public class PushServiceSocket {
private static final String SENDER_CERTIFICATE_LEGACY_PATH = "/v1/certificate/delivery";
private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery?includeUuid=true";
private static final String KBS_AUTH_PATH = "/v1/backup/auth";
private static final String ATTACHMENT_DOWNLOAD_PATH = "attachments/%d";
private static final String ATTACHMENT_UPLOAD_PATH = "attachments/";
@@ -152,6 +157,7 @@ public class PushServiceSocket {
private final ServiceConnectionHolder[] serviceClients;
private final ConnectionHolder[] cdnClients;
private final ConnectionHolder[] contactDiscoveryClients;
private final ConnectionHolder[] keyBackupServiceClients;
private final OkHttpClient attachmentClient;
private final CredentialsProvider credentialsProvider;
@@ -164,6 +170,7 @@ public class PushServiceSocket {
this.serviceClients = createServiceConnectionHolders(signalServiceConfiguration.getSignalServiceUrls());
this.cdnClients = createConnectionHolders(signalServiceConfiguration.getSignalCdnUrls());
this.contactDiscoveryClients = createConnectionHolders(signalServiceConfiguration.getSignalContactDiscoveryUrls());
this.keyBackupServiceClients = createConnectionHolders(signalServiceConfiguration.getSignalKeyBackupServiceUrls());
this.attachmentClient = createAttachmentClient();
this.random = new SecureRandom();
}
@@ -219,11 +226,12 @@ public class PushServiceSocket {
}
}
public UUID verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages, String pin,
public UUID verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
String requestBody = JsonUtil.toJson(signalingKeyEntity);
String responseBody = makeServiceRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode), "PUT", requestBody);
VerifyAccountResponse response = JsonUtil.fromJson(responseBody, VerifyAccountResponse.class);
@@ -236,11 +244,16 @@ public class PushServiceSocket {
}
}
public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin,
public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin,
if (registrationLock != null && pin != null) {
throw new AssertionError("Pin should be null if registrationLock is set.");
}
AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock,
unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
makeServiceRequest(SET_ACCOUNT_ATTRIBUTES, "PUT", JsonUtil.toJson(accountAttributes));
}
@@ -282,10 +295,20 @@ public class PushServiceSocket {
makeServiceRequest(PIN_PATH, "PUT", JsonUtil.toJson(accountLock));
}
/** Note: Setting a KBS Pin will clear this */
public void removePin() throws IOException {
makeServiceRequest(PIN_PATH, "DELETE", null);
}
public void setRegistrationLock(String registrationLock) throws IOException {
RegistrationLockV2 accountLock = new RegistrationLockV2(registrationLock);
makeServiceRequest(REGISTRATION_LOCK_PATH, "PUT", JsonUtil.toJson(accountLock));
}
public void removePinV2() throws IOException {
makeServiceRequest(REGISTRATION_LOCK_PATH, "DELETE", null);
}
public byte[] getSenderCertificateLegacy() throws IOException {
String responseText = makeServiceRequest(SENDER_CERTIFICATE_LEGACY_PATH, "GET", null);
return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate();
@@ -592,26 +615,27 @@ public class PushServiceSocket {
}
}
public String getContactDiscoveryAuthorization() throws IOException {
String response = makeServiceRequest(DIRECTORY_AUTH_PATH, "GET", null);
ContactDiscoveryCredentials token = JsonUtil.fromJson(response, ContactDiscoveryCredentials.class);
return Credentials.basic(token.getUsername(), token.getPassword());
private String getCredentials(String authPath) throws IOException {
String response = makeServiceRequest(authPath, "GET", null, NO_HEADERS);
AuthCredentials token = JsonUtil.fromJson(response, AuthCredentials.class);
return token.asBasic();
}
public Pair<RemoteAttestationResponse, List<String>> getContactDiscoveryRemoteAttestation(String authorization, RemoteAttestationRequest request, String mrenclave)
throws IOException
{
Response response = makeContactDiscoveryRequest(authorization, new LinkedList<String>(), "/v1/attestation/" + mrenclave, "PUT", JsonUtil.toJson(request));
ResponseBody body = response.body();
List<String> rawCookies = response.headers("Set-Cookie");
List<String> cookies = new LinkedList<>();
public String getContactDiscoveryAuthorization() throws IOException {
return getCredentials(DIRECTORY_AUTH_PATH);
}
for (String cookie : rawCookies) {
cookies.add(cookie.split(";")[0]);
}
public String getKeyBackupServiceAuthorization() throws IOException {
return getCredentials(KBS_AUTH_PATH);
}
public TokenResponse getKeyBackupServiceToken(String authorizationToken, String enclaveName)
throws IOException
{
ResponseBody body = makeRequest(ClientSet.KeyBackup, authorizationToken, null, "/v1/token/" + enclaveName, "GET", null).body();
if (body != null) {
return new Pair<>(JsonUtil.fromJson(body.string(), RemoteAttestationResponse.class), cookies);
return JsonUtil.fromJson(body.string(), TokenResponse.class);
} else {
throw new NonSuccessfulResponseCodeException("Empty response!");
}
@@ -620,7 +644,7 @@ public class PushServiceSocket {
public DiscoveryResponse getContactDiscoveryRegisteredUsers(String authorizationToken, DiscoveryRequest request, List<String> cookies, String mrenclave)
throws IOException
{
ResponseBody body = makeContactDiscoveryRequest(authorizationToken, cookies, "/v1/discovery/" + mrenclave, "PUT", JsonUtil.toJson(request)).body();
ResponseBody body = makeRequest(ClientSet.ContactDiscovery, authorizationToken, cookies, "/v1/discovery/" + mrenclave, "PUT", JsonUtil.toJson(request)).body();
if (body != null) {
return JsonUtil.fromJson(body.string(), DiscoveryResponse.class);
@@ -629,6 +653,18 @@ public class PushServiceSocket {
}
}
public KeyBackupResponse putKbsData(String authorizationToken, KeyBackupRequest request, List<String> cookies, String mrenclave)
throws IOException
{
ResponseBody body = makeRequest(ClientSet.KeyBackup, authorizationToken, cookies, "/v1/backup/" + mrenclave, "PUT", JsonUtil.toJson(request)).body();
if (body != null) {
return JsonUtil.fromJson(body.string(), KeyBackupResponse.class);
} else {
throw new NonSuccessfulResponseCodeException("Empty response!");
}
}
public void reportContactDiscoveryServiceMatch() throws IOException {
makeServiceRequest(String.format(DIRECTORY_FEEDBACK_PATH, "ok"), "PUT", "");
}
@@ -922,7 +958,12 @@ public class PushServiceSocket {
throw new PushNetworkException(e);
}
throw new LockedException(accountLockFailure.length, accountLockFailure.timeRemaining);
AuthCredentials credentials = accountLockFailure.backupCredentials;
String basicStorageCredentials = credentials != null ? credentials.asBasic() : null;
throw new LockedException(accountLockFailure.length,
accountLockFailure.timeRemaining,
basicStorageCredentials);
}
if (responseCode != 200 && responseCode != 204) {
@@ -960,10 +1001,12 @@ public class PushServiceSocket {
request.addHeader(header.getKey(), header.getValue());
}
if (unidentifiedAccess.isPresent()) {
request.addHeader("Unidentified-Access-Key", Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
} else if (credentialsProvider.getPassword() != null) {
request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider));
if (!headers.containsKey("Authorization")) {
if (unidentifiedAccess.isPresent()) {
request.addHeader("Unidentified-Access-Key", Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
} else if (credentialsProvider.getPassword() != null) {
request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider));
}
}
if (userAgent != null) {
@@ -992,15 +1035,33 @@ public class PushServiceSocket {
}
}
private Response makeContactDiscoveryRequest(String authorization, List<String> cookies, String path, String method, String body)
private ConnectionHolder[] clientsFor(ClientSet clientSet) {
switch (clientSet) {
case ContactDiscovery:
return contactDiscoveryClients;
case KeyBackup:
return keyBackupServiceClients;
default:
throw new AssertionError("Unknown attestation purpose");
}
}
Response makeRequest(ClientSet clientSet, String authorization, List<String> cookies, String path, String method, String body)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
ConnectionHolder connectionHolder = getRandom(contactDiscoveryClients, random);
OkHttpClient okHttpClient = connectionHolder.getClient()
.newBuilder()
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build();
ConnectionHolder connectionHolder = getRandom(clientsFor(clientSet), random);
return makeRequest(connectionHolder, authorization, cookies, path, method, body);
}
private Response makeRequest(ConnectionHolder connectionHolder, String authorization, List<String> cookies, String path, String method, String body)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
OkHttpClient okHttpClient = connectionHolder.getClient()
.newBuilder()
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build();
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
@@ -1153,12 +1214,26 @@ public class PushServiceSocket {
}
}
private static class RegistrationLockV2 {
@JsonProperty
private String registrationLock;
public RegistrationLockV2() {}
public RegistrationLockV2(String registrationLock) {
this.registrationLock = registrationLock;
}
}
private static class RegistrationLockFailure {
@JsonProperty
private int length;
@JsonProperty
private long timeRemaining;
@JsonProperty
private AuthCredentials backupCredentials;
}
private static class AttachmentDescriptor {
@@ -1225,4 +1300,6 @@ public class PushServiceSocket {
@Override
public void handle(int responseCode) { }
}
public enum ClientSet { ContactDiscovery, KeyBackup }
}

View File

@@ -0,0 +1,79 @@
package org.whispersystems.signalservice.internal.push;
import org.whispersystems.curve25519.Curve25519;
import org.whispersystems.curve25519.Curve25519KeyPair;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
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.RemoteAttestationRequest;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.io.IOException;
import java.security.KeyStore;
import java.security.SignatureException;
import java.util.LinkedList;
import java.util.List;
import okhttp3.Response;
import okhttp3.ResponseBody;
public final class RemoteAttestationUtil {
private RemoteAttestationUtil() {
}
public static RemoteAttestation getAndVerifyRemoteAttestation(PushServiceSocket socket,
PushServiceSocket.ClientSet clientSet,
KeyStore iasKeyStore,
String enclaveName,
String mrenclave,
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();
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);
}
private static Pair<RemoteAttestationResponse, List<String>> getRemoteAttestation(PushServiceSocket socket,
PushServiceSocket.ClientSet clientSet,
String authorization,
RemoteAttestationRequest request,
String enclaveName)
throws IOException
{
Response response = socket.makeRequest(clientSet, authorization, new LinkedList<String>(), "/v1/attestation/" + enclaveName, "PUT", JsonUtil.toJson(request));
ResponseBody body = response.body();
List<String> rawCookies = response.headers("Set-Cookie");
List<String> cookies = new LinkedList<>();
for (String cookie : rawCookies) {
cookies.add(cookie.split(";")[0]);
}
if (body != null) {
return new Pair<>(JsonUtil.fromJson(body.string(), RemoteAttestationResponse.class), cookies);
} else {
throw new NonSuccessfulResponseCodeException("Empty response!");
}
}
}

View File

@@ -0,0 +1,8 @@
package org.whispersystems.signalservice.internal.registrationpin;
public final class InvalidPinException extends Exception {
InvalidPinException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,166 @@
package org.whispersystems.signalservice.internal.registrationpin;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.Util;
import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.Mac;
import javax.crypto.SecretKeyFactory;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public final class PinStretcher {
private static final String HMAC_SHA256 = "HmacSHA256";
private static final Charset UTF_8 = Charset.forName("UTF-8");
public static StretchedPin stretchPin(CharSequence pin) throws InvalidPinException {
return new StretchedPin(pin);
}
public static class StretchedPin {
private final byte[] stretchedPin;
private final byte[] pinKey1;
private final byte[] kbsAccessKey;
private StretchedPin(byte[] stretchedPin, byte[] pinKey1, byte[] kbsAccessKey) {
this.stretchedPin = stretchedPin;
this.pinKey1 = pinKey1;
this.kbsAccessKey = kbsAccessKey;
}
private StretchedPin(CharSequence pin) throws InvalidPinException {
if (pin.length() < 4) throw new InvalidPinException("Pin too short");
char[] arabicPin = toArabic(pin);
stretchedPin = pbkdf2HmacSHA256(arabicPin, "nosalt", 20000, 256);
try {
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(new SecretKeySpec(stretchedPin, HMAC_SHA256));
mac.update("Master Key Encryption".getBytes(UTF_8));
pinKey1 = new byte[32];
mac.doFinal(pinKey1, 0);
mac.init(new SecretKeySpec(stretchedPin, HMAC_SHA256));
mac.update("KBS Access Key".getBytes(UTF_8));
kbsAccessKey = new byte[32];
mac.doFinal(kbsAccessKey, 0);
} catch (NoSuchAlgorithmException | ShortBufferException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public MasterKey withPinKey2(byte[] pinKey2) {
return new MasterKey(pinKey1, pinKey2, this);
}
public MasterKey withNewSecurePinKey2() {
return withPinKey2(Util.getSecretBytes(32));
}
public byte[] getPinKey1() {
return pinKey1;
}
public byte[] getStretchedPin() {
return stretchedPin;
}
public byte[] getKbsAccessKey() {
return kbsAccessKey;
}
}
public static class MasterKey extends StretchedPin {
private final byte[] pinKey2;
private final byte[] masterKey;
private final String registrationLock;
private MasterKey(byte[] pinKey1, byte[] pinKey2, StretchedPin stretchedPin) {
super(stretchedPin.stretchedPin, stretchedPin.pinKey1, stretchedPin.kbsAccessKey);
if (pinKey2.length != 32) {
throw new AssertionError("PinKey2 must be exactly 32 bytes");
}
this.pinKey2 = pinKey2.clone();
try {
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(new SecretKeySpec(pinKey1, HMAC_SHA256));
mac.update(pinKey2);
masterKey = new byte[32];
mac.doFinal(masterKey, 0);
mac.init(new SecretKeySpec(masterKey, HMAC_SHA256));
mac.update("Registration Lock".getBytes(UTF_8));
byte[] registration_lock_token_bytes = new byte[32];
mac.doFinal(registration_lock_token_bytes, 0);
registrationLock = Hex.toStringCondensed(registration_lock_token_bytes);
} catch (NoSuchAlgorithmException | ShortBufferException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public byte[] getPinKey2() {
return pinKey2;
}
public String getRegistrationLock() {
return registrationLock;
}
public byte[] getMasterKey() {
return masterKey;
}
}
private static byte[] pbkdf2HmacSHA256(char[] pin, String salt, int iterationCount, int outputSize) {
byte[] saltBytes = salt.getBytes(Charset.forName("UTF-8"));
try {
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
PBEKeySpec spec = new PBEKeySpec(pin, saltBytes, iterationCount, outputSize);
byte[] encoded = skf.generateSecret(spec).getEncoded();
spec.clearPassword();
return encoded;
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new AssertionError("Could not stretch pin", e);
}
}
/**
* Converts a string of not necessarily Arabic numerals to Arabic 0..9 characters.
*/
private static char[] toArabic(CharSequence numerals) throws InvalidPinException {
int length = numerals.length();
char[] arabic = new char[length];
for (int i = 0; i < length; i++) {
int digit = Character.digit(numerals.charAt(i), 10);
if (digit < 0) {
throw new InvalidPinException("Pin must only consist of decimals");
}
arabic[i] = (char) ('0' + digit);
}
return arabic;
}
}

View File

@@ -10,7 +10,6 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collection;
@@ -71,13 +70,9 @@ public class Util {
}
public static byte[] getSecretBytes(int size) {
try {
byte[] secret = new byte[size];
SecureRandom.getInstance("SHA1PRNG").nextBytes(secret);
return secret;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
byte[] secret = new byte[size];
new SecureRandom().nextBytes(secret);
return secret;
}
public static byte[] getRandomLengthBytes(int maxSize) {

View File

@@ -7,6 +7,7 @@ import org.whispersystems.signalservice.internal.contacts.crypto.SigningCertific
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.net.URLDecoder;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -22,7 +23,7 @@ public class SigningCertificateTest extends TestCase {
}
public void testGoodSignature() throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException, CertPathValidatorException, SignatureException {
String certificateChain = "-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A";
String certificateChain = URLDecoder.decode("-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A");
String signature = "Kn2Ya2T039qvEWIzIQeSksNyyCQIkcVjciClcp3a6C766dJANXxLLIn6CfyvUZddMtePrTOLpC2e5QTQxB4RwtWmFfr7nxRdFUtA3dH2DAQL5DqqlmPv46ZWSPfiiOXUsu8vNgX3Z4Znt4Q+dIPIquNPY8ZmiAcpKR7n2K3QtabgOnJ2EyngabY3LMQTtriXbZjpl53ynhVhV1rciMdvMaTz4DUYt7gKi+KeNd3CBFSev+eTgYPC3em96J/3bfVR+wC5m3JGbIBCrwAsbO05JkiNIMck3s+p4d/hwiABR75EplxaWmGgIm6VvUKtGhdJ/cNrmF0nxMX6Vi6N2WaLTA==";
String signatureBody = "{\"id\":\"287419896494669543891634765983074535548\",\"timestamp\":\"2019-03-11T20:01:21.658293\",\"version\":3,\"isvEnclaveQuoteStatus\":\"OK\",\"isvEnclaveQuoteBody\":\"AgAAADILAAAIAAcAAAAAAPiLWcRSSA3shraxepsGV9qF4zYUPJgE42ZZZXS2G9zaBQUCBP//AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAHAAAAAAAAAM1s/DQpN7I7G907v5chqlYVrJ/1CnXFUn1EHNMnaCbJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADrzm117Qj8NlEllyDkV4Pae4UgsPjgVXtAA5UsG90gVgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHgz6GaO6bkxfPLBYcR5rEf9Itrt81OEanXteSMcd/BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}";
@@ -35,7 +36,7 @@ public class SigningCertificateTest extends TestCase {
}
public void testBadSignature() throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException, CertPathValidatorException, SignatureException {
String certificateChain = "-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A";
String certificateChain = URLDecoder.decode("-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A");
String signature = "Kn2Ya2T039qvEWIzIQeSksNyyCQIkcVjciClcp3a6C766dJANXxLLIn6CfyvUZddMtePrTOLpC2e5QTQxB4RwtWmFfr7nxRdFUtA3dH2DAQL5DqqlmPv46ZWSPfiiOXUsu8vNgX3Z4Znt4Q+dIPIquNPY8ZmiAcpKR7n2K3QtabgOnJ2EyngabY3LMQTtriXbZjpl53ynhVhV1rciMdvMaTz4DUYt7gKi+KeNd3CBFSev+eTgYPC3em96J/3bfVR+wC5m3JGbIBCrwAsbO05JkiNIMck3s+p4d/hwiABR75EplxaWmGgIm6VvUKtGhdJ/cNrmF0nxMX6Vi6N2WaLTA==";
String signatureBody = "{\"id\":\"287419896494669543891634765983074535548\",\"timestamp\":\"2019-03-11T20:01:21.658293\",\"version\":3,\"isvEnclaveQuoteStatus\":\"OK\",\"isvEnclaveQuoteBody\":\"AgAAADILAAAIAAcAAAAAAPiLWcRSSA3shraxepsGV9qF4zYUPJgE42ZZZXS2G9zaBQUCBP//AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAHAAAAAAAAAM1s/DQpN7I7G907v5chqlYVrJ/1CnXFUn1EHNMnaCbJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADrzm117Qj8NlEllyDkV4Pae4UgsPjgVXtAA5UsG90gVgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHgz6GaO6bkxfPLBYcR5rEf9Itrt81OEanXteSMcd/BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}";
@@ -63,7 +64,7 @@ public class SigningCertificateTest extends TestCase {
}
public void testBadChain() throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException, CertPathValidatorException, SignatureException {
String certificateChain = "-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgAA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A";
String certificateChain = URLDecoder.decode("-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgAA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A");
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(getClass().getResourceAsStream("/ias.jks"), "whisper".toCharArray());

View File

@@ -0,0 +1,31 @@
package org.whispersystems.signalservice.internal.registrationpin;
import org.junit.Test;
public final class PinStretchFailureTest {
@Test(expected = InvalidPinException.class)
public void non_numeric_pin() throws InvalidPinException {
PinStretcher.stretchPin("A");
}
@Test(expected = InvalidPinException.class)
public void empty() throws InvalidPinException {
PinStretcher.stretchPin("");
}
@Test(expected = InvalidPinException.class)
public void too_few_digits() throws InvalidPinException {
PinStretcher.stretchPin("123");
}
@Test(expected = AssertionError.class)
public void pin_key_2_too_short() throws InvalidPinException {
PinStretcher.stretchPin("0000").withPinKey2(new byte[31]);
}
@Test(expected = AssertionError.class)
public void pin_key_2_too_long() throws InvalidPinException {
PinStretcher.stretchPin("0000").withPinKey2(new byte[33]);
}
}

View File

@@ -0,0 +1,127 @@
package org.whispersystems.signalservice.internal.registrationpin;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.whispersystems.signalservice.internal.util.Hex;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public final class PinStretchTest {
private final String pin;
private final byte[] expectedStretchedPin;
private final byte[] expectedKeyPin1;
private final byte[] pinKey2;
private final byte[] expectedMasterKey;
private final String expectedRegistrationLock;
private final byte[] expectedKbsAccessKey;
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[]{
"12345",
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"892f2cab29c09b13718e5f06a3e4aa0dd42cd7e0b20c411668eed10bb06f72b2",
"65cdbc33682f3be3c8809f54ed41c8f2f85cfce23b77d2a8b435ccff9681071d",
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
},
new Object[]{
"12345",
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
"abababababababababababababababababababababababababababababababab",
"01198dc427cbf9c6b47f344654d75a263e53b992db73be44b201f357d072dc38",
"bd1f4e129cc705c26c2fcebd3fbc6e7db60caade89e6c465c68ed60aeedbb0c3",
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
},
new Object[]{
"١٢٣٤٥",
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"892f2cab29c09b13718e5f06a3e4aa0dd42cd7e0b20c411668eed10bb06f72b2",
"65cdbc33682f3be3c8809f54ed41c8f2f85cfce23b77d2a8b435ccff9681071d",
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
},
new Object[]{
"9876543210",
"1ec376ca694b5c1fb185be3864343aaa08829833153f3a72813e3e48cb3579b9",
"40f35cdc3f3325b037f9fedddd25c68b7ea9c3e50e6a1a81319c43263da7bec3",
"abababababababababababababababababababababababababababababababab",
"127a435c15be2528f4b735423f8ee558b789e8ea1f6fe64d144d5b21a87c4e06",
"348d327acb823b54a988cf6bea647a154e21da25cbb121a115c13b871dccd548",
"90aaa3156952db441a8c875e8e4abab3d48965df7f563fbfb39f567d1ec7354e",
},
new Object[]{
"9876543210",
"1ec376ca694b5c1fb185be3864343aaa08829833153f3a72813e3e48cb3579b9",
"40f35cdc3f3325b037f9fedddd25c68b7ea9c3e50e6a1a81319c43263da7bec3",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"128833dbde1af3da703852b6b5a845e226fe9c7e069427b9c1e41279c0cdfb3a",
"6be0b17899cfb5c4316b92acc7db3b6a2fa5b9a19ef3e58a1c84a4de49230aa6",
"90aaa3156952db441a8c875e8e4abab3d48965df7f563fbfb39f567d1ec7354e",
},
new Object[]{
"0123",
"b9bc227d893edc7cade32d16ba210599f9e901c721bcad85ad458ab90432cbe7",
"bb8c8fc51b705dcdce43467ad7417fa5f28708941bcc9682fc4123a006701567",
"abababababababababababababababababababababababababababababababab",
"ca94b0a7b26d44078ccfcb88fd67151d891b3b8eb8c65ab94d536c3cb0e1d7dd",
"d182bde40ee91969192d5166fc871cd4bf5e261b090bbc707354bddb29fb8290",
"c0ae6e108296e507ee9ebd7fd5d8564b8e644bd53d50a2fc7ab379aea8074a91"
},
new Object[]{
"௦௧௨௩",
"b9bc227d893edc7cade32d16ba210599f9e901c721bcad85ad458ab90432cbe7",
"bb8c8fc51b705dcdce43467ad7417fa5f28708941bcc9682fc4123a006701567",
"abababababababababababababababababababababababababababababababab",
"ca94b0a7b26d44078ccfcb88fd67151d891b3b8eb8c65ab94d536c3cb0e1d7dd",
"d182bde40ee91969192d5166fc871cd4bf5e261b090bbc707354bddb29fb8290",
"c0ae6e108296e507ee9ebd7fd5d8564b8e644bd53d50a2fc7ab379aea8074a91"
});
}
public PinStretchTest(String pin,
String expectedStretchedPin,
String expectedKeyPin1,
String pinKey2,
String expectedMasterKey,
String expectedRegistrationLock,
String expectedKbsAccessKey) throws IOException {
this.pin = pin;
this.expectedStretchedPin = Hex.fromStringCondensed(expectedStretchedPin);
this.expectedKeyPin1 = Hex.fromStringCondensed(expectedKeyPin1);
this.pinKey2 = Hex.fromStringCondensed(pinKey2);
this.expectedMasterKey = Hex.fromStringCondensed(expectedMasterKey);
this.expectedRegistrationLock = expectedRegistrationLock;
this.expectedKbsAccessKey = Hex.fromStringCondensed(expectedKbsAccessKey);
}
@Test
public void stretch_pin() throws InvalidPinException {
PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin);
assertArrayEquals(expectedStretchedPin, stretchedPin.getStretchedPin());
assertArrayEquals(expectedKeyPin1, stretchedPin.getPinKey1());
assertArrayEquals(expectedKbsAccessKey, stretchedPin.getKbsAccessKey());
PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(pinKey2);
assertArrayEquals(pinKey2, masterKey.getPinKey2());
assertArrayEquals(expectedMasterKey, masterKey.getMasterKey());
assertEquals(expectedRegistrationLock, masterKey.getRegistrationLock());
assertArrayEquals(expectedStretchedPin, masterKey.getStretchedPin());
assertArrayEquals(expectedKeyPin1, masterKey.getPinKey1());
assertArrayEquals(expectedKbsAccessKey, masterKey.getKbsAccessKey());
}
}