mirror of https://github.com/oxen-io/session-ios
Merge branch 'charlesmchen/remoteAttestation'
commit
e88e7ef759
@ -1 +1 @@
|
||||
Subproject commit e9badd4baacc97c7159f7f8ffd2a51f4581fb869
|
||||
Subproject commit 0a0ef07e7afa563b97ea03d5978b78b72a50ecf6
|
Binary file not shown.
@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface CDSQuote : NSObject
|
||||
|
||||
@property (nonatomic, readonly) uint16_t version;
|
||||
@property (nonatomic, readonly) uint16_t signType;
|
||||
@property (nonatomic, readonly) BOOL isSigLinkable;
|
||||
@property (nonatomic, readonly) uint32_t gid;
|
||||
@property (nonatomic, readonly) uint16_t qeSvn;
|
||||
@property (nonatomic, readonly) uint16_t pceSvn;
|
||||
@property (nonatomic, readonly) NSData *basename;
|
||||
@property (nonatomic, readonly) NSData *cpuSvn;
|
||||
@property (nonatomic, readonly) uint64_t flags;
|
||||
@property (nonatomic, readonly) uint64_t xfrm;
|
||||
@property (nonatomic, readonly) NSData *mrenclave;
|
||||
@property (nonatomic, readonly) NSData *mrsigner;
|
||||
@property (nonatomic, readonly) uint16_t isvProdId;
|
||||
@property (nonatomic, readonly) uint16_t isvSvn;
|
||||
@property (nonatomic, readonly) NSData *reportData;
|
||||
@property (nonatomic, readonly) NSData *signature;
|
||||
|
||||
+ (nullable CDSQuote *)parseQuoteFromData:(NSData *)quoteData;
|
||||
|
||||
- (BOOL)isDebugQuote;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,330 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "CDSQuote.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ByteParser : NSObject
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface ByteParser ()
|
||||
|
||||
@property (nonatomic, readonly) BOOL littleEndian;
|
||||
@property (nonatomic, readonly) NSData *data;
|
||||
@property (nonatomic) NSUInteger cursor;
|
||||
@property (nonatomic) BOOL hasError;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation ByteParser
|
||||
|
||||
- (instancetype)initWithData:(NSData *)data littleEndian:(BOOL)littleEndian
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_littleEndian = littleEndian;
|
||||
_data = data;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Short
|
||||
|
||||
- (uint16_t)shortAtIndex:(NSUInteger)index
|
||||
{
|
||||
uint16_t value;
|
||||
const size_t valueSize = sizeof(value);
|
||||
OWSAssert(valueSize == 2);
|
||||
if (index + valueSize > self.data.length) {
|
||||
self.hasError = YES;
|
||||
return 0;
|
||||
}
|
||||
[self.data getBytes:&value range:NSMakeRange(index, valueSize)];
|
||||
if (self.littleEndian) {
|
||||
return CFSwapInt16LittleToHost(value);
|
||||
} else {
|
||||
return CFSwapInt16BigToHost(value);
|
||||
}
|
||||
}
|
||||
|
||||
- (uint16_t)nextShort
|
||||
{
|
||||
uint16_t value = [self shortAtIndex:self.cursor];
|
||||
self.cursor += sizeof(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
#pragma mark - Int
|
||||
|
||||
- (uint32_t)intAtIndex:(NSUInteger)index
|
||||
{
|
||||
uint32_t value;
|
||||
const size_t valueSize = sizeof(value);
|
||||
OWSAssert(valueSize == 4);
|
||||
if (index + valueSize > self.data.length) {
|
||||
self.hasError = YES;
|
||||
return 0;
|
||||
}
|
||||
[self.data getBytes:&value range:NSMakeRange(index, valueSize)];
|
||||
if (self.littleEndian) {
|
||||
return CFSwapInt32LittleToHost(value);
|
||||
} else {
|
||||
return CFSwapInt32BigToHost(value);
|
||||
}
|
||||
}
|
||||
|
||||
- (uint32_t)nextInt
|
||||
{
|
||||
uint32_t value = [self intAtIndex:self.cursor];
|
||||
self.cursor += sizeof(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
#pragma mark - Long
|
||||
|
||||
- (uint64_t)longAtIndex:(NSUInteger)index
|
||||
{
|
||||
uint64_t value;
|
||||
const size_t valueSize = sizeof(value);
|
||||
OWSAssert(valueSize == 8);
|
||||
if (index + valueSize > self.data.length) {
|
||||
self.hasError = YES;
|
||||
return 0;
|
||||
}
|
||||
[self.data getBytes:&value range:NSMakeRange(index, valueSize)];
|
||||
if (self.littleEndian) {
|
||||
return CFSwapInt64LittleToHost(value);
|
||||
} else {
|
||||
return CFSwapInt64BigToHost(value);
|
||||
}
|
||||
}
|
||||
|
||||
- (uint64_t)nextLong
|
||||
{
|
||||
uint64_t value = [self longAtIndex:self.cursor];
|
||||
self.cursor += sizeof(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (BOOL)readZero:(NSUInteger)length
|
||||
{
|
||||
NSData *_Nullable subdata = [self readBytes:length];
|
||||
if (!subdata) {
|
||||
return NO;
|
||||
}
|
||||
uint8_t bytes[length];
|
||||
[subdata getBytes:bytes range:NSMakeRange(0, length)];
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (bytes[i] != 0) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (nullable NSData *)readBytes:(NSUInteger)length
|
||||
{
|
||||
NSUInteger index = self.cursor;
|
||||
if (index + length > self.data.length) {
|
||||
self.hasError = YES;
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable subdata = [self.data subdataWithRange:NSMakeRange(index, length)];
|
||||
self.cursor += length;
|
||||
return subdata;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
static const long SGX_FLAGS_INITTED = 0x0000000000000001L;
|
||||
static const long SGX_FLAGS_DEBUG = 0x0000000000000002L;
|
||||
static const long SGX_FLAGS_MODE64BIT = 0x0000000000000004L;
|
||||
static const long SGX_FLAGS_PROVISION_KEY = 0x0000000000000004L;
|
||||
static const long SGX_FLAGS_EINITTOKEN_KEY = 0x0000000000000004L;
|
||||
static const long SGX_FLAGS_RESERVED = 0xFFFFFFFFFFFFFFC8L;
|
||||
static const long SGX_XFRM_LEGACY = 0x0000000000000003L;
|
||||
static const long SGX_XFRM_AVX = 0x0000000000000006L;
|
||||
static const long SGX_XFRM_RESERVED = 0xFFFFFFFFFFFFFFF8L;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface CDSQuote ()
|
||||
|
||||
@property (nonatomic) uint16_t version;
|
||||
@property (nonatomic) uint16_t signType;
|
||||
@property (nonatomic) BOOL isSigLinkable;
|
||||
@property (nonatomic) uint32_t gid;
|
||||
@property (nonatomic) uint16_t qeSvn;
|
||||
@property (nonatomic) uint16_t pceSvn;
|
||||
@property (nonatomic) NSData *basename;
|
||||
@property (nonatomic) NSData *cpuSvn;
|
||||
@property (nonatomic) uint64_t flags;
|
||||
@property (nonatomic) uint64_t xfrm;
|
||||
@property (nonatomic) NSData *mrenclave;
|
||||
@property (nonatomic) NSData *mrsigner;
|
||||
@property (nonatomic) uint16_t isvProdId;
|
||||
@property (nonatomic) uint16_t isvSvn;
|
||||
@property (nonatomic) NSData *reportData;
|
||||
@property (nonatomic) NSData *signature;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation CDSQuote
|
||||
|
||||
+ (nullable CDSQuote *)parseQuoteFromData:(NSData *)quoteData
|
||||
{
|
||||
ByteParser *_Nullable parser = [[ByteParser alloc] initWithData:quoteData littleEndian:YES];
|
||||
|
||||
uint16_t version = parser.nextShort;
|
||||
if (version < 1 || version > 2) {
|
||||
OWSProdLogAndFail(@"%@ unexpected quote version: %d", self.logTag, (int)version);
|
||||
return nil;
|
||||
}
|
||||
|
||||
uint16_t signType = parser.nextShort;
|
||||
if ((signType & ~1) != 0) {
|
||||
OWSProdLogAndFail(@"%@ invalid signType: %d", self.logTag, (int)signType);
|
||||
return nil;
|
||||
}
|
||||
|
||||
BOOL isSigLinkable = signType == 1;
|
||||
uint32_t gid = parser.nextInt;
|
||||
uint16_t qeSvn = parser.nextShort;
|
||||
|
||||
uint16_t pceSvn = 0;
|
||||
if (version > 1) {
|
||||
pceSvn = parser.nextShort;
|
||||
} else {
|
||||
if (![parser readZero:2]) {
|
||||
OWSProdLogAndFail(@"%@ non-zero pceSvn.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
if (![parser readZero:4]) {
|
||||
OWSProdLogAndFail(@"%@ non-zero xeid.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *_Nullable basename = [parser readBytes:32];
|
||||
if (!basename) {
|
||||
OWSProdLogAndFail(@"%@ couldn't read basename.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// report_body
|
||||
|
||||
NSData *_Nullable cpuSvn = [parser readBytes:16];
|
||||
if (!cpuSvn) {
|
||||
OWSProdLogAndFail(@"%@ couldn't read cpuSvn.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
if (![parser readZero:4]) {
|
||||
OWSProdLogAndFail(@"%@ non-zero misc_select.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
if (![parser readZero:28]) {
|
||||
OWSProdLogAndFail(@"%@ non-zero reserved1.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
uint64_t flags = parser.nextLong;
|
||||
if ((flags & SGX_FLAGS_RESERVED) != 0 || (flags & SGX_FLAGS_INITTED) == 0 || (flags & SGX_FLAGS_MODE64BIT) == 0) {
|
||||
OWSProdLogAndFail(@"%@ invalid flags.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
uint64_t xfrm = parser.nextLong;
|
||||
if ((xfrm & SGX_XFRM_RESERVED) != 0) {
|
||||
OWSProdLogAndFail(@"%@ invalid xfrm.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *_Nullable mrenclave = [parser readBytes:32];
|
||||
if (!mrenclave) {
|
||||
OWSProdLogAndFail(@"%@ couldn't read mrenclave.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
if (![parser readZero:32]) {
|
||||
OWSProdLogAndFail(@"%@ non-zero reserved2.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable mrsigner = [parser readBytes:32];
|
||||
if (!mrsigner) {
|
||||
OWSProdLogAndFail(@"%@ couldn't read mrsigner.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
if (![parser readZero:96]) {
|
||||
OWSProdLogAndFail(@"%@ non-zero reserved3.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
uint16_t isvProdId = parser.nextShort;
|
||||
uint16_t isvSvn = parser.nextShort;
|
||||
if (![parser readZero:60]) {
|
||||
OWSProdLogAndFail(@"%@ non-zero reserved4.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable reportData = [parser readBytes:64];
|
||||
if (!reportData) {
|
||||
OWSProdLogAndFail(@"%@ couldn't read reportData.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// quote signature
|
||||
uint32_t signatureLength = parser.nextInt;
|
||||
if (signatureLength != quoteData.length - 436) {
|
||||
OWSProdLogAndFail(@"%@ invalid signatureLength.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable signature = [parser readBytes:signatureLength];
|
||||
if (!signature) {
|
||||
OWSProdLogAndFail(@"%@ couldn't read signature.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (parser.hasError) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
CDSQuote *quote = [CDSQuote new];
|
||||
quote.version = version;
|
||||
quote.signType = signType;
|
||||
quote.isSigLinkable = isSigLinkable;
|
||||
quote.gid = gid;
|
||||
quote.qeSvn = qeSvn;
|
||||
quote.pceSvn = pceSvn;
|
||||
quote.basename = basename;
|
||||
quote.cpuSvn = cpuSvn;
|
||||
quote.flags = flags;
|
||||
quote.xfrm = xfrm;
|
||||
quote.mrenclave = mrenclave;
|
||||
quote.mrsigner = mrsigner;
|
||||
quote.isvProdId = isvProdId;
|
||||
quote.isvSvn = isvSvn;
|
||||
quote.reportData = reportData;
|
||||
quote.signature = signature;
|
||||
|
||||
return quote;
|
||||
}
|
||||
|
||||
- (BOOL)isDebugQuote
|
||||
{
|
||||
return (self.flags & SGX_FLAGS_DEBUG) != 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,15 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface CDSSigningCertificate : NSObject
|
||||
|
||||
+ (nullable CDSSigningCertificate *)parseCertificateFromPem:(NSString *)certificatePem;
|
||||
|
||||
- (BOOL)verifySignatureOfBody:(NSString *)body signature:(NSData *)theirSignature;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,297 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "CDSSigningCertificate.h"
|
||||
#import "NSData+Base64.h"
|
||||
#import "NSData+OWS.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface CDSSigningCertificate ()
|
||||
|
||||
@property (nonatomic) SecPolicyRef policy;
|
||||
@property (nonatomic) SecTrustRef trust;
|
||||
@property (nonatomic) SecKeyRef publicKey;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation CDSSigningCertificate
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_policy = NULL;
|
||||
_trust = NULL;
|
||||
_publicKey = NULL;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (_policy) {
|
||||
CFRelease(_policy);
|
||||
_policy = NULL;
|
||||
}
|
||||
if (_trust) {
|
||||
CFRelease(_trust);
|
||||
_trust = NULL;
|
||||
}
|
||||
if (_publicKey) {
|
||||
CFRelease(_publicKey);
|
||||
_publicKey = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
+ (nullable CDSSigningCertificate *)parseCertificateFromPem:(NSString *)certificatePem
|
||||
{
|
||||
OWSAssert(certificatePem);
|
||||
|
||||
CDSSigningCertificate *signingCertificate = [CDSSigningCertificate new];
|
||||
|
||||
NSArray<NSData *> *_Nullable anchorCertificates = [self anchorCertificates];
|
||||
if (anchorCertificates.count < 1) {
|
||||
OWSProdLogAndFail(@"%@ Could not load anchor certificates.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSArray<NSData *> *_Nullable certificateDerDatas = [self convertPemToDer:certificatePem];
|
||||
|
||||
if (certificateDerDatas.count < 1) {
|
||||
OWSProdLogAndFail(@"%@ Could not parse PEM.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray *certificates = [NSMutableArray new];
|
||||
for (NSData *certificateDerData in certificateDerDatas) {
|
||||
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)(certificateDerData));
|
||||
if (!certificate) {
|
||||
OWSProdLogAndFail(@"%@ Could not load DER.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
[certificates addObject:(__bridge_transfer id)certificate];
|
||||
}
|
||||
|
||||
SecPolicyRef policy = SecPolicyCreateBasicX509();
|
||||
signingCertificate.policy = policy;
|
||||
if (!policy) {
|
||||
DDLogError(@"%@ Could not create policy.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
SecTrustRef trust;
|
||||
OSStatus status = SecTrustCreateWithCertificates((__bridge CFTypeRef)certificates, policy, &trust);
|
||||
signingCertificate.trust = trust;
|
||||
if (status != errSecSuccess) {
|
||||
DDLogError(@"%@ trust could not be created.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
if (!trust) {
|
||||
DDLogError(@"%@ Could not create trust.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// TODO:
|
||||
status = SecTrustSetNetworkFetchAllowed(trust, NO);
|
||||
if (status != errSecSuccess) {
|
||||
DDLogError(@"%@ trust fetch could not be configured.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// TODO:
|
||||
status = SecTrustSetAnchorCertificatesOnly(trust, YES);
|
||||
if (status != errSecSuccess) {
|
||||
DDLogError(@"%@ trust anchor certs could not be configured.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray *pinnedCertificates = [NSMutableArray array];
|
||||
for (NSData *certificateData in anchorCertificates) {
|
||||
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)(certificateData));
|
||||
if (!certificate) {
|
||||
OWSProdLogAndFail(@"%@ Could not load DER.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
if (![self verifyDistinguishedName:certificate]) {
|
||||
OWSProdLogAndFail(@"%@ Certificate has invalid name.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
[pinnedCertificates addObject:(__bridge_transfer id)certificate];
|
||||
}
|
||||
status = SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)pinnedCertificates);
|
||||
if (status != errSecSuccess) {
|
||||
DDLogError(@"%@ The anchor certificates couldn't be set.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
SecTrustResultType result;
|
||||
status = SecTrustEvaluate(trust, &result);
|
||||
if (status != errSecSuccess) {
|
||||
DDLogError(@"%@ Could not evaluate certificates.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// `kSecTrustResultUnspecified` is confusingly named. It indicates success.
|
||||
// See the comments in the header where it is defined.
|
||||
BOOL isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
|
||||
if (!isValid) {
|
||||
DDLogError(@"%@ Certificate evaluation failed.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
SecKeyRef publicKey = SecTrustCopyPublicKey(trust);
|
||||
signingCertificate.publicKey = publicKey;
|
||||
if (!publicKey) {
|
||||
DDLogError(@"%@ Could not extract public key.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
return signingCertificate;
|
||||
}
|
||||
|
||||
// PEM is just a series of blocks of base-64 encoded DER data.
|
||||
//
|
||||
// https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail
|
||||
+ (nullable NSArray<NSData *> *)convertPemToDer:(NSString *)pemString
|
||||
{
|
||||
NSMutableArray<NSData *> *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) {
|
||||
OWSProdLogAndFail(@"%@ could parse regex: %@.", self.logTag, 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) {
|
||||
OWSProdLogAndFail(@"%@ invalid PEM regex match.", self.logTag);
|
||||
return;
|
||||
}
|
||||
NSString *_Nullable derString = [pemString substringWithRange:[result rangeAtIndex:1]];
|
||||
if (derString.length < 1) {
|
||||
OWSProdLogAndFail(@"%@ empty PEM match.", self.logTag);
|
||||
return;
|
||||
}
|
||||
// dataFromBase64String will ignore whitespace, which is
|
||||
// necessary.
|
||||
NSData *_Nullable derData = [NSData dataFromBase64String:derString];
|
||||
if (derData.length < 1) {
|
||||
OWSProdLogAndFail(@"%@ could not parse PEM match.", self.logTag);
|
||||
return;
|
||||
}
|
||||
[certificateDatas addObject:derData];
|
||||
}];
|
||||
|
||||
return certificateDatas;
|
||||
}
|
||||
|
||||
+ (nullable NSArray<NSData *> *)anchorCertificates
|
||||
{
|
||||
static NSArray<NSData *> *anchorCertificates = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
// We need to use an Intel certificate as the anchor for IAS verification.
|
||||
NSData *_Nullable anchorCertificate = [self certificateDataForService:@"ias-root"];
|
||||
if (!anchorCertificate) {
|
||||
OWSProdLogAndFail(@"%@ could not load anchor certificate.", self.logTag);
|
||||
OWSRaiseException(@"OWSSignalService_CouldNotLoadCertificate", @"%s", __PRETTY_FUNCTION__);
|
||||
} else {
|
||||
anchorCertificates = @[ anchorCertificate ];
|
||||
}
|
||||
});
|
||||
return anchorCertificates;
|
||||
}
|
||||
|
||||
+ (nullable NSData *)certificateDataForService:(NSString *)service
|
||||
{
|
||||
NSBundle *bundle = [NSBundle bundleForClass:self.class];
|
||||
NSString *path = [bundle pathForResource:service ofType:@"cer"];
|
||||
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
||||
OWSProdLogAndFail(@"%@ could not locate certificate file.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *_Nullable certificateData = [NSData dataWithContentsOfFile:path];
|
||||
return certificateData;
|
||||
}
|
||||
|
||||
- (BOOL)verifySignatureOfBody:(NSString *)body signature:(NSData *)theirSignature
|
||||
{
|
||||
BOOL result = NO;
|
||||
|
||||
// TODO: Which algorithm should we be using?
|
||||
DDLogVerbose(@"%@ kSecKeyAlgorithmRSASignatureDigestPSSSHA256.", self.logTag);
|
||||
result = result ||
|
||||
[self verifySignatureOfBody:body
|
||||
signature:theirSignature
|
||||
algorithm:kSecKeyAlgorithmRSASignatureDigestPSSSHA256];
|
||||
DDLogVerbose(@"%@ kSecKeyAlgorithmRSASignatureMessagePSSSHA256.", self.logTag);
|
||||
result = result ||
|
||||
[self verifySignatureOfBody:body
|
||||
signature:theirSignature
|
||||
algorithm:kSecKeyAlgorithmRSASignatureMessagePSSSHA256];
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: This method requires iOS 10.
|
||||
- (BOOL)verifySignatureOfBody:(NSString *)body signature:(NSData *)signature algorithm:(SecKeyAlgorithm)algorithm
|
||||
{
|
||||
OWSAssert(body.length > 0);
|
||||
OWSAssert(signature.length > 0);
|
||||
OWSAssert(self.publicKey);
|
||||
|
||||
NSData *bodyData = [body dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
BOOL canSign = SecKeyIsAlgorithmSupported(self.publicKey, kSecKeyOperationTypeVerify, algorithm);
|
||||
if (!canSign) {
|
||||
OWSProdLogAndFail(@"%@ signature algorithm is not supported.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
|
||||
CFErrorRef error = NULL;
|
||||
BOOL isValid = SecKeyVerifySignature(
|
||||
self.publicKey, algorithm, (__bridge CFDataRef)bodyData, (__bridge CFDataRef)signature, &error);
|
||||
if (error) {
|
||||
NSError *nsError = CFBridgingRelease(error);
|
||||
// TODO:
|
||||
DDLogError(@"%@ signature verification failed: %@.", self.logTag, nsError);
|
||||
// OWSProdLogAndFail(@"%@ signature verification failed: %@.", self.logTag, nsError);
|
||||
return NO;
|
||||
}
|
||||
if (!isValid) {
|
||||
OWSProdLogAndFail(@"%@ signatures do not match.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
DDLogVerbose(@"%@ signature verification succeeded.", self.logTag);
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)verifyDistinguishedName:(SecCertificateRef)certificate
|
||||
{
|
||||
OWSAssert(certificate);
|
||||
|
||||
NSString *expectedDistinguishedName
|
||||
= @"CN=Intel SGX Attestation Report Signing,O=Intel Corporation,L=Santa Clara,ST=CA,C=US";
|
||||
|
||||
// The Security framework doesn't offer access to certificate details like the name.
|
||||
// TODO: Use OpenSSL to extract the name.
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,17 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ContactDiscoveryService : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
+ (instancetype)sharedService;
|
||||
|
||||
- (void)testService;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,607 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ContactDiscoveryService.h"
|
||||
#import "CDSQuote.h"
|
||||
#import "CDSSigningCertificate.h"
|
||||
#import "Cryptography.h"
|
||||
#import "NSData+OWS.h"
|
||||
#import "OWSRequestFactory.h"
|
||||
#import "TSNetworkManager.h"
|
||||
#import <Curve25519Kit/Curve25519.h>
|
||||
#import <HKDFKit/HKDFKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RemoteAttestationAuth : NSObject
|
||||
|
||||
@property (nonatomic) NSString *username;
|
||||
@property (nonatomic) NSString *authToken;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation RemoteAttestationAuth
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface RemoteAttestationKeys : NSObject
|
||||
|
||||
@property (nonatomic) ECKeyPair *keyPair;
|
||||
@property (nonatomic) NSData *serverEphemeralPublic;
|
||||
@property (nonatomic) NSData *serverStaticPublic;
|
||||
|
||||
@property (nonatomic) NSData *clientKey;
|
||||
@property (nonatomic) NSData *serverKey;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation RemoteAttestationKeys
|
||||
|
||||
+ (nullable RemoteAttestationKeys *)keysForKeyPair:(ECKeyPair *)keyPair
|
||||
serverEphemeralPublic:(NSData *)serverEphemeralPublic
|
||||
serverStaticPublic:(NSData *)serverStaticPublic
|
||||
{
|
||||
RemoteAttestationKeys *keys = [RemoteAttestationKeys new];
|
||||
keys.keyPair = keyPair;
|
||||
keys.serverEphemeralPublic = serverEphemeralPublic;
|
||||
keys.serverStaticPublic = serverStaticPublic;
|
||||
if (![keys deriveKeys]) {
|
||||
return nil;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// Returns YES on success.
|
||||
- (BOOL)deriveKeys
|
||||
{
|
||||
NSData *ephemeralToEphemeral =
|
||||
[Curve25519 generateSharedSecretFromPublicKey:self.serverEphemeralPublic andKeyPair:self.keyPair];
|
||||
NSData *ephemeralToStatic =
|
||||
[Curve25519 generateSharedSecretFromPublicKey:self.serverStaticPublic andKeyPair:self.keyPair];
|
||||
|
||||
NSData *masterSecret = [ephemeralToEphemeral dataByAppendingData:ephemeralToStatic];
|
||||
NSData *publicKeys = [[self.keyPair.publicKey dataByAppendingData:self.serverEphemeralPublic]
|
||||
dataByAppendingData:self.serverStaticPublic];
|
||||
|
||||
NSData *_Nullable derivedMaterial;
|
||||
@try {
|
||||
derivedMaterial = [HKDFKit deriveKey:masterSecret info:nil salt:publicKeys outputSize:ECCKeyLength * 2];
|
||||
} @catch (NSException *exception) {
|
||||
DDLogError(@"%@ could not derive service key: %@", self.logTag, exception);
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (!derivedMaterial) {
|
||||
OWSProdLogAndFail(@"%@ missing derived service key.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
if (derivedMaterial.length != ECCKeyLength * 2) {
|
||||
OWSProdLogAndFail(@"%@ derived service key has unexpected length.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
NSData *_Nullable clientKey = [derivedMaterial subdataWithRange:NSMakeRange(ECCKeyLength * 0, ECCKeyLength)];
|
||||
NSData *_Nullable serverKey = [derivedMaterial subdataWithRange:NSMakeRange(ECCKeyLength * 1, ECCKeyLength)];
|
||||
if (clientKey.length != ECCKeyLength) {
|
||||
OWSProdLogAndFail(@"%@ clientKey has unexpected length.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
if (serverKey.length != ECCKeyLength) {
|
||||
OWSProdLogAndFail(@"%@ serverKey has unexpected length.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
|
||||
self.clientKey = clientKey;
|
||||
self.serverKey = serverKey;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface RemoteAttestation : NSObject
|
||||
|
||||
@property (nonatomic) RemoteAttestationKeys *keys;
|
||||
// TODO: Do we need to support multiple cookies?
|
||||
@property (nonatomic) NSString *cookie;
|
||||
@property (nonatomic) NSData *requestId;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation RemoteAttestation
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface NSDictionary (CDS)
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation NSDictionary (CDS)
|
||||
|
||||
- (nullable NSData *)base64DataForKey:(NSString *)key
|
||||
{
|
||||
NSString *_Nullable valueString = self[key];
|
||||
if (![valueString isKindOfClass:[NSString class]]) {
|
||||
OWSProdLogAndFail(@"%@ couldn't parse base 64 value for key: %@", self.logTag, key);
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable valueData = [[NSData alloc] initWithBase64EncodedString:valueString options:0];
|
||||
if (!valueData) {
|
||||
OWSProdLogAndFail(@"%@ couldn't decode base 64 value for key: %@", self.logTag, key);
|
||||
return nil;
|
||||
}
|
||||
return valueData;
|
||||
}
|
||||
|
||||
- (nullable NSData *)base64DataForKey:(NSString *)key expectedLength:(NSUInteger)expectedLength
|
||||
{
|
||||
NSData *_Nullable valueData = [self base64DataForKey:key];
|
||||
if (valueData && valueData.length != expectedLength) {
|
||||
OWSProdLogAndFail(@"%@ decoded base 64 value for key: %@, has unexpected length: %zd != %zd",
|
||||
self.logTag,
|
||||
key,
|
||||
valueData.length,
|
||||
expectedLength);
|
||||
return nil;
|
||||
}
|
||||
return valueData;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation ContactDiscoveryService
|
||||
|
||||
+ (instancetype)sharedService {
|
||||
static dispatch_once_t onceToken;
|
||||
static id sharedInstance = nil;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedInstance = [[ContactDiscoveryService alloc] initDefault];
|
||||
});
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
|
||||
- (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 performRemoteAttestation];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)performRemoteAttestation
|
||||
{
|
||||
[self performRemoteAttestationAuth];
|
||||
}
|
||||
|
||||
// TODO: Add success and failure?
|
||||
- (void)performRemoteAttestationAuth
|
||||
{
|
||||
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];
|
||||
if (!auth) {
|
||||
DDLogError(@"%@ remote attestation auth could not be parsed: %@", self.logTag, responseDict);
|
||||
return;
|
||||
}
|
||||
[self performRemoteAttestationWithAuth:auth];
|
||||
});
|
||||
}
|
||||
failure:^(NSURLSessionDataTask *task, NSError *error) {
|
||||
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
|
||||
DDLogVerbose(@"%@ remote attestation auth failure: %zd", self.logTag, response.statusCode);
|
||||
}];
|
||||
}
|
||||
|
||||
- (nullable RemoteAttestationAuth *)parseAuthToken:(id)response
|
||||
{
|
||||
if (![response isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSDictionary *responseDict = response;
|
||||
NSString *_Nullable token = responseDict[@"token"];
|
||||
if (![token isKindOfClass:[NSString class]]) {
|
||||
OWSProdLogAndFail(@"%@ missing or invalid token.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
if (token.length < 1) {
|
||||
OWSProdLogAndFail(@"%@ 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;
|
||||
}
|
||||
if (username.length < 1) {
|
||||
OWSProdLogAndFail(@"%@ empty username.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
RemoteAttestationAuth *result = [RemoteAttestationAuth new];
|
||||
result.username = username;
|
||||
result.authToken = token;
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)performRemoteAttestationWithAuth:(RemoteAttestationAuth *)auth
|
||||
{
|
||||
ECKeyPair *keyPair = [Curve25519 generateKeyPair];
|
||||
|
||||
// TODO:
|
||||
NSString *enclaveId = @"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9";
|
||||
|
||||
TSRequest *request = [OWSRequestFactory remoteAttestationRequest:keyPair
|
||||
enclaveId:enclaveId
|
||||
username:auth.username
|
||||
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
|
||||
response:task.response
|
||||
keyPair:keyPair
|
||||
enclaveId:enclaveId];
|
||||
});
|
||||
}
|
||||
failure:^(NSURLSessionDataTask *task, NSError *error) {
|
||||
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
|
||||
DDLogVerbose(@"%@ remote attestation failure: %zd", self.logTag, response.statusCode);
|
||||
}];
|
||||
}
|
||||
|
||||
- (nullable RemoteAttestation *)parseAttestationResponseJson:(id)responseJson
|
||||
response:(NSURLResponse *)response
|
||||
keyPair:(ECKeyPair *)keyPair
|
||||
enclaveId:(NSString *)enclaveId
|
||||
{
|
||||
OWSAssert(responseJson);
|
||||
OWSAssert(response);
|
||||
OWSAssert(keyPair);
|
||||
OWSAssert(enclaveId.length > 0);
|
||||
|
||||
if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
OWSProdLogAndFail(@"%@ unexpected response type.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSDictionary *responseHeaders = ((NSHTTPURLResponse *)response).allHeaderFields;
|
||||
|
||||
NSString *_Nullable cookie = responseHeaders[@"Set-Cookie"];
|
||||
if (![cookie isKindOfClass:[NSString class]]) {
|
||||
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
|
||||
// We want to strip everything after the semicolon (;).
|
||||
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) {
|
||||
OWSProdLogAndFail(@"%@ couldn't parse serverEphemeralPublic.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable serverStaticPublic = [responseDict base64DataForKey:@"serverStaticPublic" expectedLength:32];
|
||||
if (!serverStaticPublic) {
|
||||
OWSProdLogAndFail(@"%@ couldn't parse serverStaticPublic.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable encryptedRequestId = [responseDict base64DataForKey:@"ciphertext"];
|
||||
if (!encryptedRequestId) {
|
||||
OWSProdLogAndFail(@"%@ couldn't parse encryptedRequestId.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable encryptedRequestIv = [responseDict base64DataForKey:@"iv" expectedLength:12];
|
||||
if (!encryptedRequestIv) {
|
||||
OWSProdLogAndFail(@"%@ couldn't parse encryptedRequestIv.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable encryptedRequestTag = [responseDict base64DataForKey:@"tag" expectedLength:16];
|
||||
if (!encryptedRequestTag) {
|
||||
OWSProdLogAndFail(@"%@ couldn't parse encryptedRequestTag.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable quoteData = [responseDict base64DataForKey:@"quote"];
|
||||
if (!quoteData) {
|
||||
OWSProdLogAndFail(@"%@ couldn't parse quote data.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSString *_Nullable signatureBody = responseDict[@"signatureBody"];
|
||||
if (![signatureBody isKindOfClass:[NSString class]]) {
|
||||
OWSProdLogAndFail(@"%@ couldn't parse signatureBody.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable signature = [responseDict base64DataForKey:@"signature"];
|
||||
if (!signature) {
|
||||
OWSProdLogAndFail(@"%@ couldn't parse signature.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSString *_Nullable encodedCertificates = responseDict[@"certificates"];
|
||||
if (![encodedCertificates isKindOfClass:[NSString class]]) {
|
||||
OWSProdLogAndFail(@"%@ couldn't parse encodedCertificates.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSString *_Nullable certificates = [encodedCertificates stringByRemovingPercentEncoding];
|
||||
if (!certificates) {
|
||||
OWSProdLogAndFail(@"%@ couldn't parse certificates.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
RemoteAttestationKeys *_Nullable keys = [RemoteAttestationKeys keysForKeyPair:keyPair
|
||||
serverEphemeralPublic:serverEphemeralPublic
|
||||
serverStaticPublic:serverStaticPublic];
|
||||
if (!keys) {
|
||||
OWSProdLogAndFail(@"%@ couldn't derive keys.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
CDSQuote *_Nullable quote = [CDSQuote parseQuoteFromData:quoteData];
|
||||
if (!quote) {
|
||||
OWSProdLogAndFail(@"%@ couldn't parse quote.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable requestId = [self decryptRequestId:encryptedRequestId
|
||||
encryptedRequestIv:encryptedRequestIv
|
||||
encryptedRequestTag:encryptedRequestTag
|
||||
keys:keys];
|
||||
if (!requestId) {
|
||||
OWSProdLogAndFail(@"%@ couldn't decrypt request id.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![self verifyServerQuote:quote keys:keys enclaveId:enclaveId]) {
|
||||
OWSProdLogAndFail(@"%@ couldn't verify quote.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![self verifyIasSignatureWithCertificates:certificates
|
||||
signatureBody:signatureBody
|
||||
signature:signature
|
||||
quote:quote]) {
|
||||
OWSProdLogAndFail(@"%@ couldn't verify ias signature.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
//+ RemoteAttestation remoteAttestation = new RemoteAttestation(requestId, keys);
|
||||
//+ List<String> addressBook = new LinkedList<>();
|
||||
//+
|
||||
//+ for (String e164number : e164numbers) {
|
||||
//+ addressBook.add(e164number.substring(1));
|
||||
//+ }
|
||||
//+
|
||||
//+ DiscoveryRequest request = cipher.createDiscoveryRequest(addressBook, remoteAttestation);
|
||||
//+ DiscoveryResponse response = this.pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization,
|
||||
//request, attestationResponse.second(), mrenclave);
|
||||
//+ byte[] data = cipher.getDiscoveryResponseData(response, remoteAttestation);
|
||||
//+
|
||||
//+ Iterator<String> addressBookIterator = addressBook.iterator();
|
||||
//+ List<String> results = new LinkedList<>();
|
||||
//+
|
||||
//+ for (byte aData : data) {
|
||||
//+ String candidate = addressBookIterator.next();
|
||||
//+
|
||||
//+ if (aData != 0) results.add('+' + candidate);
|
||||
//+ }
|
||||
//+
|
||||
//+ 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
|
||||
{
|
||||
OWSAssert(certificates.length > 0);
|
||||
OWSAssert(signatureBody.length > 0);
|
||||
OWSAssert(signature.length > 0);
|
||||
OWSAssert(quote);
|
||||
|
||||
CDSSigningCertificate *_Nullable certificate = [CDSSigningCertificate parseCertificateFromPem:certificates];
|
||||
if (!certificate) {
|
||||
OWSProdLogAndFail(@"%@ could not parse signing certificate.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
if (![certificate verifySignatureOfBody:signatureBody signature:signature]) {
|
||||
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);
|
||||
// }
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)verifyServerQuote:(CDSQuote *)quote keys:(RemoteAttestationKeys *)keys enclaveId:(NSString *)enclaveId
|
||||
{
|
||||
OWSAssert(quote);
|
||||
OWSAssert(keys);
|
||||
OWSAssert(enclaveId.length > 0);
|
||||
|
||||
if (quote.reportData.length < keys.serverStaticPublic.length) {
|
||||
OWSProdLogAndFail(@"%@ reportData has unexpected length: %zd != %zd.",
|
||||
self.logTag,
|
||||
quote.reportData.length,
|
||||
keys.serverStaticPublic.length);
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSData *_Nullable theirServerPublicStatic =
|
||||
[quote.reportData subdataWithRange:NSMakeRange(0, keys.serverStaticPublic.length)];
|
||||
if (theirServerPublicStatic.length != keys.serverStaticPublic.length) {
|
||||
OWSProdLogAndFail(@"%@ could not extract server public static.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
if (![keys.serverStaticPublic ows_constantTimeIsEqualToData:theirServerPublicStatic]) {
|
||||
OWSProdLogAndFail(@"%@ server public statics do not match.", self.logTag);
|
||||
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]) {
|
||||
OWSProdLogAndFail(@"%@ enclave ids do not match.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
// TODO: Reverse this condition in production.
|
||||
if (!quote.isDebugQuote) {
|
||||
OWSProdLogAndFail(@"%@ quote has invalid isDebugQuote value.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (nullable NSData *)decryptRequestId:(NSData *)encryptedRequestId
|
||||
encryptedRequestIv:(NSData *)encryptedRequestIv
|
||||
encryptedRequestTag:(NSData *)encryptedRequestTag
|
||||
keys:(RemoteAttestationKeys *)keys
|
||||
{
|
||||
OWSAssert(encryptedRequestId.length > 0);
|
||||
OWSAssert(encryptedRequestIv.length > 0);
|
||||
OWSAssert(encryptedRequestTag.length > 0);
|
||||
OWSAssert(keys);
|
||||
|
||||
OWSAES256Key *_Nullable key = [OWSAES256Key keyWithData:keys.serverKey];
|
||||
if (!key) {
|
||||
OWSProdLogAndFail(@"%@ invalid server key.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable decryptedData = [Cryptography decryptAESGCMWithInitializationVector:encryptedRequestIv
|
||||
ciphertext:encryptedRequestId
|
||||
authTag:encryptedRequestTag
|
||||
key:key];
|
||||
if (!decryptedData) {
|
||||
OWSProdLogAndFail(@"%@ couldn't decrypt request id.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
return decryptedData;
|
||||
}
|
||||
|
||||
// A successful (HTTP 200) response json object consists of:
|
||||
// serverEphemeralPublic: (32 bytes, base64) an ephemeral curve25519 public key generated by the server
|
||||
// serverStaticPublic: (32 bytes, base64) a static curve25519 public key generated by the server
|
||||
// ciphertext: (variable length, base64) a "request id" to be decrypted by the client (see below for derivation
|
||||
// of server_key) (ciphertext, tag) = AES-256-GCM(key=server_key, plaintext=requestId, AAD=(), iv) iv: (12
|
||||
// bytes, base64) an IV for encrypted ciphertext tag: (16 bytes, base64) a MAC for encrypted ciphertext
|
||||
// quote: (variable length, base64) a binary structure from an Intel CPU containing runtime information
|
||||
// about a running enclave signatureBody: (json object) a response from Intel Attestation Services attesting to
|
||||
// the genuineness of the Quote signature: (base64) a signature over signatureBody, with public key in
|
||||
// corresponding signing certificate certificates: (url-encoded PEM) signing certificate chain The response also
|
||||
// contains HTTP session cookies which must be preserved for exactly one corresponding Contact Discovery request.
|
||||
//
|
||||
// After this PUT response is received, the client must:
|
||||
// parse and verify fields in quote (see sample client code for parsing details):
|
||||
// report_data: (64 bytes) must equal (serverStaticPublic || 0 ...)
|
||||
// mrenclave: (32 bytes) must equal the request's enclaveId
|
||||
// flags: (8 bytes) debug flag must be unset, as well as being validated against expected values during
|
||||
// parsing all other fields must be validated against a range of expected values during parsing (as shown in example
|
||||
// parsing code), but are otherwise ignored See client/src/main/java/org/whispersystems/contactdiscovery/Quote.java parse
|
||||
// and verify fields in signatureBody json object: isvEnclaveQuoteBody: (base64) must equal quote
|
||||
// isvEnclaveQuoteStatus: (ascii) must equal "OK"
|
||||
//"GROUP_OUT_OF_DATE" may be allowed for testing only
|
||||
// timestamp: (ascii) UTC timestamp formatted as "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" which must fall within the
|
||||
// last 24h verify validity of signature over signatureBody using the public key contained in the leaf signing
|
||||
// certificate in certificates verify signing certificates chain, with fixed trust anchors to be hard-coded in clients
|
||||
// client/src/main/java/org/whispersystems/contactdiscovery/SigningCertificate.java contains X.509 PKI certificate chain
|
||||
// validation code to follow
|
||||
//
|
||||
// After Curve25519-DH/HKDF key derivation upon the three public keys (client ephemeral private key, server ephemeral
|
||||
// public key, and server static public key, described below), the client can now decrypt the ciphertext in the Remote
|
||||
// Attestation Response containing a requestId to be sent along with a Contact Discovery request, and encrypt the body of
|
||||
// a Contact Discovery request using client_key, bound for the enclave it performed attestation with.
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,24 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TSRequest.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface CDSAttestationRequest : TSRequest
|
||||
|
||||
@property (nonatomic, readonly) NSString *authToken;
|
||||
@property (nonatomic, readonly) NSString *username;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (TSRequest *)initWithURL:(NSURL *)URL
|
||||
method:(NSString *)method
|
||||
parameters:(nullable NSDictionary<NSString *, id> *)parameters
|
||||
username:(NSString *)username
|
||||
authToken:(NSString *)authToken;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,29 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "CDSAttestationRequest.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation CDSAttestationRequest
|
||||
|
||||
- (TSRequest *)initWithURL:(NSURL *)URL
|
||||
method:(NSString *)method
|
||||
parameters:(nullable NSDictionary<NSString *, id> *)parameters
|
||||
username:(NSString *)username
|
||||
authToken:(NSString *)authToken
|
||||
{
|
||||
OWSAssert(authToken.length > 0);
|
||||
|
||||
if (self = [super initWithURL:URL method:method parameters:parameters]) {
|
||||
_username = username;
|
||||
_authToken = authToken;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,16 +1,20 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSData (OWSConstantTimeCompare)
|
||||
@interface NSData (OWS)
|
||||
|
||||
/**
|
||||
* Compares data in constant time so as to help avoid potential timing attacks.
|
||||
*/
|
||||
- (BOOL)ows_constantTimeIsEqualToData:(NSData *)other;
|
||||
|
||||
- (NSData *)dataByAppendingData:(NSData *)data;
|
||||
|
||||
- (NSString *)hexadecimalString;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,56 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSData+OWS.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation NSData (OWS)
|
||||
|
||||
- (BOOL)ows_constantTimeIsEqualToData:(NSData *)other
|
||||
{
|
||||
volatile UInt8 isEqual = 0;
|
||||
|
||||
if (self.length != other.length) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
UInt8 *leftBytes = (UInt8 *)self.bytes;
|
||||
UInt8 *rightBytes = (UInt8 *)other.bytes;
|
||||
for (int i = 0; i < self.length; i++) {
|
||||
// rather than returning as soon as we find a discrepency, we compare the rest of
|
||||
// the byte stream to maintain a constant time comparison
|
||||
isEqual |= leftBytes[i] ^ rightBytes[i];
|
||||
}
|
||||
|
||||
return isEqual == 0;
|
||||
}
|
||||
|
||||
- (NSData *)dataByAppendingData:(NSData *)data
|
||||
{
|
||||
NSMutableData *result = [self mutableCopy];
|
||||
[result appendData:data];
|
||||
return [result copy];
|
||||
}
|
||||
|
||||
- (NSString *)hexadecimalString
|
||||
{
|
||||
/* Returns hexadecimal string of NSData. Empty string if data is empty. */
|
||||
const unsigned char *dataBuffer = (const unsigned char *)[self bytes];
|
||||
if (!dataBuffer) {
|
||||
return @"";
|
||||
}
|
||||
|
||||
NSUInteger dataLength = [self length];
|
||||
NSMutableString *hexString = [NSMutableString stringWithCapacity:(dataLength * 2)];
|
||||
|
||||
for (NSUInteger i = 0; i < dataLength; ++i) {
|
||||
[hexString appendFormat:@"%02x", dataBuffer[i]];
|
||||
}
|
||||
return [hexString copy];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,32 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSData+OWSConstantTimeCompare.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation NSData (OWSConstantTimeCompare)
|
||||
|
||||
- (BOOL)ows_constantTimeIsEqualToData:(NSData *)other
|
||||
{
|
||||
volatile UInt8 isEqual = 0;
|
||||
|
||||
if (self.length != other.length) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
UInt8 *leftBytes = (UInt8 *)self.bytes;
|
||||
UInt8 *rightBytes = (UInt8 *)other.bytes;
|
||||
for (int i = 0; i < self.length; i++) {
|
||||
// rather than returning as soon as we find a discrepency, we compare the rest of
|
||||
// the byte stream to maintain a constant time comparison
|
||||
isEqual |= leftBytes[i] ^ rightBytes[i];
|
||||
}
|
||||
|
||||
return isEqual == 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,9 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
@interface NSData (hexString)
|
||||
|
||||
- (NSString *)hexadecimalString;
|
||||
|
||||
@end
|
@ -1,25 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSData+hexString.h"
|
||||
|
||||
@implementation NSData (hexString)
|
||||
|
||||
- (NSString *)hexadecimalString {
|
||||
/* Returns hexadecimal string of NSData. Empty string if data is empty. */
|
||||
const unsigned char *dataBuffer = (const unsigned char *)[self bytes];
|
||||
if (!dataBuffer) {
|
||||
return @"";
|
||||
}
|
||||
|
||||
NSUInteger dataLength = [self length];
|
||||
NSMutableString *hexString = [NSMutableString stringWithCapacity:(dataLength * 2)];
|
||||
|
||||
for (NSUInteger i = 0; i < dataLength; ++i) {
|
||||
[hexString appendFormat:@"%02x", dataBuffer[i]];
|
||||
}
|
||||
return [hexString copy];
|
||||
}
|
||||
|
||||
@end
|
Loading…
Reference in New Issue