chromium/ios/chrome/credential_provider_extension/passkey_util_unittest.mm

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/credential_provider_extension/passkey_util.h"

#import <CommonCrypto/CommonCrypto.h>

#import "base/strings/string_number_conversions.h"
#import "ios/chrome/common/credential_provider/archivable_credential.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"

namespace {

constexpr int64_t kJan1st2024 = 1704085200;

NSData* StringToData(std::string str) {
  return [NSData dataWithBytes:str.data() length:str.length()];
}

NSData* Sha256(NSData* data) {
  NSMutableData* mac_out =
      [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
  CC_SHA256(data.bytes, data.length,
            static_cast<unsigned char*>(mac_out.mutableBytes));
  return mac_out;
}

NSData* ClientDataHash() {
  return Sha256(StringToData("ClientDataHash"));
}

NSData* SecurityDomainSecret() {
  std::vector<uint8_t> sds;
  base::HexStringToBytes(
      "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF", &sds);
  return [NSData dataWithBytes:sds.data() length:sds.size()];
}

ArchivableCredential* TestPasskeyCredential() {
  return
      [[ArchivableCredential alloc] initWithFavicon:@"favicon"
                                               gaia:nil
                                   recordIdentifier:@"recordIdentifier"
                                             syncId:StringToData("syncId")
                                           username:@"username"
                                    userDisplayName:@"userDisplayName"
                                             userId:StringToData("userId")
                                       credentialId:StringToData("credentialId")
                                               rpId:@"rpId"
                                         privateKey:StringToData("privateKey")
                                          encrypted:StringToData("encrypted")
                                       creationTime:kJan1st2024
                                       lastUsedTime:kJan1st2024];
}

}  // namespace

namespace credential_provider_extension {

class PasskeyUtilTest : public PlatformTest {
 public:
  void SetUp() override;
  void TearDown() override;
};

void PasskeyUtilTest::SetUp() {
  if (@available(iOS 17.0, *)) {
  } else {
    GTEST_SKIP() << "Does not apply on iOS 16 and below";
  }
}

void PasskeyUtilTest::TearDown() {}

// Tests assertion returns valid authenticator data.
TEST_F(PasskeyUtilTest, AssertionAuthenticatorDataIsValid) {
  if (@available(iOS 17.0, *)) {
    NSData* clientDataHash = ClientDataHash();
    id<Credential> credential = TestPasskeyCredential();

    // An empty allowedCredentials list means all credentials are accepted.
    NSArray<NSData*>* allowedCredentials = [NSArray array];

    // Compute the SHA256 of rpId, which is included in the assertion
    // credential.
    NSRange rpIdRange = NSMakeRange(0, 32);
    NSData* rpIdSha =
        Sha256([credential.rpId dataUsingEncoding:NSUTF8StringEncoding]);

    ASPasskeyAssertionCredential* passkeyAssertionCredential =
        PerformPasskeyAssertion(credential, clientDataHash, allowedCredentials,
                                SecurityDomainSecret());

    ASSERT_NSEQ(clientDataHash, passkeyAssertionCredential.clientDataHash);
    ASSERT_NSEQ(credential.credentialId,
                passkeyAssertionCredential.credentialID);
    ASSERT_NSEQ(credential.rpId, passkeyAssertionCredential.relyingParty);
    ASSERT_NSEQ(credential.userId, passkeyAssertionCredential.userHandle);

    // Verify that the first 32 bytes of the authenticator data are the SHA256
    // of rpId.
    ASSERT_NSEQ([passkeyAssertionCredential.authenticatorData
                    subdataWithRange:rpIdRange],
                rpIdSha);
  }
}

// Tests assertion fails if the credential is not allowed.
TEST_F(PasskeyUtilTest, AssertionFailsOnCredentialId) {
  if (@available(iOS 17.0, *)) {
    NSData* clientDataHash = ClientDataHash();
    id<Credential> credential = TestPasskeyCredential();

    NSArray<NSData*>* allowedCredentials =
        [NSArray arrayWithObject:StringToData("otherCredentialId")];
    ASPasskeyAssertionCredential* passkeyAssertionCredential =
        PerformPasskeyAssertion(credential, clientDataHash, allowedCredentials,
                                SecurityDomainSecret());
    ASSERT_NSEQ(passkeyAssertionCredential, nil);
  }
}

// Tests assertion succeeds if the credential is allowed.
TEST_F(PasskeyUtilTest, AssertionSucceedsOnCredentialId) {
  if (@available(iOS 17.0, *)) {
    NSData* clientDataHash = ClientDataHash();
    id<Credential> credential = TestPasskeyCredential();

    NSArray<NSData*>* allowedCredentials =
        [NSArray arrayWithObject:credential.credentialId];
    ASPasskeyAssertionCredential* passkeyAssertionCredential =
        PerformPasskeyAssertion(credential, clientDataHash, allowedCredentials,
                                SecurityDomainSecret());
    ASSERT_NSNE(passkeyAssertionCredential, nil);
  }
}

// Tests that creating a passkey works properly.
TEST_F(PasskeyUtilTest, CreationSucceeds) {
  if (@available(iOS 17.0, *)) {
    NSData* clientDataHash = ClientDataHash();
    id<Credential> credential = TestPasskeyCredential();

    ASPasskeyRegistrationCredential* passkeyRegistrationCredential =
        PerformPasskeyCreation(clientDataHash, credential.rpId,
                               credential.username, credential.userId,
                               SecurityDomainSecret());

    ASSERT_NSEQ(clientDataHash, passkeyRegistrationCredential.clientDataHash);
    ASSERT_EQ(passkeyRegistrationCredential.credentialID.length, 16u);
    ASSERT_NSEQ(credential.rpId, passkeyRegistrationCredential.relyingParty);
    ASSERT_NSNE(passkeyRegistrationCredential.attestationObject, nil);
  }
}

}  // namespace credential_provider_extension