From f7379deb6923fead220c97e35c28175df069d263 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 1 Oct 2018 11:38:40 -0400 Subject: [PATCH] Add setup method to UD manager. Try to verify server certificate expiration. --- Signal/src/AppDelegate.m | 3 + .../Messages/UD/OWSCertificateExpiration.h | 13 +++ .../Messages/UD/OWSCertificateExpiration.m | 109 ++++++++++++++++++ .../src/Messages/{ => UD}/OWSUDManager.swift | 55 ++++++++- SignalServiceKit/src/TSConstants.h | 28 ++--- .../src/Tests/OWSFakeUDManager.swift | 20 +++- 6 files changed, 209 insertions(+), 19 deletions(-) create mode 100644 SignalServiceKit/src/Messages/UD/OWSCertificateExpiration.h create mode 100644 SignalServiceKit/src/Messages/UD/OWSCertificateExpiration.m rename SignalServiceKit/src/Messages/{ => UD}/OWSUDManager.swift (71%) diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 5d9437fa1..b6e3cec20 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -44,6 +44,7 @@ #import #import #import +#import #import #import #import @@ -1087,6 +1088,8 @@ static NSTimeInterval launchStartedAt; // Resume lazy restore. [OWSBackupLazyRestoreJob runAsync]; #endif + + [SSKEnvironment.shared.udManager setup]; } - (void)registrationStateDidChange diff --git a/SignalServiceKit/src/Messages/UD/OWSCertificateExpiration.h b/SignalServiceKit/src/Messages/UD/OWSCertificateExpiration.h new file mode 100644 index 000000000..50f6aca63 --- /dev/null +++ b/SignalServiceKit/src/Messages/UD/OWSCertificateExpiration.h @@ -0,0 +1,13 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSCertificateExpiration : NSObject + ++ (nullable NSDate *)expirationDataForCertificate:(NSData *)certificateData; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/UD/OWSCertificateExpiration.m b/SignalServiceKit/src/Messages/UD/OWSCertificateExpiration.m new file mode 100644 index 000000000..b4fd41759 --- /dev/null +++ b/SignalServiceKit/src/Messages/UD/OWSCertificateExpiration.m @@ -0,0 +1,109 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSCertificateExpiration.h" +#import "OWSFileSystem.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSCertificateExpiration + +// PEM is just a series of blocks of base-64 encoded DER data. +// +// https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail ++ (nullable NSArray *)convertPemToDer:(NSString *)pemString +{ + NSMutableArray *certificateDatas = [NSMutableArray new]; + + NSError *error; + // We use ? for non-greedy matching. + NSRegularExpression *_Nullable regex = [NSRegularExpression + regularExpressionWithPattern:@"-----BEGIN.*?-----(.+?)-----END.*?-----" + options:NSRegularExpressionCaseInsensitive | NSRegularExpressionDotMatchesLineSeparators + error:&error]; + if (!regex || error) { + OWSFailDebug(@"could parse regex: %@.", error); + return nil; + } + + [regex enumerateMatchesInString:pemString + options:0 + range:NSMakeRange(0, pemString.length) + usingBlock:^(NSTextCheckingResult *_Nullable result, NSMatchingFlags flags, BOOL *stop) { + if (result.numberOfRanges != 2) { + OWSFailDebug(@"invalid PEM regex match."); + return; + } + NSString *_Nullable derString = [pemString substringWithRange:[result rangeAtIndex:1]]; + if (derString.length < 1) { + OWSFailDebug(@"empty PEM match."); + return; + } + // dataFromBase64String will ignore whitespace, which is + // necessary. + NSData *_Nullable derData = [NSData dataFromBase64String:derString]; + if (derData.length < 1) { + OWSFailDebug(@"could not parse PEM match."); + return; + } + [certificateDatas addObject:derData]; + }]; + + return certificateDatas; +} + ++ (nullable NSDate *)expirationDataForCertificate:(NSData *)certificateData +{ + OWSAssertDebug(certificateData); + + NSString *temporaryFilePath = [OWSFileSystem temporaryFilePath]; + [certificateData writeToFile:temporaryFilePath atomically:YES]; + OWSLogInfo(@"temporaryFilePath: %@", temporaryFilePath); + + OWSLogInfo(@"certificateData: %@", certificateData.hexadecimalString); + NSString *pemString = [[NSString alloc] initWithData:certificateData encoding:NSUTF8StringEncoding]; + OWSLogInfo(@"pemString: %@", pemString); + [DDLog flushLog]; + + if (certificateData.length >= UINT32_MAX) { + OWSFailDebug(@"certificate data is too long."); + return nil; + } + const unsigned char *certificateDataBytes = (const unsigned char *)[certificateData bytes]; + X509 *_Nullable certificateX509 = d2i_X509(NULL, &certificateDataBytes, [certificateData length]); + if (!certificateX509) { + OWSFailDebug(@"could not parse certificate."); + return nil; + } + + ASN1_TIME *not_after = X509_get_notAfter(certificateX509); + OWSAssert(not_after); + + BIO *b = BIO_new(BIO_s_mem()); + int rc = ASN1_TIME_print(b, not_after); + if (rc <= 0) { + OWSLogError(@"ASN1_TIME_print() failed."); + BIO_free(b); + return nil; + } + + const NSUInteger kASN1TimeBufferLength = 128; + char buffer[kASN1TimeBufferLength]; + rc = BIO_gets(b, buffer, kASN1TimeBufferLength); + if (rc <= 0) { + OWSLogError(@"BIO_gets() failed."); + BIO_free(b); + return nil; + } + BIO_free(b); + + return nil; +} +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/OWSUDManager.swift b/SignalServiceKit/src/Messages/UD/OWSUDManager.swift similarity index 71% rename from SignalServiceKit/src/Messages/OWSUDManager.swift rename to SignalServiceKit/src/Messages/UD/OWSUDManager.swift index 6c25c4771..4e0b7ee13 100644 --- a/SignalServiceKit/src/Messages/OWSUDManager.swift +++ b/SignalServiceKit/src/Messages/UD/OWSUDManager.swift @@ -11,6 +11,10 @@ public enum OWSUDError: Error { @objc public protocol OWSUDManager: class { + @objc func setup() + + // MARK: - Recipient state + @objc func isUDRecipientId(_ recipientId: String) -> Bool // No-op if this recipient id is already marked as a "UD recipient". @@ -18,6 +22,9 @@ public enum OWSUDError: Error { // No-op if this recipient id is already marked as _NOT_ a "UD recipient". @objc func removeUDRecipientId(_ recipientId: String) + + @objc func ensureServerCertificateObjC(success:@escaping (Data) -> Void, + failure:@escaping (Error) -> Void) } // MARK: - @@ -40,6 +47,26 @@ public class OWSUDManagerImpl: NSObject, OWSUDManager { SwiftSingletons.register(self) } + @objc public func setup() { + AppReadiness.runNowOrWhenAppIsReady { + guard TSAccountManager.isRegistered() else { + return + } + self.ensureServerCertificate().retainUntilComplete() + } + NotificationCenter.default.addObserver(self, + selector: #selector(registrationStateDidChange), + name: .RegistrationStateDidChange, + object: nil) + } + + @objc + func registrationStateDidChange() { + AssertIsOnMainThread() + + ensureServerCertificate().retainUntilComplete() + } + // MARK: - Singletons private var networkManager: TSNetworkManager { @@ -73,10 +100,17 @@ public class OWSUDManagerImpl: NSObject, OWSUDManager { #endif private func serverCertificate() -> Data? { + return nil guard let certificateData = dbConnection.object(forKey: kUDCurrentServerCertificateKey, inCollection: kUDCollection) as? Data else { return nil } - // TODO: Parse certificate and ensure that it is still valid. + + // Parse certificate and ensure that it is still valid. + guard !isCertificateExpired(certificateData: certificateData) else { + Logger.warn("Current server certificate has expired.") + return nil + } + return certificateData } @@ -98,7 +132,7 @@ public class OWSUDManagerImpl: NSObject, OWSUDManager { public func ensureServerCertificate() -> Promise { return Promise { fulfill, reject in - // If there is an existing server certificate, use that. + // If there is a valid cached server certificate, use that. if let certificateData = serverCertificate() { fulfill(certificateData) return @@ -123,9 +157,16 @@ public class OWSUDManagerImpl: NSObject, OWSUDManager { do { let certificateData = try self.parseServerCertificateResponse(responseObject: responseObject) + guard !self.isCertificateExpired(certificateData: certificateData) else { + reject (OWSUDError.assertionError(description: "Invalid server certificate returned by server")) + return + } + + // Cache the current server certificate. + self.setServerCertificate(certificateData) + fulfill(certificateData) } catch { - reject(error) } }, @@ -147,4 +188,12 @@ public class OWSUDManagerImpl: NSObject, OWSUDManager { return try parser.requiredBase64EncodedData(key: "certificate") } + + private func isCertificateExpired(certificateData: Data) -> Bool { + guard let expirationData = OWSCertificateExpiration.expirationData(forCertificate: certificateData) else { + return true + } + // TODO: + return false + } } diff --git a/SignalServiceKit/src/TSConstants.h b/SignalServiceKit/src/TSConstants.h index e95403f08..a5899b6fc 100644 --- a/SignalServiceKit/src/TSConstants.h +++ b/SignalServiceKit/src/TSConstants.h @@ -26,24 +26,24 @@ typedef NS_ENUM(NSInteger, TSWhisperMessageType) { //#ifndef DEBUG -// Production -#define textSecureWebSocketAPI @"wss://textsecure-service.whispersystems.org/v1/websocket/" -#define textSecureServerURL @"https://textsecure-service.whispersystems.org/" -#define textSecureCDNServerURL @"https://cdn.signal.org" -// Use same reflector for service and CDN -#define textSecureServiceReflectorHost @"textsecure-service-reflected.whispersystems.org" -#define textSecureCDNReflectorHost @"textsecure-service-reflected.whispersystems.org" -#define contactDiscoveryURL @"https://api.directory.signal.org" +//// Production +//#define textSecureWebSocketAPI @"wss://textsecure-service.whispersystems.org/v1/websocket/" +//#define textSecureServerURL @"https://textsecure-service.whispersystems.org/" +//#define textSecureCDNServerURL @"https://cdn.signal.org" +//// Use same reflector for service and CDN +//#define textSecureServiceReflectorHost @"textsecure-service-reflected.whispersystems.org" +//#define textSecureCDNReflectorHost @"textsecure-service-reflected.whispersystems.org" +//#define contactDiscoveryURL @"https://api.directory.signal.org" //#else // //// Staging -//#define textSecureWebSocketAPI @"wss://textsecure-service-staging.whispersystems.org/v1/websocket/" -//#define textSecureServerURL @"https://textsecure-service-staging.whispersystems.org/" -//#define textSecureCDNServerURL @"https://cdn-staging.signal.org" -//#define textSecureServiceReflectorHost @"meek-signal-service-staging.appspot.com"; -//#define textSecureCDNReflectorHost @"meek-signal-cdn-staging.appspot.com"; -//#define contactDiscoveryURL @"https://api-staging.directory.signal.org" +#define textSecureWebSocketAPI @"wss://textsecure-service-staging.whispersystems.org/v1/websocket/" +#define textSecureServerURL @"https://textsecure-service-staging.whispersystems.org/" +#define textSecureCDNServerURL @"https://cdn-staging.signal.org" +#define textSecureServiceReflectorHost @"meek-signal-service-staging.appspot.com"; +#define textSecureCDNReflectorHost @"meek-signal-cdn-staging.appspot.com"; +#define contactDiscoveryURL @"https://api-staging.directory.signal.org" // //#endif diff --git a/SignalServiceKit/src/Tests/OWSFakeUDManager.swift b/SignalServiceKit/src/Tests/OWSFakeUDManager.swift index 2bea21c0f..f012d6916 100644 --- a/SignalServiceKit/src/Tests/OWSFakeUDManager.swift +++ b/SignalServiceKit/src/Tests/OWSFakeUDManager.swift @@ -9,9 +9,11 @@ import Foundation @objc public class OWSFakeUDManager: NSObject, OWSUDManager { - private var udRecipientSet = Set() + @objc public func setup() {} + + // MARK: - Recipient state - // MARK: - + private var udRecipientSet = Set() @objc public func isUDRecipientId(_ recipientId: String) -> Bool { @@ -27,6 +29,20 @@ public class OWSFakeUDManager: NSObject, OWSUDManager { public func removeUDRecipientId(_ recipientId: String) { udRecipientSet.remove(recipientId) } + + // MARK: - Server Certificate + + // Tests can control the behavior of this mock by setting this property. + @objc public var nextServerCertificate: Data? + + @objc public func ensureServerCertificateObjC(success:@escaping (Data) -> Void, + failure:@escaping (Error) -> Void) { + guard let certificateData = nextServerCertificate else { + failure(OWSUDError.assertionError(description: "No mock server certificate data")) + return + } + success(certificateData) + } } #endif