mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
775 lines
30 KiB
Matlab
775 lines
30 KiB
Matlab
5 years ago
|
//
|
||
|
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||
|
//
|
||
|
|
||
|
#import "ContactDiscoveryService.h"
|
||
|
#import "CDSQuote.h"
|
||
|
#import "CDSSigningCertificate.h"
|
||
|
#import "NSError+MessageSending.h"
|
||
|
#import "OWSError.h"
|
||
|
#import "OWSRequestFactory.h"
|
||
|
#import "SSKEnvironment.h"
|
||
|
#import "TSNetworkManager.h"
|
||
|
#import <Curve25519Kit/Curve25519.h>
|
||
|
#import <HKDFKit/HKDFKit.h>
|
||
|
#import "SSKAsserts.h"
|
||
|
#import <SessionProtocolKit/SessionProtocolKit.h>
|
||
|
|
||
|
NS_ASSUME_NONNULL_BEGIN
|
||
|
|
||
|
NSErrorUserInfoKey const ContactDiscoveryServiceErrorKey_Reason = @"ContactDiscoveryServiceErrorKey_Reason";
|
||
|
NSErrorDomain const ContactDiscoveryServiceErrorDomain = @"SignalServiceKit.ContactDiscoveryService";
|
||
|
|
||
|
NSError *ContactDiscoveryServiceErrorMakeWithReason(NSInteger code, NSString *reason)
|
||
|
{
|
||
|
OWSCFailDebug(@"Error: %@", reason);
|
||
|
|
||
|
return [NSError errorWithDomain:ContactDiscoveryServiceErrorDomain
|
||
|
code:code
|
||
|
userInfo:@{ ContactDiscoveryServiceErrorKey_Reason : reason }];
|
||
|
}
|
||
|
|
||
|
@interface RemoteAttestationAuth ()
|
||
|
|
||
|
@property (nonatomic) NSString *username;
|
||
|
@property (nonatomic) NSString *password;
|
||
|
|
||
|
@end
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
@implementation RemoteAttestationAuth
|
||
|
|
||
|
@end
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
@interface RemoteAttestationKeys ()
|
||
|
|
||
|
@property (nonatomic) ECKeyPair *keyPair;
|
||
|
@property (nonatomic) NSData *serverEphemeralPublic;
|
||
|
@property (nonatomic) NSData *serverStaticPublic;
|
||
|
|
||
|
@property (nonatomic) OWSAES256Key *clientKey;
|
||
|
@property (nonatomic) OWSAES256Key *serverKey;
|
||
|
|
||
|
@end
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
@implementation RemoteAttestationKeys
|
||
|
|
||
|
+ (nullable RemoteAttestationKeys *)keysForKeyPair:(ECKeyPair *)keyPair
|
||
|
serverEphemeralPublic:(NSData *)serverEphemeralPublic
|
||
|
serverStaticPublic:(NSData *)serverStaticPublic
|
||
|
error:(NSError **)error
|
||
|
{
|
||
|
if (!keyPair) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"Missing keyPair");
|
||
|
return nil;
|
||
|
}
|
||
|
if (serverEphemeralPublic.length < 1) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"Invalid serverEphemeralPublic");
|
||
|
return nil;
|
||
|
}
|
||
|
if (serverStaticPublic.length < 1) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"Invalid serverStaticPublic");
|
||
|
return nil;
|
||
|
}
|
||
|
RemoteAttestationKeys *keys = [RemoteAttestationKeys new];
|
||
|
keys.keyPair = keyPair;
|
||
|
keys.serverEphemeralPublic = serverEphemeralPublic;
|
||
|
keys.serverStaticPublic = serverStaticPublic;
|
||
|
if (![keys deriveKeys]) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"failed to derive keys");
|
||
|
return nil;
|
||
|
}
|
||
|
return keys;
|
||
|
}
|
||
|
|
||
|
// Returns YES on success.
|
||
|
- (BOOL)deriveKeys
|
||
|
{
|
||
|
NSData *ephemeralToEphemeral;
|
||
|
NSData *ephemeralToStatic;
|
||
|
@try {
|
||
|
ephemeralToEphemeral =
|
||
|
[Curve25519 generateSharedSecretFromPublicKey:self.serverEphemeralPublic andKeyPair:self.keyPair];
|
||
|
ephemeralToStatic =
|
||
|
[Curve25519 generateSharedSecretFromPublicKey:self.serverStaticPublic andKeyPair:self.keyPair];
|
||
|
} @catch (NSException *exception) {
|
||
|
OWSFailDebug(@"could not generate shared secrets: %@", exception);
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
NSData *masterSecret = [ephemeralToEphemeral dataByAppendingData:ephemeralToStatic];
|
||
|
NSData *publicKeys = [NSData join:@[
|
||
|
self.keyPair.publicKey,
|
||
|
self.serverEphemeralPublic,
|
||
|
self.serverStaticPublic,
|
||
|
]];
|
||
|
|
||
|
NSData *_Nullable derivedMaterial;
|
||
|
@try {
|
||
|
derivedMaterial =
|
||
|
[HKDFKit deriveKey:masterSecret info:nil salt:publicKeys outputSize:(int)kAES256_KeyByteLength * 2];
|
||
|
} @catch (NSException *exception) {
|
||
|
OWSFailDebug(@"could not derive service key: %@", exception);
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
if (!derivedMaterial) {
|
||
|
OWSFailDebug(@"missing derived service key.");
|
||
|
return NO;
|
||
|
}
|
||
|
if (derivedMaterial.length != kAES256_KeyByteLength * 2) {
|
||
|
OWSFailDebug(@"derived service key has unexpected length.");
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
NSData *_Nullable clientKeyData =
|
||
|
[derivedMaterial subdataWithRange:NSMakeRange(kAES256_KeyByteLength * 0, kAES256_KeyByteLength)];
|
||
|
OWSAES256Key *_Nullable clientKey = [OWSAES256Key keyWithData:clientKeyData];
|
||
|
if (!clientKey) {
|
||
|
OWSFailDebug(@"clientKey has unexpected length.");
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
NSData *_Nullable serverKeyData =
|
||
|
[derivedMaterial subdataWithRange:NSMakeRange(kAES256_KeyByteLength * 1, kAES256_KeyByteLength)];
|
||
|
OWSAES256Key *_Nullable serverKey = [OWSAES256Key keyWithData:serverKeyData];
|
||
|
if (!serverKey) {
|
||
|
OWSFailDebug(@"serverKey has unexpected length.");
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
self.clientKey = clientKey;
|
||
|
self.serverKey = serverKey;
|
||
|
|
||
|
return YES;
|
||
|
}
|
||
|
|
||
|
@end
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
@interface RemoteAttestation ()
|
||
|
|
||
|
@property (nonatomic) RemoteAttestationKeys *keys;
|
||
|
@property (nonatomic) NSArray<NSHTTPCookie *> *cookies;
|
||
|
@property (nonatomic) NSData *requestId;
|
||
|
@property (nonatomic) NSString *enclaveId;
|
||
|
@property (nonatomic) RemoteAttestationAuth *auth;
|
||
|
|
||
|
@end
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
@implementation RemoteAttestation
|
||
|
|
||
|
@end
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
@interface SignatureBodyEntity : NSObject
|
||
|
|
||
|
@property (nonatomic) NSData *isvEnclaveQuoteBody;
|
||
|
@property (nonatomic) NSString *isvEnclaveQuoteStatus;
|
||
|
@property (nonatomic) NSString *timestamp;
|
||
|
@property (nonatomic) NSNumber *version;
|
||
|
|
||
|
@end
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
@implementation SignatureBodyEntity
|
||
|
|
||
|
@end
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
@interface NSDictionary (CDS)
|
||
|
|
||
|
@end
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
@implementation NSDictionary (CDS)
|
||
|
|
||
|
- (nullable NSString *)stringForKey:(NSString *)key
|
||
|
{
|
||
|
NSString *_Nullable valueString = self[key];
|
||
|
if (![valueString isKindOfClass:[NSString class]]) {
|
||
|
OWSFailDebug(@"couldn't parse string for key: %@", key);
|
||
|
return nil;
|
||
|
}
|
||
|
return valueString;
|
||
|
}
|
||
|
|
||
|
- (nullable NSNumber *)numberForKey:(NSString *)key
|
||
|
{
|
||
|
NSNumber *_Nullable value = self[key];
|
||
|
if (![value isKindOfClass:[NSNumber class]]) {
|
||
|
OWSFailDebug(@"couldn't parse number for key: %@", key);
|
||
|
return nil;
|
||
|
}
|
||
|
return value;
|
||
|
}
|
||
|
|
||
|
- (nullable NSData *)base64DataForKey:(NSString *)key
|
||
|
{
|
||
|
NSString *_Nullable valueString = self[key];
|
||
|
if (![valueString isKindOfClass:[NSString class]]) {
|
||
|
OWSFailDebug(@"couldn't parse base 64 value for key: %@", key);
|
||
|
return nil;
|
||
|
}
|
||
|
NSData *_Nullable valueData = [[NSData alloc] initWithBase64EncodedString:valueString options:0];
|
||
|
if (!valueData) {
|
||
|
OWSFailDebug(@"couldn't decode base 64 value for key: %@", key);
|
||
|
return nil;
|
||
|
}
|
||
|
return valueData;
|
||
|
}
|
||
|
|
||
|
- (nullable NSData *)base64DataForKey:(NSString *)key expectedLength:(NSUInteger)expectedLength
|
||
|
{
|
||
|
NSData *_Nullable valueData = [self base64DataForKey:key];
|
||
|
if (valueData && valueData.length != expectedLength) {
|
||
|
OWSLogDebug(@"decoded base 64 value for key: %@, has unexpected length: %lu != %lu",
|
||
|
key,
|
||
|
(unsigned long)valueData.length,
|
||
|
(unsigned long)expectedLength);
|
||
|
OWSFailDebug(@"decoded base 64 value for key has unexpected length: %lu != %lu",
|
||
|
(unsigned long)valueData.length,
|
||
|
(unsigned long)expectedLength);
|
||
|
return nil;
|
||
|
}
|
||
|
return valueData;
|
||
|
}
|
||
|
|
||
|
@end
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
@implementation ContactDiscoveryService
|
||
|
|
||
|
+ (instancetype)shared
|
||
|
{
|
||
|
OWSAssertDebug(SSKEnvironment.shared.contactDiscoveryService);
|
||
|
|
||
|
return SSKEnvironment.shared.contactDiscoveryService;
|
||
|
}
|
||
|
|
||
|
- (instancetype)initDefault
|
||
|
{
|
||
|
self = [super init];
|
||
|
if (!self) {
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
OWSSingletonAssert();
|
||
|
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
- (void)testService
|
||
|
{
|
||
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||
|
[self
|
||
|
performRemoteAttestationWithSuccess:^(RemoteAttestation *remoteAttestation) {
|
||
|
OWSLogDebug(@"succeeded");
|
||
|
}
|
||
|
failure:^(NSError *error) {
|
||
|
OWSLogDebug(@"failed with error: %@", error);
|
||
|
}];
|
||
|
});
|
||
|
}
|
||
|
|
||
|
- (void)performRemoteAttestationWithSuccess:(void (^)(RemoteAttestation *remoteAttestation))successHandler
|
||
|
failure:(void (^)(NSError *error))failureHandler
|
||
|
{
|
||
|
[self
|
||
|
getRemoteAttestationAuthWithSuccess:^(RemoteAttestationAuth *auth) {
|
||
|
[self performRemoteAttestationWithAuth:auth success:successHandler failure:failureHandler];
|
||
|
}
|
||
|
failure:failureHandler];
|
||
|
}
|
||
|
|
||
|
- (void)getRemoteAttestationAuthWithSuccess:(void (^)(RemoteAttestationAuth *))successHandler
|
||
|
failure:(void (^)(NSError *error))failureHandler
|
||
|
{
|
||
|
TSRequest *request = [OWSRequestFactory remoteAttestationAuthRequest];
|
||
|
[[TSNetworkManager sharedManager] makeRequest:request
|
||
|
success:^(NSURLSessionDataTask *task, id responseDict) {
|
||
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||
|
RemoteAttestationAuth *_Nullable auth = [self parseAuthParams:responseDict];
|
||
|
if (!auth) {
|
||
|
OWSLogError(@"remote attestation auth could not be parsed: %@", responseDict);
|
||
|
NSError *error = OWSErrorMakeUnableToProcessServerResponseError();
|
||
|
failureHandler(error);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
successHandler(auth);
|
||
|
});
|
||
|
}
|
||
|
failure:^(NSURLSessionDataTask *task, NSError *error) {
|
||
|
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
|
||
|
OWSLogVerbose(@"remote attestation auth failure: %lu", (unsigned long)response.statusCode);
|
||
|
failureHandler(error);
|
||
|
}];
|
||
|
}
|
||
|
|
||
|
- (nullable RemoteAttestationAuth *)parseAuthParams:(id)response
|
||
|
{
|
||
|
if (![response isKindOfClass:[NSDictionary class]]) {
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
NSDictionary *responseDict = response;
|
||
|
NSString *_Nullable password = [responseDict stringForKey:@"password"];
|
||
|
if (password.length < 1) {
|
||
|
OWSFailDebug(@"missing or empty password.");
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
NSString *_Nullable username = [responseDict stringForKey:@"username"];
|
||
|
if (username.length < 1) {
|
||
|
OWSFailDebug(@"missing or empty username.");
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
RemoteAttestationAuth *result = [RemoteAttestationAuth new];
|
||
|
result.username = username;
|
||
|
result.password = password;
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
- (void)performRemoteAttestationWithAuth:(RemoteAttestationAuth *)auth
|
||
|
success:(void (^)(RemoteAttestation *remoteAttestation))successHandler
|
||
|
failure:(void (^)(NSError *error))failureHandler
|
||
|
{
|
||
|
return; // Loki: Do nothing
|
||
|
|
||
|
ECKeyPair *keyPair = [Curve25519 generateKeyPair];
|
||
|
|
||
|
NSString *enclaveId = @"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9";
|
||
|
|
||
|
TSRequest *request = [OWSRequestFactory remoteAttestationRequest:keyPair
|
||
|
enclaveId:enclaveId
|
||
|
authUsername:auth.username
|
||
|
authPassword:auth.password];
|
||
|
|
||
|
[[TSNetworkManager sharedManager] makeRequest:request
|
||
|
success:^(NSURLSessionDataTask *task, id responseJson) {
|
||
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||
|
NSError *_Nullable error;
|
||
|
RemoteAttestation *_Nullable attestation = [self parseAttestationResponseJson:responseJson
|
||
|
response:task.response
|
||
|
keyPair:keyPair
|
||
|
enclaveId:enclaveId
|
||
|
auth:auth
|
||
|
error:&error];
|
||
|
|
||
|
if (!attestation) {
|
||
|
if (!error) {
|
||
|
OWSFailDebug(@"error was unexpectedly nil");
|
||
|
error = ContactDiscoveryServiceErrorMakeWithReason(ContactDiscoveryServiceErrorAssertionError,
|
||
|
@"failure when parsing attestation - no reason given");
|
||
|
} else {
|
||
|
OWSFailDebug(@"error with attestation: %@", error);
|
||
|
}
|
||
|
error.isRetryable = NO;
|
||
|
failureHandler(error);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
successHandler(attestation);
|
||
|
});
|
||
|
}
|
||
|
failure:^(NSURLSessionDataTask *task, NSError *error) {
|
||
|
failureHandler(error);
|
||
|
}];
|
||
|
}
|
||
|
|
||
|
- (nullable RemoteAttestation *)parseAttestationResponseJson:(id)responseJson
|
||
|
response:(NSURLResponse *)response
|
||
|
keyPair:(ECKeyPair *)keyPair
|
||
|
enclaveId:(NSString *)enclaveId
|
||
|
auth:(RemoteAttestationAuth *)auth
|
||
|
error:(NSError **)error
|
||
|
{
|
||
|
OWSAssertDebug(responseJson);
|
||
|
OWSAssertDebug(response);
|
||
|
OWSAssertDebug(keyPair);
|
||
|
OWSAssertDebug(enclaveId.length > 0);
|
||
|
|
||
|
if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(ContactDiscoveryServiceErrorAssertionError, @"unexpected response type.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
||
|
NSArray<NSHTTPCookie *> *cookies =
|
||
|
[NSHTTPCookie cookiesWithResponseHeaderFields:httpResponse.allHeaderFields forURL:httpResponse.URL];
|
||
|
if (cookies.count < 1) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"couldn't parse cookie.");
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
if (![responseJson isKindOfClass:[NSDictionary class]]) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"invalid json response");
|
||
|
return nil;
|
||
|
}
|
||
|
NSDictionary *responseDict = responseJson;
|
||
|
NSData *_Nullable serverEphemeralPublic =
|
||
|
[responseDict base64DataForKey:@"serverEphemeralPublic" expectedLength:32];
|
||
|
if (!serverEphemeralPublic) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"couldn't parse serverEphemeralPublic.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSData *_Nullable serverStaticPublic = [responseDict base64DataForKey:@"serverStaticPublic" expectedLength:32];
|
||
|
if (!serverStaticPublic) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"couldn't parse serverStaticPublic.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSData *_Nullable encryptedRequestId = [responseDict base64DataForKey:@"ciphertext"];
|
||
|
if (!encryptedRequestId) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"couldn't parse encryptedRequestId.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSData *_Nullable encryptedRequestIv = [responseDict base64DataForKey:@"iv" expectedLength:12];
|
||
|
if (!encryptedRequestIv) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"couldn't parse encryptedRequestIv.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSData *_Nullable encryptedRequestTag = [responseDict base64DataForKey:@"tag" expectedLength:16];
|
||
|
if (!encryptedRequestTag) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"couldn't parse encryptedRequestTag.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSData *_Nullable quoteData = [responseDict base64DataForKey:@"quote"];
|
||
|
if (!quoteData) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"couldn't parse quote data.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSString *_Nullable signatureBody = [responseDict stringForKey:@"signatureBody"];
|
||
|
if (![signatureBody isKindOfClass:[NSString class]]) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"couldn't parse signatureBody.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSData *_Nullable signature = [responseDict base64DataForKey:@"signature"];
|
||
|
if (!signature) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"couldn't parse signature.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSString *_Nullable encodedCertificates = [responseDict stringForKey:@"certificates"];
|
||
|
if (![encodedCertificates isKindOfClass:[NSString class]]) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"couldn't parse encodedCertificates.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSString *_Nullable certificates = [encodedCertificates stringByRemovingPercentEncoding];
|
||
|
if (!certificates) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"couldn't parse certificates.");
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
RemoteAttestationKeys *_Nullable keys = [RemoteAttestationKeys keysForKeyPair:keyPair
|
||
|
serverEphemeralPublic:serverEphemeralPublic
|
||
|
serverStaticPublic:serverStaticPublic
|
||
|
error:error];
|
||
|
if (!keys || *error != nil) {
|
||
|
if (*error == nil) {
|
||
|
OWSFailDebug(@"missing error specifics");
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"Couldn't derive keys. No reason given");
|
||
|
}
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
CDSQuote *_Nullable quote = [CDSQuote parseQuoteFromData:quoteData];
|
||
|
if (!quote) {
|
||
|
OWSFailDebug(@"couldn't parse quote.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSData *_Nullable requestId = [self decryptRequestId:encryptedRequestId
|
||
|
encryptedRequestIv:encryptedRequestIv
|
||
|
encryptedRequestTag:encryptedRequestTag
|
||
|
keys:keys];
|
||
|
if (!requestId) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"couldn't decrypt request id.");
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
if (![self verifyServerQuote:quote keys:keys enclaveId:enclaveId]) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAttestationFailed, @"couldn't verify quote.");
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
if (![self verifyIasSignatureWithCertificates:certificates
|
||
|
signatureBody:signatureBody
|
||
|
signature:signature
|
||
|
quoteData:quoteData
|
||
|
error:error]) {
|
||
|
|
||
|
if (*error == nil) {
|
||
|
OWSFailDebug(@"missing error specifics");
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(ContactDiscoveryServiceErrorAssertionError,
|
||
|
@"verifyIasSignatureWithCertificates failed. No reason given");
|
||
|
}
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
RemoteAttestation *result = [RemoteAttestation new];
|
||
|
result.cookies = cookies;
|
||
|
result.keys = keys;
|
||
|
result.requestId = requestId;
|
||
|
result.enclaveId = enclaveId;
|
||
|
result.auth = auth;
|
||
|
|
||
|
OWSLogVerbose(@"remote attestation complete.");
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
- (BOOL)verifyIasSignatureWithCertificates:(NSString *)certificates
|
||
|
signatureBody:(NSString *)signatureBody
|
||
|
signature:(NSData *)signature
|
||
|
quoteData:(NSData *)quoteData
|
||
|
error:(NSError **)error
|
||
|
{
|
||
|
OWSAssertDebug(certificates.length > 0);
|
||
|
OWSAssertDebug(signatureBody.length > 0);
|
||
|
OWSAssertDebug(signature.length > 0);
|
||
|
OWSAssertDebug(quoteData);
|
||
|
|
||
|
NSError *signingError;
|
||
|
CDSSigningCertificate *_Nullable certificate =
|
||
|
[CDSSigningCertificate parseCertificateFromPem:certificates error:&signingError];
|
||
|
if (signingError) {
|
||
|
*error = signingError;
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
if (!certificate) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"could not parse signing certificate.");
|
||
|
return NO;
|
||
|
}
|
||
|
if (![certificate verifySignatureOfBody:signatureBody signature:signature]) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAttestationFailed, @"could not verify signature.");
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
SignatureBodyEntity *_Nullable signatureBodyEntity = [self parseSignatureBodyEntity:signatureBody];
|
||
|
if (!signatureBodyEntity) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"could not parse signature body.");
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
// Compare the first N bytes of the quote data with the signed quote body.
|
||
|
const NSUInteger kQuoteBodyComparisonLength = 432;
|
||
|
if (signatureBodyEntity.isvEnclaveQuoteBody.length < kQuoteBodyComparisonLength) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"isvEnclaveQuoteBody has unexpected length.");
|
||
|
return NO;
|
||
|
}
|
||
|
// NOTE: This version is separate from and does _NOT_ match the CDS quote version.
|
||
|
const NSUInteger kSignatureBodyVersion = 3;
|
||
|
if (![signatureBodyEntity.version isEqual:@(kSignatureBodyVersion)]) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"signatureBodyEntity has unexpected version.");
|
||
|
return NO;
|
||
|
}
|
||
|
if (quoteData.length < kQuoteBodyComparisonLength) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"quoteData has unexpected length.");
|
||
|
return NO;
|
||
|
}
|
||
|
NSData *isvEnclaveQuoteBodyForComparison =
|
||
|
[signatureBodyEntity.isvEnclaveQuoteBody subdataWithRange:NSMakeRange(0, kQuoteBodyComparisonLength)];
|
||
|
NSData *quoteDataForComparison = [quoteData subdataWithRange:NSMakeRange(0, kQuoteBodyComparisonLength)];
|
||
|
if (![isvEnclaveQuoteBodyForComparison ows_constantTimeIsEqualToData:quoteDataForComparison]) {
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAttestationFailed, @"isvEnclaveQuoteBody and quoteData do not match.");
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
if (![@"OK" isEqualToString:signatureBodyEntity.isvEnclaveQuoteStatus]) {
|
||
|
NSString *reason =
|
||
|
[NSString stringWithFormat:@"invalid isvEnclaveQuoteStatus: %@", signatureBodyEntity.isvEnclaveQuoteStatus];
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(ContactDiscoveryServiceErrorAttestationFailed, reason);
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||
|
NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
|
||
|
[dateFormatter setTimeZone:timeZone];
|
||
|
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSSSS"];
|
||
|
|
||
|
// Specify parsing locale
|
||
|
// from: https://developer.apple.com/library/archive/qa/qa1480/_index.html
|
||
|
// Q: I'm using NSDateFormatter to parse an Internet-style date, but this fails for some users in some regions.
|
||
|
// I've set a specific date format string; shouldn't that force NSDateFormatter to work independently of the user's
|
||
|
// region settings? A: No. While setting a date format string will appear to work for most users, it's not the right
|
||
|
// solution to this problem. There are many places where format strings behave in unexpected ways. [...]
|
||
|
NSLocale *enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
|
||
|
[dateFormatter setLocale:enUSPOSIXLocale];
|
||
|
NSDate *timestampDate = [dateFormatter dateFromString:signatureBodyEntity.timestamp];
|
||
|
if (!timestampDate) {
|
||
|
OWSFailDebug(@"Could not parse signature body timestamp: %@", signatureBodyEntity.timestamp);
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAssertionError, @"could not parse signature body timestamp.");
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
// Only accept signatures from the last 24 hours.
|
||
|
NSDateComponents *dayComponent = [[NSDateComponents alloc] init];
|
||
|
dayComponent.day = 1;
|
||
|
NSCalendar *calendar = [NSCalendar currentCalendar];
|
||
|
NSDate *timestampDatePlus1Day = [calendar dateByAddingComponents:dayComponent toDate:timestampDate options:0];
|
||
|
|
||
|
NSDate *now = [NSDate new];
|
||
|
BOOL isExpired = [now isAfterDate:timestampDatePlus1Day];
|
||
|
|
||
|
if (isExpired) {
|
||
|
OWSFailDebug(@"Signature is expired: %@", signatureBodyEntity.timestamp);
|
||
|
*error = ContactDiscoveryServiceErrorMakeWithReason(
|
||
|
ContactDiscoveryServiceErrorAttestationFailed, @"Signature is expired.");
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
return YES;
|
||
|
}
|
||
|
|
||
|
- (nullable SignatureBodyEntity *)parseSignatureBodyEntity:(NSString *)signatureBody
|
||
|
{
|
||
|
OWSAssertDebug(signatureBody.length > 0);
|
||
|
|
||
|
NSError *error = nil;
|
||
|
NSDictionary *_Nullable jsonDict =
|
||
|
[NSJSONSerialization JSONObjectWithData:[signatureBody dataUsingEncoding:NSUTF8StringEncoding]
|
||
|
options:0
|
||
|
error:&error];
|
||
|
if (error || ![jsonDict isKindOfClass:[NSDictionary class]]) {
|
||
|
OWSFailDebug(@"could not parse signature body JSON: %@.", error);
|
||
|
return nil;
|
||
|
}
|
||
|
NSString *_Nullable timestamp = [jsonDict stringForKey:@"timestamp"];
|
||
|
if (timestamp.length < 1) {
|
||
|
OWSFailDebug(@"could not parse signature timestamp.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSData *_Nullable isvEnclaveQuoteBody = [jsonDict base64DataForKey:@"isvEnclaveQuoteBody"];
|
||
|
if (isvEnclaveQuoteBody.length < 1) {
|
||
|
OWSFailDebug(@"could not parse signature isvEnclaveQuoteBody.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSString *_Nullable isvEnclaveQuoteStatus = [jsonDict stringForKey:@"isvEnclaveQuoteStatus"];
|
||
|
if (isvEnclaveQuoteStatus.length < 1) {
|
||
|
OWSFailDebug(@"could not parse signature isvEnclaveQuoteStatus.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSNumber *_Nullable version = [jsonDict numberForKey:@"version"];
|
||
|
if (!version) {
|
||
|
OWSFailDebug(@"could not parse signature version.");
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
SignatureBodyEntity *result = [SignatureBodyEntity new];
|
||
|
result.isvEnclaveQuoteBody = isvEnclaveQuoteBody;
|
||
|
result.isvEnclaveQuoteStatus = isvEnclaveQuoteStatus;
|
||
|
result.timestamp = timestamp;
|
||
|
result.version = version;
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
- (BOOL)verifyServerQuote:(CDSQuote *)quote keys:(RemoteAttestationKeys *)keys enclaveId:(NSString *)enclaveId
|
||
|
{
|
||
|
OWSAssertDebug(quote);
|
||
|
OWSAssertDebug(keys);
|
||
|
OWSAssertDebug(enclaveId.length > 0);
|
||
|
|
||
|
if (quote.reportData.length < keys.serverStaticPublic.length) {
|
||
|
OWSFailDebug(@"reportData has unexpected length: %lu != %lu.",
|
||
|
(unsigned long)quote.reportData.length,
|
||
|
(unsigned long)keys.serverStaticPublic.length);
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
NSData *_Nullable theirServerPublicStatic =
|
||
|
[quote.reportData subdataWithRange:NSMakeRange(0, keys.serverStaticPublic.length)];
|
||
|
if (theirServerPublicStatic.length != keys.serverStaticPublic.length) {
|
||
|
OWSFailDebug(@"could not extract server public static.");
|
||
|
return NO;
|
||
|
}
|
||
|
if (![keys.serverStaticPublic ows_constantTimeIsEqualToData:theirServerPublicStatic]) {
|
||
|
OWSFailDebug(@"server public statics do not match.");
|
||
|
return NO;
|
||
|
}
|
||
|
// It's easier to compare as hex data than parsing hexadecimal.
|
||
|
NSData *_Nullable ourEnclaveIdHexData = [enclaveId dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
NSData *_Nullable theirEnclaveIdHexData =
|
||
|
[quote.mrenclave.hexadecimalString dataUsingEncoding:NSUTF8StringEncoding];
|
||
|
if (!ourEnclaveIdHexData || !theirEnclaveIdHexData
|
||
|
|| ![ourEnclaveIdHexData ows_constantTimeIsEqualToData:theirEnclaveIdHexData]) {
|
||
|
OWSFailDebug(@"enclave ids do not match.");
|
||
|
return NO;
|
||
|
}
|
||
|
if (quote.isDebugQuote) {
|
||
|
OWSFailDebug(@"quote has invalid isDebugQuote value.");
|
||
|
return NO;
|
||
|
}
|
||
|
return YES;
|
||
|
}
|
||
|
|
||
|
- (nullable NSData *)decryptRequestId:(NSData *)encryptedRequestId
|
||
|
encryptedRequestIv:(NSData *)encryptedRequestIv
|
||
|
encryptedRequestTag:(NSData *)encryptedRequestTag
|
||
|
keys:(RemoteAttestationKeys *)keys
|
||
|
{
|
||
|
OWSAssertDebug(encryptedRequestId.length > 0);
|
||
|
OWSAssertDebug(encryptedRequestIv.length > 0);
|
||
|
OWSAssertDebug(encryptedRequestTag.length > 0);
|
||
|
OWSAssertDebug(keys);
|
||
|
|
||
|
OWSAES256Key *_Nullable key = keys.serverKey;
|
||
|
if (!key) {
|
||
|
OWSFailDebug(@"invalid server key.");
|
||
|
return nil;
|
||
|
}
|
||
|
NSData *_Nullable decryptedData = [Cryptography decryptAESGCMWithInitializationVector:encryptedRequestIv
|
||
|
ciphertext:encryptedRequestId
|
||
|
additionalAuthenticatedData:nil
|
||
|
authTag:encryptedRequestTag
|
||
|
key:key];
|
||
|
if (!decryptedData) {
|
||
|
OWSFailDebug(@"couldn't decrypt request id.");
|
||
|
return nil;
|
||
|
}
|
||
|
return decryptedData;
|
||
|
}
|
||
|
|
||
|
@end
|
||
|
|
||
|
NS_ASSUME_NONNULL_END
|