diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index 319894846..4ac614c6b 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -69,7 +69,7 @@ NSThirdPartyExceptionRequiresForwardSecrecy - api-staging.directory.signal.org + api-staging.directory.signal.org NSExceptionAllowsInsecureHTTPLoads diff --git a/SignalServiceKit/src/Contacts/ContactDiscoveryService.h b/SignalServiceKit/src/Contacts/ContactDiscoveryService.h index 60031ea0c..2b19fc8bd 100644 --- a/SignalServiceKit/src/Contacts/ContactDiscoveryService.h +++ b/SignalServiceKit/src/Contacts/ContactDiscoveryService.h @@ -1,12 +1,13 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN +extern NSErrorUserInfoKey const ContactDiscoveryServiceErrorKey_Reason; extern NSErrorDomain const ContactDiscoveryServiceErrorDomain; typedef NS_ERROR_ENUM(ContactDiscoveryServiceErrorDomain, ContactDiscoveryServiceError){ - ContactDiscoveryServiceErrorAttestationFailed = 100, + ContactDiscoveryServiceErrorAttestationFailed = 100, ContactDiscoveryServiceErrorAssertionError = 101 }; @class ECKeyPair; diff --git a/SignalServiceKit/src/Contacts/ContactDiscoveryService.m b/SignalServiceKit/src/Contacts/ContactDiscoveryService.m index 2153936bd..cb373c9ed 100644 --- a/SignalServiceKit/src/Contacts/ContactDiscoveryService.m +++ b/SignalServiceKit/src/Contacts/ContactDiscoveryService.m @@ -18,8 +18,16 @@ NS_ASSUME_NONNULL_BEGIN +NSErrorUserInfoKey const ContactDiscoveryServiceErrorKey_Reason = @"ContactDiscoveryServiceErrorKey_Reason"; NSErrorDomain const ContactDiscoveryServiceErrorDomain = @"SignalServiceKit.ContactDiscoveryService"; +NSError *ContactDiscoveryServiceErrorMakeWithReason(NSInteger code, NSString *reason) +{ + return [NSError errorWithDomain:ContactDiscoveryServiceErrorDomain + code:code + userInfo:@{ ContactDiscoveryServiceErrorKey_Reason : reason }]; +} + @interface RemoteAttestationAuth () @property (nonatomic) NSString *username; @@ -53,17 +61,21 @@ NSErrorDomain const ContactDiscoveryServiceErrorDomain = @"SignalServiceKit.Cont + (nullable RemoteAttestationKeys *)keysForKeyPair:(ECKeyPair *)keyPair serverEphemeralPublic:(NSData *)serverEphemeralPublic serverStaticPublic:(NSData *)serverStaticPublic + error:(NSError **)error { if (!keyPair) { - OWSFailDebug(@"Missing keyPair"); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"Missing keyPair"); return nil; } if (serverEphemeralPublic.length < 1) { - OWSFailDebug(@"Invalid serverEphemeralPublic"); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"Invalid serverEphemeralPublic"); return nil; } if (serverStaticPublic.length < 1) { - OWSFailDebug(@"Invalid serverStaticPublic"); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"Invalid serverStaticPublic"); return nil; } RemoteAttestationKeys *keys = [RemoteAttestationKeys new]; @@ -71,6 +83,8 @@ NSErrorDomain const ContactDiscoveryServiceErrorDomain = @"SignalServiceKit.Cont keys.serverEphemeralPublic = serverEphemeralPublic; keys.serverStaticPublic = serverStaticPublic; if (![keys deriveKeys]) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"failed to derive keys"); return nil; } return keys; @@ -350,16 +364,22 @@ NSErrorDomain const ContactDiscoveryServiceErrorDomain = @"SignalServiceKit.Cont [[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]; + auth:auth + error:&error]; if (!attestation) { - NSError *error = [NSError errorWithDomain:ContactDiscoveryServiceErrorDomain - code:ContactDiscoveryServiceErrorAttestationFailed - userInfo:nil]; + 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; @@ -378,6 +398,7 @@ NSErrorDomain const ContactDiscoveryServiceErrorDomain = @"SignalServiceKit.Cont keyPair:(ECKeyPair *)keyPair enclaveId:(NSString *)enclaveId auth:(RemoteAttestationAuth *)auth + error:(NSError **)error { OWSAssertDebug(responseJson); OWSAssertDebug(response); @@ -385,78 +406,96 @@ NSErrorDomain const ContactDiscoveryServiceErrorDomain = @"SignalServiceKit.Cont OWSAssertDebug(enclaveId.length > 0); if (![response isKindOfClass:[NSHTTPURLResponse class]]) { - OWSFailDebug(@"unexpected response type."); + *error = ContactDiscoveryServiceErrorMakeWithReason(ContactDiscoveryServiceErrorAssertionError, @"unexpected response type."); return nil; } NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:httpResponse.allHeaderFields forURL:httpResponse.URL]; if (cookies.count < 1) { - OWSFailDebug(@"couldn't parse cookie."); + *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) { - OWSFailDebug(@"couldn't parse serverEphemeralPublic."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse serverEphemeralPublic."); return nil; } NSData *_Nullable serverStaticPublic = [responseDict base64DataForKey:@"serverStaticPublic" expectedLength:32]; if (!serverStaticPublic) { - OWSFailDebug(@"couldn't parse serverStaticPublic."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse serverStaticPublic."); return nil; } NSData *_Nullable encryptedRequestId = [responseDict base64DataForKey:@"ciphertext"]; if (!encryptedRequestId) { - OWSFailDebug(@"couldn't parse encryptedRequestId."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse encryptedRequestId."); return nil; } NSData *_Nullable encryptedRequestIv = [responseDict base64DataForKey:@"iv" expectedLength:12]; if (!encryptedRequestIv) { - OWSFailDebug(@"couldn't parse encryptedRequestIv."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse encryptedRequestIv."); return nil; } NSData *_Nullable encryptedRequestTag = [responseDict base64DataForKey:@"tag" expectedLength:16]; if (!encryptedRequestTag) { - OWSFailDebug(@"couldn't parse encryptedRequestTag."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse encryptedRequestTag."); return nil; } NSData *_Nullable quoteData = [responseDict base64DataForKey:@"quote"]; if (!quoteData) { - OWSFailDebug(@"couldn't parse quote data."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse quote data."); return nil; } NSString *_Nullable signatureBody = [responseDict stringForKey:@"signatureBody"]; if (![signatureBody isKindOfClass:[NSString class]]) { - OWSFailDebug(@"couldn't parse signatureBody."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse signatureBody."); return nil; } NSData *_Nullable signature = [responseDict base64DataForKey:@"signature"]; if (!signature) { - OWSFailDebug(@"couldn't parse signature."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse signature."); return nil; } NSString *_Nullable encodedCertificates = [responseDict stringForKey:@"certificates"]; if (![encodedCertificates isKindOfClass:[NSString class]]) { - OWSFailDebug(@"couldn't parse encodedCertificates."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse encodedCertificates."); return nil; } NSString *_Nullable certificates = [encodedCertificates stringByRemovingPercentEncoding]; if (!certificates) { - OWSFailDebug(@"couldn't parse certificates."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse certificates."); return nil; } RemoteAttestationKeys *_Nullable keys = [RemoteAttestationKeys keysForKeyPair:keyPair serverEphemeralPublic:serverEphemeralPublic - serverStaticPublic:serverStaticPublic]; - if (!keys) { - OWSFailDebug(@"couldn't derive keys."); + 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; } @@ -470,20 +509,28 @@ NSErrorDomain const ContactDiscoveryServiceErrorDomain = @"SignalServiceKit.Cont encryptedRequestTag:encryptedRequestTag keys:keys]; if (!requestId) { - OWSFailDebug(@"couldn't decrypt request id."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't decrypt request id."); return nil; } if (![self verifyServerQuote:quote keys:keys enclaveId:enclaveId]) { - OWSFailDebug(@"couldn't verify quote."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAttestationFailed, @"couldn't verify quote."); return nil; } if (![self verifyIasSignatureWithCertificates:certificates signatureBody:signatureBody signature:signature - quoteData:quoteData]) { - OWSFailDebug(@"couldn't verify ias signature."); + quoteData:quoteData + error:error]) { + + if (*error == nil) { + OWSFailDebug(@"missing error specifics"); + *error = ContactDiscoveryServiceErrorMakeWithReason(ContactDiscoveryServiceErrorAssertionError, + @"verifyIasSignatureWithCertificates failed. No reason given"); + } return nil; } @@ -503,61 +550,71 @@ NSErrorDomain const ContactDiscoveryServiceErrorDomain = @"SignalServiceKit.Cont 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 *error; + NSError *signingError; CDSSigningCertificate *_Nullable certificate = - [CDSSigningCertificate parseCertificateFromPem:certificates error:&error]; - if (error) { - OWSFailDebug(@"error when parsing signing certificate. %@", error.localizedDescription); + [CDSSigningCertificate parseCertificateFromPem:certificates error:&signingError]; + if (signingError) { + *error = signingError; return NO; } if (!certificate) { - OWSFailDebug(@"could not parse signing certificate."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"could not parse signing certificate."); return NO; } if (![certificate verifySignatureOfBody:signatureBody signature:signature]) { - OWSFailDebug(@"could not verify signature."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAttestationFailed, @"could not verify signature."); return NO; } SignatureBodyEntity *_Nullable signatureBodyEntity = [self parseSignatureBodyEntity:signatureBody]; if (!signatureBodyEntity) { - OWSFailDebug(@"could not parse signature body."); + *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) { - OWSFailDebug(@"isvEnclaveQuoteBody has unexpected length."); + *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)]) { - OWSFailDebug(@"signatureBodyEntity has unexpected version."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"signatureBodyEntity has unexpected version."); return NO; } if (quoteData.length < kQuoteBodyComparisonLength) { - OWSFailDebug(@"quoteData has unexpected length."); + *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]) { - OWSFailDebug(@"isvEnclaveQuoteBody and quoteData do not match."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAttestationFailed, @"isvEnclaveQuoteBody and quoteData do not match."); return NO; } if (![@"OK" isEqualToString:signatureBodyEntity.isvEnclaveQuoteStatus]) { - OWSFailDebug(@"invalid isvEnclaveQuoteStatus: %@.", signatureBodyEntity.isvEnclaveQuoteStatus); + NSString *reason = + [NSString stringWithFormat:@"invalid isvEnclaveQuoteStatus: %@", signatureBodyEntity.isvEnclaveQuoteStatus]; + *error = ContactDiscoveryServiceErrorMakeWithReason(ContactDiscoveryServiceErrorAttestationFailed, reason); return NO; } @@ -567,7 +624,8 @@ NSErrorDomain const ContactDiscoveryServiceErrorDomain = @"SignalServiceKit.Cont [dateFormatter setDateFormat:@"yyy-MM-dd'T'HH:mm:ss.SSSSSS"]; NSDate *timestampDate = [dateFormatter dateFromString:signatureBodyEntity.timestamp]; if (!timestampDate) { - OWSFailDebug(@"could not parse signature body timestamp."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"could not parse signature body timestamp."); return NO; } @@ -581,7 +639,8 @@ NSErrorDomain const ContactDiscoveryServiceErrorDomain = @"SignalServiceKit.Cont BOOL isExpired = [now isAfterDate:timestampDatePlus1Day]; if (isExpired) { - OWSFailDebug(@"Signature is expired."); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAttestationFailed, @"Signature is expired."); return NO; } diff --git a/SignalServiceKit/src/Contacts/OWSContactDiscoveryOperation.swift b/SignalServiceKit/src/Contacts/OWSContactDiscoveryOperation.swift index b55b7eaab..e3dd4c81e 100644 --- a/SignalServiceKit/src/Contacts/OWSContactDiscoveryOperation.swift +++ b/SignalServiceKit/src/Contacts/OWSContactDiscoveryOperation.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation @@ -400,11 +400,11 @@ class CDSBatchOperation: OWSOperation { class CDSFeedbackOperation: OWSOperation { - enum FeedbackResult: String { + enum FeedbackResult { case ok case mismatch - case attestationError = "attestation-error" - case unexpectedError = "unexpected-error" + case attestationError(reason: String) + case unexpectedError(reason: String) } private let legacyRegisteredRecipientIds: Set @@ -455,10 +455,22 @@ class CDSFeedbackOperation: OWSOperation { case ContactDiscoveryError.serverError, ContactDiscoveryError.clientError: // Server already has this information, no need submit feedback self.reportSuccess() - case ContactDiscoveryServiceError.attestationFailed: - self.makeRequest(result: .attestationError) + case let cdsError as ContactDiscoveryServiceError: + let reason = cdsError.reason + switch cdsError.code { + case .assertionError: + self.makeRequest(result: .unexpectedError(reason: "CDS assertionError: \(reason ?? "unknown")")) + case .attestationFailed: + self.makeRequest(result: .attestationError(reason: "CDS attestationFailed: \(reason ?? "unknown")")) + } + case ContactDiscoveryError.assertionError(let assertionDescription): + self.makeRequest(result: .unexpectedError(reason: "assertionError: \(assertionDescription)")) + case ContactDiscoveryError.parseError(description: let parseErrorDescription): + self.makeRequest(result: .unexpectedError(reason: "parseError: \(parseErrorDescription)")) default: - self.makeRequest(result: .unexpectedError) + let nsError = error as NSError + let reason = "unexpectedError code:\(nsError.code)" + self.makeRequest(result: .unexpectedError(reason: reason)) } return @@ -474,7 +486,18 @@ class CDSFeedbackOperation: OWSOperation { } func makeRequest(result: FeedbackResult) { - let request = OWSRequestFactory.cdsFeedbackRequest(result: result.rawValue) + let reason: String? + switch result { + case .ok: + reason = nil + case .mismatch: + reason = nil + case .attestationError(let attestationErrorReason): + reason = attestationErrorReason + case .unexpectedError(let unexpectedErrorReason): + reason = unexpectedErrorReason + } + let request = OWSRequestFactory.cdsFeedbackRequest(status: result.statusPath, reason: reason) self.networkManager.makeRequest(request, success: { _, _ in self.reportSuccess() }, failure: { _, error in self.reportError(error) }) @@ -488,3 +511,24 @@ extension Array { } } } + +extension CDSFeedbackOperation.FeedbackResult { + var statusPath: String { + switch self { + case .ok: + return "ok" + case .mismatch: + return "mismatch" + case .attestationError: + return "attestation-error" + case .unexpectedError: + return "unexpected-error" + } + } +} + +extension ContactDiscoveryServiceError { + var reason: String? { + return userInfo[ContactDiscoveryServiceErrorKey_Reason] as? String + } +} diff --git a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h index b7ef07a7c..197009257 100644 --- a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h +++ b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @@ -102,7 +102,8 @@ typedef NS_ENUM(NSUInteger, TSVerificationTransport) { TSVerificationTransportVo cookies:(NSArray *)cookies; + (TSRequest *)remoteAttestationAuthRequest; -+ (TSRequest *)cdsFeedbackRequestWithResult:(NSString *)result NS_SWIFT_NAME(cdsFeedbackRequest(result:)); ++ (TSRequest *)cdsFeedbackRequestWithStatus:(NSString *)status + reason:(nullable NSString *)reason NS_SWIFT_NAME(cdsFeedbackRequest(status:reason:)); #pragma mark - UD diff --git a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m index bc2e4b6ae..1b0c08600 100644 --- a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m +++ b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "OWSRequestFactory.h" @@ -490,10 +490,26 @@ NS_ASSUME_NONNULL_BEGIN return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; } -+ (TSRequest *)cdsFeedbackRequestWithResult:(NSString *)result ++ (TSRequest *)cdsFeedbackRequestWithStatus:(NSString *)status + reason:(nullable NSString *)reason { - NSString *path = [NSString stringWithFormat:@"/v1/directory/feedback/%@", result]; - return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"PUT" parameters:@{}]; + + NSDictionary *parameters; + if (reason == nil) { + parameters = @{}; + } else { + const NSUInteger kServerReasonLimit = 1000; + NSString *limitedReason; + if (reason.length < kServerReasonLimit) { + limitedReason = reason; + } else { + OWSFailDebug(@"failure: reason should be under 1000"); + limitedReason = [reason substringToIndex:kServerReasonLimit - 1]; + } + parameters = @{ @"reason": limitedReason }; + } + NSString *path = [NSString stringWithFormat:@"/v1/directory/feedback-v2/%@", status]; + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"PUT" parameters:parameters]; } #pragma mark - UD