// // 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 "NSDate+OWS.h" #import "OWSRequestFactory.h" #import "TSNetworkManager.h" #import #import 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 SignatureBodyEntity : NSObject @property (nonatomic) NSData *isvEnclaveQuoteBody; @property (nonatomic) NSString *isvEnclaveQuoteStatus; @property (nonatomic) NSString *timestamp; @end #pragma mark - @implementation SignatureBodyEntity @end #pragma mark - @interface NSDictionary (CDS) @end #pragma mark - @implementation NSDictionary (CDS) - (nullable NSString *)stringForKey:(NSString *)key { NSString *_Nullable valueString = self[key]; if (![valueString isKindOfClass:[NSString class]]) { OWSProdLogAndFail(@"%@ couldn't parse string for key: %@", self.logTag, key); return nil; } return valueString; } - (nullable NSData *)base64DataForKey:(NSString *)key { NSString *_Nullable valueString = self[key]; 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); 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; } // 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]; } if (![responseJson isKindOfClass:[NSDictionary class]]) { return nil; } NSDictionary *responseDict = responseJson; 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 quoteData:quoteData]) { OWSProdLogAndFail(@"%@ couldn't verify ias signature.", self.logTag); return nil; } //+ RemoteAttestation remoteAttestation = new RemoteAttestation(requestId, keys); //+ List 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 addressBookIterator = addressBook.iterator(); //+ List 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 quoteData:(NSData *)quoteData { OWSAssert(certificates.length > 0); OWSAssert(signatureBody.length > 0); OWSAssert(signature.length > 0); OWSAssert(quoteData); 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; } SignatureBodyEntity *_Nullable signatureBodyEntity = [self parseSignatureBodyEntity:signatureBody]; if (!signatureBodyEntity) { OWSProdLogAndFail(@"%@ could not parse signature body.", self.logTag); return NO; } // Compare the first N bytes of the quote data with the signed quote body. const NSUInteger kQuoteBodyComparisonLength = 432; if (signatureBodyEntity.isvEnclaveQuoteBody.length < kQuoteBodyComparisonLength) { OWSProdLogAndFail(@"%@ isvEnclaveQuoteBody has unexpected length.", self.logTag); return NO; } if (quoteData.length < kQuoteBodyComparisonLength) { OWSProdLogAndFail(@"%@ quoteData has unexpected length.", self.logTag); return NO; } NSData *isvEnclaveQuoteBodyForComparison = [signatureBodyEntity.isvEnclaveQuoteBody subdataWithRange:NSMakeRange(0, kQuoteBodyComparisonLength)]; NSData *quoteDataForComparison = [quoteData subdataWithRange:NSMakeRange(0, kQuoteBodyComparisonLength)]; if (![isvEnclaveQuoteBodyForComparison ows_constantTimeIsEqualToData:quoteDataForComparison]) { OWSProdLogAndFail(@"%@ isvEnclaveQuoteBody and quoteData do not match.", self.logTag); return NO; } // TODO: Before going to production, remove GROUP_OUT_OF_DATE. if (![@"OK" isEqualToString:signatureBodyEntity.isvEnclaveQuoteStatus] && ![@"GROUP_OUT_OF_DATE" isEqualToString:signatureBodyEntity.isvEnclaveQuoteStatus]) { OWSProdLogAndFail( @"%@ invalid isvEnclaveQuoteStatus: %@.", self.logTag, signatureBodyEntity.isvEnclaveQuoteStatus); return NO; } NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; [dateFormatter setTimeZone:timeZone]; [dateFormatter setDateFormat:@"yyy-MM-dd'T'HH:mm:ss.SSSSSS"]; NSDate *timestampDate = [dateFormatter dateFromString:signatureBodyEntity.timestamp]; if (!timestampDate) { OWSProdLogAndFail(@"%@ could not parse signature body timestamp.", self.logTag); return NO; } // Only accept signatures from the last 24 hours. NSDateComponents *dayComponent = [[NSDateComponents alloc] init]; dayComponent.day = 1; NSCalendar *calendar = [NSCalendar currentCalendar]; NSDate *timestampDatePlus1Day = [calendar dateByAddingComponents:dayComponent toDate:timestampDate options:0]; NSDate *now = [NSDate new]; BOOL isExpired = [now isAfterDate:timestampDatePlus1Day]; if (isExpired) { OWSProdLogAndFail(@"%@ Signature is expired.", self.logTag); return NO; } return YES; } - (nullable SignatureBodyEntity *)parseSignatureBodyEntity:(NSString *)signatureBody { OWSAssert(signatureBody.length > 0); NSError *error = nil; NSDictionary *_Nullable jsonDict = [NSJSONSerialization JSONObjectWithData:[signatureBody dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&error]; if (error || ![jsonDict isKindOfClass:[NSDictionary class]]) { OWSProdLogAndFail(@"%@ could not parse signature body JSON: %@.", self.logTag, error); return nil; } NSString *_Nullable timestamp = [jsonDict stringForKey:@"timestamp"]; if (timestamp.length < 1) { OWSProdLogAndFail(@"%@ could not parse signature timestamp.", self.logTag); return nil; } NSData *_Nullable isvEnclaveQuoteBody = [jsonDict base64DataForKey:@"isvEnclaveQuoteBody"]; if (isvEnclaveQuoteBody.length < 1) { OWSProdLogAndFail(@"%@ could not parse signature isvEnclaveQuoteBody.", self.logTag); return nil; } NSString *_Nullable isvEnclaveQuoteStatus = [jsonDict stringForKey:@"isvEnclaveQuoteStatus"]; if (isvEnclaveQuoteStatus.length < 1) { OWSProdLogAndFail(@"%@ could not parse signature isvEnclaveQuoteStatus.", self.logTag); return nil; } SignatureBodyEntity *result = [SignatureBodyEntity new]; result.isvEnclaveQuoteBody = isvEnclaveQuoteBody; result.isvEnclaveQuoteStatus = isvEnclaveQuoteStatus; result.timestamp = timestamp; return result; } - (BOOL)verifyServerQuote:(CDSQuote *)quote keys:(RemoteAttestationKeys *)keys enclaveId:(NSString *)enclaveId { OWSAssert(quote); 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