App Attest Validation Nonce Not Matched

Greetings,

We are struggling to implement device binding according to your documentation. We are generation a nonce value in backend like this:

public static String generateNonce(int byteLength) {
    byte[] randomBytes = new byte[byteLength];
    new SecureRandom().nextBytes(randomBytes);
    return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
}

And our mobile client implement the attestation flow like this:

@implementation AppAttestModule

- (NSData *)sha256FromString:(NSString *)input {
    const char *str = [input UTF8String];
    unsigned char result[CC_SHA256_DIGEST_LENGTH];
    CC_SHA256(str, (CC_LONG)strlen(str), result);
    return [NSData dataWithBytes:result length:CC_SHA256_DIGEST_LENGTH];
}

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(generateAttestation:(NSString *)nonce
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  if (@available(iOS 14.0, *)) {
    DCAppAttestService *service = [DCAppAttestService sharedService];

    if (![service isSupported]) {
      reject(@"not_supported", @"App Attest is not supported on this device.", nil);
      return;
    }
    NSData *nonceData = [self sha256FromString:nonce];
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *savedKeyId = [defaults stringForKey:@"AppAttestKeyId"];
    NSString *savedAttestation = [defaults stringForKey:@"AppAttestAttestationData"];

    void (^resolveWithValues)(NSString *keyId, NSData *assertion, NSString *attestationB64) = ^(NSString *keyId, NSData *assertion, NSString *attestationB64) {
      NSString *assertionB64 = [assertion base64EncodedStringWithOptions:0];

      resolve(@{
        @"nonce": nonce,
        @"signature": assertionB64,
        @"deviceType": @"IOS",
        @"attestationData": attestationB64 ?: @"",
        @"keyId": keyId
      });
    };

    void (^handleAssertion)(NSString *keyId, NSString *attestationB64) = ^(NSString *keyId, NSString *attestationB64) {
      [service generateAssertion:keyId clientDataHash:nonceData completionHandler:^(NSData *assertion, NSError *assertError) {
        if (!assertion) {
           reject(@"assertion_error", @"Failed to generate assertion", assertError);
          return;
        }
        resolveWithValues(keyId, assertion, attestationB64);
      }];
    };

    if (savedKeyId && savedAttestation) {
      handleAssertion(savedKeyId, savedAttestation);
    } else {
      
      [service generateKeyWithCompletionHandler:^(NSString *keyId, NSError *keyError) {
        if (!keyId) {
          reject(@"keygen_error", @"Failed to generate key", keyError);
          return;
        }

        [service attestKey:keyId clientDataHash:nonceData completionHandler:^(NSData *attestation, NSError *attestError) {
          if (!attestation) {
            reject(@"attestation_error", @"Failed to generate attestation", attestError);
            return;
          }

          NSString *attestationB64 = [attestation base64EncodedStringWithOptions:0];
          [defaults setObject:keyId forKey:@"AppAttestKeyId"];
          [defaults setObject:attestationB64 forKey:@"AppAttestAttestationData"];
          [defaults synchronize];

          handleAssertion(keyId, attestationB64);
        }];
      }];
    }
  } else {
    reject(@"ios_version", @"App Attest requires iOS 14+", nil);
  }
}

@end

For validation we are extracting the nonce from the certificate like this:

private static byte[] extractNonceFromAttestationCert(X509Certificate certificate) throws IOException {
    byte[] extensionValue = certificate.getExtensionValue("1.2.840.113635.100.8.2");
    if (Objects.isNull(extensionValue)) {
        throw new IllegalArgumentException("Apple App Attest nonce extension not found in certificate.");
    }

    ASN1Primitive extensionPrimitive = ASN1Primitive.fromByteArray(extensionValue);
    ASN1OctetString outerOctet = ASN1OctetString.getInstance(extensionPrimitive);
    ASN1Sequence sequence = (ASN1Sequence) ASN1Primitive.fromByteArray(outerOctet.getOctets());
    ASN1TaggedObject taggedObject = (ASN1TaggedObject) sequence.getObjectAt(0);
    ASN1OctetString nonceOctet = ASN1OctetString.getInstance(taggedObject.getObject());
    return nonceOctet.getOctets();
}

And for the verification we are using this method:

private OptionalMethodResult<Void> verifyNonce(X509Certificate certificate, String expectedNonce, byte[] authData) {
    byte[] expectedNonceHash;
    try {
        byte[] nonceBytes = MessageDigest.getInstance("SHA-256").digest(expectedNonce.getBytes());
        byte[] combined = ByteBuffer.allocate(authData.length + nonceBytes.length).put(authData).put(nonceBytes).array();
        expectedNonceHash = MessageDigest.getInstance("SHA-256").digest(combined);
    } catch (NoSuchAlgorithmException e) {
        log.error("Error while validations iOS attestation: {}", e.getMessage(), e);
        return OptionalMethodResult.ofError(deviceBindError.getChallengeNotMatchedError());
    }

    byte[] actualNonceFromCert;
    try {
        actualNonceFromCert = extractNonceFromAttestationCert(certificate);
    } catch (Exception e) {
        log.error("Error while extracting nonce from certificate: {}", e.getMessage(), e);
        return OptionalMethodResult.ofError(deviceBindError.getChallengeNotMatchedError());
    }

    if (!Arrays.equals(expectedNonceHash, actualNonceFromCert)) {
        return OptionalMethodResult.ofError(deviceBindError.getChallengeNotMatchedError());
    }

    return OptionalMethodResult.empty();
}

But the values did not matched. What are we doing wrong here?

Thanks.

It is not easy to guess what might be wrong from code snippets, and also to determine which step of the generate-extract-validate flow could have a mistake in it across multiple sources.

Can you start by manually comparing the generated nonce and the one from the cert, and make sure what you are putting in matches the generated, and what you extract matches the rest. And if they do, check your validation step. If they don't, then you can debug your code to find out where the nonces are straying away from each other.

App Attest Validation Nonce Not Matched
 
 
Q