|
|
|
@ -7,6 +7,7 @@
|
|
|
|
|
#import "CDSSigningCertificate.h"
|
|
|
|
|
#import "Cryptography.h"
|
|
|
|
|
#import "NSData+OWS.h"
|
|
|
|
|
#import "NSDate+OWS.h"
|
|
|
|
|
#import "OWSRequestFactory.h"
|
|
|
|
|
#import "TSNetworkManager.h"
|
|
|
|
|
#import <Curve25519Kit/Curve25519.h>
|
|
|
|
@ -124,6 +125,22 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
|
@interface SignatureBodyEntity : NSObject
|
|
|
|
|
|
|
|
|
|
@property (nonatomic) NSData *isvEnclaveQuoteBody;
|
|
|
|
|
@property (nonatomic) NSString *isvEnclaveQuoteStatus;
|
|
|
|
|
@property (nonatomic) NSString *timestamp;
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
|
@implementation SignatureBodyEntity
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
|
@interface NSDictionary (CDS)
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
@ -132,6 +149,16 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
|
|
|
|
|
@implementation NSDictionary (CDS)
|
|
|
|
|
|
|
|
|
|
- (nullable NSString *)stringForKey:(NSString *)key
|
|
|
|
|
{
|
|
|
|
|
NSString *_Nullable valueString = self[key];
|
|
|
|
|
if (![valueString isKindOfClass:[NSString class]]) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ couldn't parse string for key: %@", self.logTag, key);
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
return valueString;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (nullable NSData *)base64DataForKey:(NSString *)key
|
|
|
|
|
{
|
|
|
|
|
NSString *_Nullable valueString = self[key];
|
|
|
|
@ -207,7 +234,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
TSRequest *request = [OWSRequestFactory remoteAttestationAuthRequest];
|
|
|
|
|
[[TSNetworkManager sharedManager] makeRequest:request
|
|
|
|
|
success:^(NSURLSessionDataTask *task, id responseDict) {
|
|
|
|
|
DDLogVerbose(@"%@ remote attestation auth success: %@", self.logTag, responseDict);
|
|
|
|
|
|
|
|
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
|
|
RemoteAttestationAuth *_Nullable auth = [self parseAuthToken:responseDict];
|
|
|
|
@ -231,23 +257,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSDictionary *responseDict = response;
|
|
|
|
|
NSString *_Nullable token = responseDict[@"token"];
|
|
|
|
|
if (![token isKindOfClass:[NSString class]]) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ missing or invalid token.", self.logTag);
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
NSString *_Nullable token = [responseDict stringForKey:@"token"];
|
|
|
|
|
if (token.length < 1) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ empty token.", self.logTag);
|
|
|
|
|
OWSProdLogAndFail(@"%@ missing or empty token.", self.logTag);
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSString *_Nullable username = responseDict[@"username"];
|
|
|
|
|
if (![username isKindOfClass:[NSString class]]) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ missing or invalid username.", self.logTag);
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
NSString *_Nullable username = [responseDict stringForKey:@"username"];
|
|
|
|
|
if (username.length < 1) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ empty username.", self.logTag);
|
|
|
|
|
OWSProdLogAndFail(@"%@ missing or empty username.", self.logTag);
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -270,8 +288,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
authToken:auth.authToken];
|
|
|
|
|
[[TSNetworkManager sharedManager] makeRequest:request
|
|
|
|
|
success:^(NSURLSessionDataTask *task, id responseJson) {
|
|
|
|
|
DDLogVerbose(@"%@ remote attestation success: %@", self.logTag, responseJson);
|
|
|
|
|
|
|
|
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
|
|
// TODO: Handle result.
|
|
|
|
|
[self parseAttestationResponseJson:responseJson
|
|
|
|
@ -302,12 +318,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
}
|
|
|
|
|
NSDictionary *responseHeaders = ((NSHTTPURLResponse *)response).allHeaderFields;
|
|
|
|
|
|
|
|
|
|
NSString *_Nullable cookie = responseHeaders[@"Set-Cookie"];
|
|
|
|
|
if (![cookie isKindOfClass:[NSString class]]) {
|
|
|
|
|
NSString *_Nullable cookie = [responseHeaders stringForKey:@"Set-Cookie"];
|
|
|
|
|
if (cookie.length < 1) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ couldn't parse cookie.", self.logTag);
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
DDLogVerbose(@"%@ cookie: %@", self.logTag, cookie);
|
|
|
|
|
|
|
|
|
|
// The cookie header will have this form:
|
|
|
|
|
// Set-Cookie: __NSCFString, c2131364675-413235ic=c1656171-249545-958227; Path=/; Secure
|
|
|
|
@ -315,18 +330,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
NSRange cookieRange = [cookie rangeOfString:@";"];
|
|
|
|
|
if (cookieRange.length != NSNotFound) {
|
|
|
|
|
cookie = [cookie substringToIndex:cookieRange.location];
|
|
|
|
|
DDLogVerbose(@"%@ trimmed cookie: %@", self.logTag, cookie);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (![responseJson isKindOfClass:[NSDictionary class]]) {
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
NSDictionary *responseDict = responseJson;
|
|
|
|
|
DDLogVerbose(@"%@ parseAttestationResponse: %@", self.logTag, responseDict);
|
|
|
|
|
for (NSString *key in responseDict) {
|
|
|
|
|
id value = responseDict[key];
|
|
|
|
|
DDLogVerbose(@"%@ \t %@: %@, %@", self.logTag, key, [value class], value);
|
|
|
|
|
}
|
|
|
|
|
NSData *_Nullable serverEphemeralPublic =
|
|
|
|
|
[responseDict base64DataForKey:@"serverEphemeralPublic" expectedLength:32];
|
|
|
|
|
if (!serverEphemeralPublic) {
|
|
|
|
@ -358,7 +367,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
OWSProdLogAndFail(@"%@ couldn't parse quote data.", self.logTag);
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
NSString *_Nullable signatureBody = responseDict[@"signatureBody"];
|
|
|
|
|
NSString *_Nullable signatureBody = [responseDict stringForKey:@"signatureBody"];
|
|
|
|
|
if (![signatureBody isKindOfClass:[NSString class]]) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ couldn't parse signatureBody.", self.logTag);
|
|
|
|
|
return nil;
|
|
|
|
@ -368,7 +377,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
OWSProdLogAndFail(@"%@ couldn't parse signature.", self.logTag);
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
NSString *_Nullable encodedCertificates = responseDict[@"certificates"];
|
|
|
|
|
NSString *_Nullable encodedCertificates = [responseDict stringForKey:@"certificates"];
|
|
|
|
|
if (![encodedCertificates isKindOfClass:[NSString class]]) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ couldn't parse encodedCertificates.", self.logTag);
|
|
|
|
|
return nil;
|
|
|
|
@ -409,11 +418,18 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
if (![self verifyIasSignatureWithCertificates:certificates
|
|
|
|
|
signatureBody:signatureBody
|
|
|
|
|
signature:signature
|
|
|
|
|
quote:quote]) {
|
|
|
|
|
quoteData:quoteData]) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ couldn't verify ias signature.", self.logTag);
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RemoteAttestation *result = [RemoteAttestation new];
|
|
|
|
|
result.cookie = cookie;
|
|
|
|
|
result.keys = keys;
|
|
|
|
|
result.requestId = requestId;
|
|
|
|
|
|
|
|
|
|
DDLogVerbose(@"%@ remote attestation complete.", self.logTag);
|
|
|
|
|
|
|
|
|
|
//+ RemoteAttestation remoteAttestation = new RemoteAttestation(requestId, keys);
|
|
|
|
|
//+ List<String> addressBook = new LinkedList<>();
|
|
|
|
|
//+
|
|
|
|
@ -437,23 +453,18 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
//+
|
|
|
|
|
//+ return results;
|
|
|
|
|
|
|
|
|
|
RemoteAttestation *result = [RemoteAttestation new];
|
|
|
|
|
result.cookie = cookie;
|
|
|
|
|
result.keys = keys;
|
|
|
|
|
result.requestId = requestId;
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (BOOL)verifyIasSignatureWithCertificates:(NSString *)certificates
|
|
|
|
|
signatureBody:(NSString *)signatureBody
|
|
|
|
|
signature:(NSData *)signature
|
|
|
|
|
quote:(CDSQuote *)quote
|
|
|
|
|
quoteData:(NSData *)quoteData
|
|
|
|
|
{
|
|
|
|
|
OWSAssert(certificates.length > 0);
|
|
|
|
|
OWSAssert(signatureBody.length > 0);
|
|
|
|
|
OWSAssert(signature.length > 0);
|
|
|
|
|
OWSAssert(quote);
|
|
|
|
|
OWSAssert(quoteData);
|
|
|
|
|
|
|
|
|
|
CDSSigningCertificate *_Nullable certificate = [CDSSigningCertificate parseCertificateFromPem:certificates];
|
|
|
|
|
if (!certificate) {
|
|
|
|
@ -464,44 +475,102 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
OWSProdLogAndFail(@"%@ could not verify signature.", self.logTag);
|
|
|
|
|
return NO;
|
|
|
|
|
}
|
|
|
|
|
////public void verifyIasSignature(KeyStore trustStore, String certificates, String signatureBody, String signature,
|
|
|
|
|
///Quote quote) /throws SignatureException
|
|
|
|
|
////{
|
|
|
|
|
//// try {
|
|
|
|
|
// SigningCertificate signingCertificate = new SigningCertificate(certificates, trustStore);
|
|
|
|
|
// signingCertificate.verifySignature(signatureBody, signature);
|
|
|
|
|
//
|
|
|
|
|
// SignatureBodyEntity signatureBodyEntity = JsonUtil.fromJson(signatureBody, SignatureBodyEntity.class);
|
|
|
|
|
//
|
|
|
|
|
// 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()) &&
|
|
|
|
|
// !"GROUP_OUT_OF_DATE".equals(signatureBodyEntity.getIsvEnclaveQuoteStatus())) {
|
|
|
|
|
// // 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);
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
SignatureBodyEntity *_Nullable signatureBodyEntity = [self parseSignatureBodyEntity:signatureBody];
|
|
|
|
|
if (!signatureBodyEntity) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ could not parse signature body.", self.logTag);
|
|
|
|
|
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) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ isvEnclaveQuoteBody has unexpected length.", self.logTag);
|
|
|
|
|
return NO;
|
|
|
|
|
}
|
|
|
|
|
if (quoteData.length < kQuoteBodyComparisonLength) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ quoteData has unexpected length.", self.logTag);
|
|
|
|
|
return NO;
|
|
|
|
|
}
|
|
|
|
|
NSData *isvEnclaveQuoteBodyForComparison =
|
|
|
|
|
[signatureBodyEntity.isvEnclaveQuoteBody subdataWithRange:NSMakeRange(0, kQuoteBodyComparisonLength)];
|
|
|
|
|
NSData *quoteDataForComparison = [quoteData subdataWithRange:NSMakeRange(0, kQuoteBodyComparisonLength)];
|
|
|
|
|
if (![isvEnclaveQuoteBodyForComparison ows_constantTimeIsEqualToData:quoteDataForComparison]) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ isvEnclaveQuoteBody and quoteData do not match.", self.logTag);
|
|
|
|
|
return NO;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Before going to production, remove GROUP_OUT_OF_DATE.
|
|
|
|
|
if (![@"OK" isEqualToString:signatureBodyEntity.isvEnclaveQuoteStatus]
|
|
|
|
|
&& ![@"GROUP_OUT_OF_DATE" isEqualToString:signatureBodyEntity.isvEnclaveQuoteStatus]) {
|
|
|
|
|
OWSProdLogAndFail(
|
|
|
|
|
@"%@ invalid isvEnclaveQuoteStatus: %@.", self.logTag, signatureBodyEntity.isvEnclaveQuoteStatus);
|
|
|
|
|
return NO;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
|
|
|
|
NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
|
|
|
|
|
[dateFormatter setTimeZone:timeZone];
|
|
|
|
|
[dateFormatter setDateFormat:@"yyy-MM-dd'T'HH:mm:ss.SSSSSS"];
|
|
|
|
|
NSDate *timestampDate = [dateFormatter dateFromString:signatureBodyEntity.timestamp];
|
|
|
|
|
if (!timestampDate) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ could not parse signature body timestamp.", self.logTag);
|
|
|
|
|
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) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ Signature is expired.", self.logTag);
|
|
|
|
|
return NO;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return YES;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (nullable SignatureBodyEntity *)parseSignatureBodyEntity:(NSString *)signatureBody
|
|
|
|
|
{
|
|
|
|
|
OWSAssert(signatureBody.length > 0);
|
|
|
|
|
|
|
|
|
|
NSError *error = nil;
|
|
|
|
|
NSDictionary *_Nullable jsonDict =
|
|
|
|
|
[NSJSONSerialization JSONObjectWithData:[signatureBody dataUsingEncoding:NSUTF8StringEncoding]
|
|
|
|
|
options:0
|
|
|
|
|
error:&error];
|
|
|
|
|
if (error || ![jsonDict isKindOfClass:[NSDictionary class]]) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ could not parse signature body JSON: %@.", self.logTag, error);
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
NSString *_Nullable timestamp = [jsonDict stringForKey:@"timestamp"];
|
|
|
|
|
if (timestamp.length < 1) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ could not parse signature timestamp.", self.logTag);
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
NSData *_Nullable isvEnclaveQuoteBody = [jsonDict base64DataForKey:@"isvEnclaveQuoteBody"];
|
|
|
|
|
if (isvEnclaveQuoteBody.length < 1) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ could not parse signature isvEnclaveQuoteBody.", self.logTag);
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
NSString *_Nullable isvEnclaveQuoteStatus = [jsonDict stringForKey:@"isvEnclaveQuoteStatus"];
|
|
|
|
|
if (isvEnclaveQuoteStatus.length < 1) {
|
|
|
|
|
OWSProdLogAndFail(@"%@ could not parse signature isvEnclaveQuoteStatus.", self.logTag);
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SignatureBodyEntity *result = [SignatureBodyEntity new];
|
|
|
|
|
result.isvEnclaveQuoteBody = isvEnclaveQuoteBody;
|
|
|
|
|
result.isvEnclaveQuoteStatus = isvEnclaveQuoteStatus;
|
|
|
|
|
result.timestamp = timestamp;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (BOOL)verifyServerQuote:(CDSQuote *)quote keys:(RemoteAttestationKeys *)keys enclaveId:(NSString *)enclaveId
|
|
|
|
|
{
|
|
|
|
|
OWSAssert(quote);
|
|
|
|
|