diff --git a/SignalServiceKit/src/Contacts/CDSSigningCertificate.m b/SignalServiceKit/src/Contacts/CDSSigningCertificate.m
index e25b4ad56..cfe7a2abf 100644
--- a/SignalServiceKit/src/Contacts/CDSSigningCertificate.m
+++ b/SignalServiceKit/src/Contacts/CDSSigningCertificate.m
@@ -3,8 +3,10 @@
 //
 
 #import "CDSSigningCertificate.h"
+#import "Cryptography.h"
 #import "NSData+Base64.h"
 #import "NSData+OWS.h"
+#import <CommonCrypto/CommonCrypto.h>
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -95,14 +97,12 @@ NS_ASSUME_NONNULL_BEGIN
         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);
@@ -229,54 +229,31 @@ NS_ASSUME_NONNULL_BEGIN
     return certificateData;
 }
 
-- (BOOL)verifySignatureOfBody:(NSString *)body signature:(NSData *)theirSignature
+- (BOOL)verifySignatureOfBody:(NSString *)body signature:(NSData *)signature
 {
-    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;
-    }
+    size_t signedHashBytesSize = SecKeyGetBlockSize(self.publicKey);
+    const void *signedHashBytes = [signature bytes];
 
-    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);
+    NSData *_Nullable hashData = [Cryptography computeSHA256Digest:bodyData];
+    if (hashData.length != CC_SHA256_DIGEST_LENGTH) {
+        OWSProdLogAndFail(@"%@ could not SHA256 for signature verification.", self.logTag);
         return NO;
     }
+    size_t hashBytesSize = CC_SHA256_DIGEST_LENGTH;
+    const void *hashBytes = [hashData bytes];
+
+    OSStatus status = SecKeyRawVerify(
+        self.publicKey, kSecPaddingPKCS1SHA256, hashBytes, hashBytesSize, signedHashBytes, signedHashBytesSize);
+
+    BOOL isValid = status == errSecSuccess;
     if (!isValid) {
         OWSProdLogAndFail(@"%@ signatures do not match.", self.logTag);
         return NO;
     }
-    DDLogVerbose(@"%@ signature verification succeeded.", self.logTag);
     return YES;
 }
 
diff --git a/SignalServiceKit/src/Contacts/ContactDiscoveryService.m b/SignalServiceKit/src/Contacts/ContactDiscoveryService.m
index aa738d7b4..2acf7e5d5 100644
--- a/SignalServiceKit/src/Contacts/ContactDiscoveryService.m
+++ b/SignalServiceKit/src/Contacts/ContactDiscoveryService.m
@@ -7,6 +7,7 @@
 #import "CDSSigningCertificate.h"
 #import "Cryptography.h"
 #import "NSData+OWS.h"
+#import "NSDate+OWS.h"
 #import "OWSRequestFactory.h"
 #import "TSNetworkManager.h"
 #import <Curve25519Kit/Curve25519.h>
@@ -124,6 +125,22 @@ NS_ASSUME_NONNULL_BEGIN
 
 #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
@@ -132,6 +149,16 @@ NS_ASSUME_NONNULL_BEGIN
 
 @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];
@@ -207,7 +234,6 @@ NS_ASSUME_NONNULL_BEGIN
     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];
@@ -231,23 +257,15 @@ NS_ASSUME_NONNULL_BEGIN
     }
 
     NSDictionary *responseDict = response;
-    NSString *_Nullable token = responseDict[@"token"];
-    if (![token isKindOfClass:[NSString class]]) {
-        OWSProdLogAndFail(@"%@ missing or invalid token.", self.logTag);
-        return nil;
-    }
+    NSString *_Nullable token = [responseDict stringForKey:@"token"];
     if (token.length < 1) {
-        OWSProdLogAndFail(@"%@ empty token.", self.logTag);
+        OWSProdLogAndFail(@"%@ missing or 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;
-    }
+    NSString *_Nullable username = [responseDict stringForKey:@"username"];
     if (username.length < 1) {
-        OWSProdLogAndFail(@"%@ empty username.", self.logTag);
+        OWSProdLogAndFail(@"%@ missing or empty username.", self.logTag);
         return nil;
     }
 
@@ -270,8 +288,6 @@ NS_ASSUME_NONNULL_BEGIN
                                                            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
@@ -302,12 +318,11 @@ NS_ASSUME_NONNULL_BEGIN
     }
     NSDictionary *responseHeaders = ((NSHTTPURLResponse *)response).allHeaderFields;
 
-    NSString *_Nullable cookie = responseHeaders[@"Set-Cookie"];
-    if (![cookie isKindOfClass:[NSString class]]) {
+    NSString *_Nullable cookie = [responseHeaders stringForKey:@"Set-Cookie"];
+    if (cookie.length < 1) {
         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
@@ -315,18 +330,12 @@ NS_ASSUME_NONNULL_BEGIN
     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) {
@@ -358,7 +367,7 @@ NS_ASSUME_NONNULL_BEGIN
         OWSProdLogAndFail(@"%@ couldn't parse quote data.", self.logTag);
         return nil;
     }
-    NSString *_Nullable signatureBody = responseDict[@"signatureBody"];
+    NSString *_Nullable signatureBody = [responseDict stringForKey:@"signatureBody"];
     if (![signatureBody isKindOfClass:[NSString class]]) {
         OWSProdLogAndFail(@"%@ couldn't parse signatureBody.", self.logTag);
         return nil;
@@ -368,7 +377,7 @@ NS_ASSUME_NONNULL_BEGIN
         OWSProdLogAndFail(@"%@ couldn't parse signature.", self.logTag);
         return nil;
     }
-    NSString *_Nullable encodedCertificates = responseDict[@"certificates"];
+    NSString *_Nullable encodedCertificates = [responseDict stringForKey:@"certificates"];
     if (![encodedCertificates isKindOfClass:[NSString class]]) {
         OWSProdLogAndFail(@"%@ couldn't parse encodedCertificates.", self.logTag);
         return nil;
@@ -409,11 +418,18 @@ NS_ASSUME_NONNULL_BEGIN
     if (![self verifyIasSignatureWithCertificates:certificates
                                     signatureBody:signatureBody
                                         signature:signature
-                                            quote:quote]) {
+                                        quoteData:quoteData]) {
         OWSProdLogAndFail(@"%@ couldn't verify ias signature.", self.logTag);
         return nil;
     }
 
+    RemoteAttestation *result = [RemoteAttestation new];
+    result.cookie = cookie;
+    result.keys = keys;
+    result.requestId = requestId;
+
+    DDLogVerbose(@"%@ remote attestation complete.", self.logTag);
+
     //+      RemoteAttestation remoteAttestation = new RemoteAttestation(requestId, keys);
     //+      List<String>      addressBook       = new LinkedList<>();
     //+
@@ -437,23 +453,18 @@ NS_ASSUME_NONNULL_BEGIN
     //+
     //+      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(quote);
+    OWSAssert(quoteData);
 
     CDSSigningCertificate *_Nullable certificate = [CDSSigningCertificate parseCertificateFromPem:certificates];
     if (!certificate) {
@@ -464,44 +475,102 @@ NS_ASSUME_NONNULL_BEGIN
         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);
-    //    }
+
+    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);