Merge branch 'mkirk/cds-feedback-specifics' into release/2.36.0

pull/2/head
Michael Kirk 6 years ago
commit cd13be64fe

@ -69,7 +69,7 @@
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<key>api-staging.directory.signal.org </key>
<key>api-staging.directory.signal.org</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>

@ -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;

@ -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<NSHTTPCookie *> *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;
}

@ -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<String>
@ -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
}
}

@ -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<NSHTTPCookie *> *)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

@ -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<NSString *, NSString *> *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

Loading…
Cancel
Save