[WebAuthn] Support CTAP Client Pin
authorjiewen_tan@apple.com <jiewen_tan@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 13 Jan 2020 18:53:59 +0000 (18:53 +0000)
committerjiewen_tan@apple.com <jiewen_tan@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 13 Jan 2020 18:53:59 +0000 (18:53 +0000)
https://bugs.webkit.org/show_bug.cgi?id=191516
<rdar://problem/56558558>

Reviewed by Brent Fulgham.

Source/WebCore:

Covered by API tests.

* Modules/webauthn/fido/DeviceRequestConverter.cpp:
(fido::encodeMakeCredenitalRequestAsCBOR):
(fido::encodeGetAssertionRequestAsCBOR):
* Modules/webauthn/fido/Pin.cpp:
(fido::pin::RetriesResponse::parse):
(fido::pin::TokenResponse::parse):
(fido::pin::TokenRequest::tryCreate):
(fido::pin::encodeAsCBOR):
* Modules/webauthn/fido/Pin.h:
* crypto/algorithms/CryptoAlgorithmAES_CBC.h:
* crypto/gcrypt/CryptoAlgorithmAES_CBCGCrypt.cpp:
(WebCore::CryptoAlgorithmAES_CBC::platformEncrypt):
(WebCore::CryptoAlgorithmAES_CBC::platformDecrypt):
* crypto/mac/CryptoAlgorithmAES_CBCMac.cpp:
(WebCore::transformAES_CBC):
(WebCore::CryptoAlgorithmAES_CBC::platformEncrypt):
(WebCore::CryptoAlgorithmAES_CBC::platformDecrypt):
* testing/MockWebAuthenticationConfiguration.h:
(WebCore::MockWebAuthenticationConfiguration::HidConfiguration::encode const):
(WebCore::MockWebAuthenticationConfiguration::HidConfiguration::decode):
* testing/MockWebAuthenticationConfiguration.idl:

Source/WebKit:

This patch implements authenticatorClientPIN from the spec:
https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorClientPIN
Specifically, it implements section 5.5.1, 5.5.3, 5.5.4, 5.5.7, and 5.5.8.

Here is the flow how makeCredential/getAssertion works with a PIN in our implementation:
1. Determine if the connected authenticator has a PIN;
2. If yes, send the makeCredential/getAssertion request to the authenticator with an empty pinAuth
such that the authenticator will wink for user gestures. This step intends to confirm the authenticator
is the one the user wants to use. Otherwise, we don't know which authenticator to send the PIN
if multiple are connected;
3. Once the user confirms the authetnicator, it will return either CTAP2_ERR_PIN_INVALID or
CTAP2_ERR_PIN_AUTH_INVALID. Some authenticators return CTAP2_ERR_PIN_AUTH_INVALID even though
it is not suggested by the spec;
4. Get retries from the authenticator;
5. Get key agreement from the authenticator;
6. Ask the UI client for the PIN and at the meantime inform it the retries;
7. Get pin token from the authenticator;
8. Resend the makeCredential/getAssertion request with the desired pinAuth.

Besides implementating the above flow, this patch also fixes some bugs within the PIN commands encoder:
1. pinAuth/pinProtocol are wrongly encoded for makeCredential/getAssertion;
2. AES CBC should be called without any padding. Therefore, CryptoAlgorithmAES_CBC adds a no padding mode;
3. The sharedSecret is the SHA256 digest of the ECDH key agreement instead of the raw key agreement.

* UIProcess/API/APIWebAuthenticationPanelClient.h:
(API::WebAuthenticationPanelClient::requestPin const):
* UIProcess/WebAuthentication/Authenticator.h:
* UIProcess/WebAuthentication/AuthenticatorManager.cpp:
(WebKit::AuthenticatorManager::requestPin):
* UIProcess/WebAuthentication/AuthenticatorManager.h:
* UIProcess/WebAuthentication/Cocoa/WebAuthenticationPanelClient.h:
* UIProcess/WebAuthentication/Cocoa/WebAuthenticationPanelClient.mm:
(WebKit::WebAuthenticationPanelClient::WebAuthenticationPanelClient):
(WebKit::WebAuthenticationPanelClient::requestPin const):
* UIProcess/WebAuthentication/Mock/MockHidConnection.cpp:
(WebKit::MockHidConnection::feedReports):
* UIProcess/WebAuthentication/fido/CtapAuthenticator.cpp:
(WebKit::CtapAuthenticator::makeCredential):
(WebKit::CtapAuthenticator::continueMakeCredentialAfterResponseReceived):
(WebKit::CtapAuthenticator::getAssertion):
(WebKit::CtapAuthenticator::continueGetAssertionAfterResponseReceived):
(WebKit::CtapAuthenticator::getRetries):
(WebKit::CtapAuthenticator::continueGetKeyAgreementAfterGetRetries):
(WebKit::CtapAuthenticator::continueRequestPinAfterGetKeyAgreement):
(WebKit::CtapAuthenticator::continueGetPinTokenAfterRequestPin):
(WebKit::CtapAuthenticator::continueRequestAfterGetPinToken):
(WebKit::CtapAuthenticator::continueMakeCredentialAfterResponseReceived const): Deleted.
* UIProcess/WebAuthentication/fido/CtapAuthenticator.h:

Tools:

* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/WebCore/CtapPinTest.cpp:
(TestWebKitAPI::TEST):
* TestWebKitAPI/Tests/WebCore/FidoTestData.h:
* TestWebKitAPI/Tests/WebKitCocoa/_WKWebAuthenticationPanel.mm:
(-[TestWebAuthenticationPanelDelegate panel:requestPINWithRemainingRetries:completionHandler:]):
(TestWebKitAPI::TEST):
* TestWebKitAPI/Tests/WebKitCocoa/web-authentication-get-assertion-hid-pin.html: Added.
* TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-key-agreement-error.html: Added.
* TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-pin-token-error.html: Added.
* TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-retries-error.html: Added.
* TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@254439 268f45cc-cd09-0410-ab3c-d52691b4dbfc

29 files changed:
Source/WebCore/ChangeLog
Source/WebCore/Modules/webauthn/fido/DeviceRequestConverter.cpp
Source/WebCore/Modules/webauthn/fido/Pin.cpp
Source/WebCore/Modules/webauthn/fido/Pin.h
Source/WebCore/crypto/algorithms/CryptoAlgorithmAES_CBC.h
Source/WebCore/crypto/gcrypt/CryptoAlgorithmAES_CBCGCrypt.cpp
Source/WebCore/crypto/mac/CryptoAlgorithmAES_CBCMac.cpp
Source/WebCore/testing/MockWebAuthenticationConfiguration.h
Source/WebCore/testing/MockWebAuthenticationConfiguration.idl
Source/WebKit/ChangeLog
Source/WebKit/UIProcess/API/APIWebAuthenticationPanelClient.h
Source/WebKit/UIProcess/WebAuthentication/Authenticator.h
Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp
Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.h
Source/WebKit/UIProcess/WebAuthentication/Cocoa/WebAuthenticationPanelClient.h
Source/WebKit/UIProcess/WebAuthentication/Cocoa/WebAuthenticationPanelClient.mm
Source/WebKit/UIProcess/WebAuthentication/Mock/MockHidConnection.cpp
Source/WebKit/UIProcess/WebAuthentication/fido/CtapAuthenticator.cpp
Source/WebKit/UIProcess/WebAuthentication/fido/CtapAuthenticator.h
Tools/ChangeLog
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/WebCore/CtapPinTest.cpp
Tools/TestWebKitAPI/Tests/WebCore/FidoTestData.h
Tools/TestWebKitAPI/Tests/WebKitCocoa/_WKWebAuthenticationPanel.mm
Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-get-assertion-hid-pin.html [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-key-agreement-error.html [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-pin-token-error.html [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-retries-error.html [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin.html [new file with mode: 0644]

index 0ddfd6b..7efded1 100644 (file)
@@ -1,3 +1,35 @@
+2020-01-06  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthn] Support CTAP Client Pin
+        https://bugs.webkit.org/show_bug.cgi?id=191516
+        <rdar://problem/56558558>
+
+        Reviewed by Brent Fulgham.
+
+        Covered by API tests.
+
+        * Modules/webauthn/fido/DeviceRequestConverter.cpp:
+        (fido::encodeMakeCredenitalRequestAsCBOR):
+        (fido::encodeGetAssertionRequestAsCBOR):
+        * Modules/webauthn/fido/Pin.cpp:
+        (fido::pin::RetriesResponse::parse):
+        (fido::pin::TokenResponse::parse):
+        (fido::pin::TokenRequest::tryCreate):
+        (fido::pin::encodeAsCBOR):
+        * Modules/webauthn/fido/Pin.h:
+        * crypto/algorithms/CryptoAlgorithmAES_CBC.h:
+        * crypto/gcrypt/CryptoAlgorithmAES_CBCGCrypt.cpp:
+        (WebCore::CryptoAlgorithmAES_CBC::platformEncrypt):
+        (WebCore::CryptoAlgorithmAES_CBC::platformDecrypt):
+        * crypto/mac/CryptoAlgorithmAES_CBCMac.cpp:
+        (WebCore::transformAES_CBC):
+        (WebCore::CryptoAlgorithmAES_CBC::platformEncrypt):
+        (WebCore::CryptoAlgorithmAES_CBC::platformDecrypt):
+        * testing/MockWebAuthenticationConfiguration.h:
+        (WebCore::MockWebAuthenticationConfiguration::HidConfiguration::encode const):
+        (WebCore::MockWebAuthenticationConfiguration::HidConfiguration::decode):
+        * testing/MockWebAuthenticationConfiguration.idl:
+
 2020-01-13  Zalan Bujtas  <zalan@apple.com>
 
         [LFC][Integration] Turn off trailing letter-space trimming for the LineBreaker content
index b96dabb..155e5d1 100644 (file)
@@ -126,8 +126,8 @@ Vector<uint8_t> encodeMakeCredenitalRequestAsCBOR(const Vector<uint8_t>& hash, c
 
     if (pin) {
         ASSERT(pin->protocol >= 0);
-        cborMap[CBORValue(8)] = CBORValue(pin->protocol);
-        cborMap[CBORValue(9)] = CBORValue(WTFMove(pin->auth));
+        cborMap[CBORValue(8)] = CBORValue(WTFMove(pin->auth));
+        cborMap[CBORValue(9)] = CBORValue(pin->protocol);
     }
 
     auto serializedParam = CBORWriter::write(CBORValue(WTFMove(cborMap)));
@@ -172,8 +172,8 @@ Vector<uint8_t> encodeGetAssertionRequestAsCBOR(const Vector<uint8_t>& hash, con
 
     if (pin) {
         ASSERT(pin->protocol >= 0);
-        cborMap[CBORValue(8)] = CBORValue(pin->protocol);
-        cborMap[CBORValue(9)] = CBORValue(WTFMove(pin->auth));
+        cborMap[CBORValue(6)] = CBORValue(WTFMove(pin->auth));
+        cborMap[CBORValue(7)] = CBORValue(pin->protocol);
     }
 
     auto serializedParam = CBORWriter::write(CBORValue(WTFMove(cborMap)));
index 16e68f8..2716efd 100644 (file)
@@ -131,7 +131,7 @@ Optional<RetriesResponse> RetriesResponse::parse(const Vector<uint8_t>& inBuffer
         return WTF::nullopt;
 
     RetriesResponse ret;
-    ret.retries = static_cast<int64_t>(it->second.getUnsigned());
+    ret.retries = static_cast<uint64_t>(it->second.getUnsigned());
     return ret;
 }
 
@@ -232,7 +232,7 @@ Optional<TokenResponse> TokenResponse::parse(const WebCore::CryptoKeyAES& shared
         return WTF::nullopt;
     const auto& encryptedToken = it->second.getByteString();
 
-    auto tokenResult = CryptoAlgorithmAES_CBC::platformDecrypt({ }, sharedKey, encryptedToken);
+    auto tokenResult = CryptoAlgorithmAES_CBC::platformDecrypt({ }, sharedKey, encryptedToken, CryptoAlgorithmAES_CBC::Padding::No);
     if (tokenResult.hasException())
         return WTF::nullopt;
     auto token = tokenResult.releaseReturnValue();
@@ -272,11 +272,16 @@ Optional<TokenRequest> TokenRequest::tryCreate(const CString& pin, const CryptoK
     ASSERT(!keyPairResult.hasException());
     auto keyPair = keyPairResult.releaseReturnValue();
 
-    // 2. Use ECDH to compute the shared AES-CBC key.
+    // 2. Use ECDH and SHA-256 to compute the shared AES-CBC key.
     auto sharedKeyResult = CryptoAlgorithmECDH::platformDeriveBits(downcast<CryptoKeyEC>(*keyPair.privateKey), peerKey);
     if (!sharedKeyResult)
         return WTF::nullopt;
-    auto sharedKey = CryptoKeyAES::importRaw(CryptoAlgorithmIdentifier::AES_CBC, WTFMove(*sharedKeyResult), true, CryptoKeyUsageEncrypt | CryptoKeyUsageDecrypt);
+
+    auto crypto = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256);
+    crypto->addBytes(sharedKeyResult->data(), sharedKeyResult->size());
+    auto sharedKeyHash = crypto->computeHash();
+
+    auto sharedKey = CryptoKeyAES::importRaw(CryptoAlgorithmIdentifier::AES_CBC, WTFMove(sharedKeyHash), true, CryptoKeyUsageEncrypt | CryptoKeyUsageDecrypt);
     ASSERT(sharedKey);
 
     // The following encodes the public key of the above key pair into COSE format.
@@ -285,7 +290,7 @@ Optional<TokenRequest> TokenRequest::tryCreate(const CString& pin, const CryptoK
     auto coseKey = encodeCOSEPublicKey(rawPublicKeyResult.returnValue());
 
     // The following calculates a SHA-256 digest of the PIN, and shrink to the left 16 bytes.
-    auto crypto = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256);
+    crypto = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256);
     crypto->addBytes(pin.data(), pin.length());
     auto pinHash = crypto->computeHash();
     pinHash.shrink(16);
@@ -307,7 +312,7 @@ const CryptoKeyAES& TokenRequest::sharedKey() const
 
 Vector<uint8_t> encodeAsCBOR(const TokenRequest& request)
 {
-    auto result = CryptoAlgorithmAES_CBC::platformEncrypt({ }, request.sharedKey(), request.m_pinHash);
+    auto result = CryptoAlgorithmAES_CBC::platformEncrypt({ }, request.sharedKey(), request.m_pinHash, CryptoAlgorithmAES_CBC::Padding::No);
     ASSERT(!result.hasException());
 
     return encodePinCommand(Subcommand::kGetPinToken, [coseKey = WTFMove(request.m_coseKey), encryptedPin = result.releaseReturnValue()] (CBORValue::MapValue* map) mutable {
index 35743a0..f5b1eca 100644 (file)
@@ -79,7 +79,7 @@ enum class ResponseKey : uint8_t {
 
 // kProtocolVersion is the version of the PIN protocol that this code
 // implements.
-constexpr int kProtocolVersion = 1;
+constexpr int64_t kProtocolVersion = 1;
 
 // encodeCOSEPublicKey takes a raw ECDH256 public key and returns it as a COSE structure.
 WEBCORE_EXPORT cbor::CBORValue::MapValue encodeCOSEPublicKey(const Vector<uint8_t>& key);
@@ -110,7 +110,7 @@ struct RetriesResponse {
 
     // retries is the number of PIN attempts remaining before the authenticator
     // locks.
-    int retries;
+    uint64_t retries;
 
 private:
     RetriesResponse();
@@ -145,7 +145,7 @@ public:
 
     // sharedKey returns the shared ECDH key that was used to encrypt the PIN.
     // This is needed to decrypt the response.
-    const WebCore::CryptoKeyAES& sharedKey() const;
+    WEBCORE_EXPORT const WebCore::CryptoKeyAES& sharedKey() const;
 
     friend Vector<uint8_t> encodeAsCBOR(const TokenRequest&);
 
index fd98912..2732985 100644 (file)
@@ -36,13 +36,18 @@ class CryptoKeyAES;
 
 class CryptoAlgorithmAES_CBC final : public CryptoAlgorithm {
 public:
+    enum class Padding : uint8_t {
+        Yes,
+        No
+    };
+
     static constexpr const char* s_name = "AES-CBC";
     static constexpr CryptoAlgorithmIdentifier s_identifier = CryptoAlgorithmIdentifier::AES_CBC;
     static Ref<CryptoAlgorithm> create();
 
     // Operations can be performed directly.
-    WEBCORE_EXPORT static ExceptionOr<Vector<uint8_t>> platformEncrypt(const CryptoAlgorithmAesCbcCfbParams&, const CryptoKeyAES&, const Vector<uint8_t>&);
-    WEBCORE_EXPORT static ExceptionOr<Vector<uint8_t>> platformDecrypt(const CryptoAlgorithmAesCbcCfbParams&, const CryptoKeyAES&, const Vector<uint8_t>&);
+    WEBCORE_EXPORT static ExceptionOr<Vector<uint8_t>> platformEncrypt(const CryptoAlgorithmAesCbcCfbParams&, const CryptoKeyAES&, const Vector<uint8_t>&, Padding padding = Padding::Yes);
+    WEBCORE_EXPORT static ExceptionOr<Vector<uint8_t>> platformDecrypt(const CryptoAlgorithmAesCbcCfbParams&, const CryptoKeyAES&, const Vector<uint8_t>&, Padding padding = Padding::Yes);
 
 private:
     CryptoAlgorithmAES_CBC() = default;
index 98a6b21..a51b66d 100644 (file)
@@ -166,7 +166,7 @@ static Optional<Vector<uint8_t>> gcryptDecrypt(const Vector<uint8_t>& key, const
     return output;
 }
 
-ExceptionOr<Vector<uint8_t>> CryptoAlgorithmAES_CBC::platformEncrypt(const CryptoAlgorithmAesCbcCfbParams& parameters, const CryptoKeyAES& key, const Vector<uint8_t>& plainText)
+ExceptionOr<Vector<uint8_t>> CryptoAlgorithmAES_CBC::platformEncrypt(const CryptoAlgorithmAesCbcCfbParams& parameters, const CryptoKeyAES& key, const Vector<uint8_t>& plainText, Padding)
 {
     auto output = gcryptEncrypt(key.key(), parameters.ivVector(), Vector<uint8_t>(plainText));
     if (!output)
@@ -174,7 +174,7 @@ ExceptionOr<Vector<uint8_t>> CryptoAlgorithmAES_CBC::platformEncrypt(const Crypt
     return WTFMove(*output);
 }
 
-ExceptionOr<Vector<uint8_t>> CryptoAlgorithmAES_CBC::platformDecrypt(const CryptoAlgorithmAesCbcCfbParams& parameters, const CryptoKeyAES& key, const Vector<uint8_t>& cipherText)
+ExceptionOr<Vector<uint8_t>> CryptoAlgorithmAES_CBC::platformDecrypt(const CryptoAlgorithmAesCbcCfbParams& parameters, const CryptoKeyAES& key, const Vector<uint8_t>& cipherText, Padding)
 {
     auto output = gcryptDecrypt(key.key(), parameters.ivVector(), cipherText);
     if (!output)
index 517cfc8..c57f107 100644 (file)
 
 namespace WebCore {
 
-static ExceptionOr<Vector<uint8_t>> transformAES_CBC(CCOperation operation, const Vector<uint8_t>& iv, const Vector<uint8_t>& key, const Vector<uint8_t>& data)
+static ExceptionOr<Vector<uint8_t>> transformAES_CBC(CCOperation operation, const Vector<uint8_t>& iv, const Vector<uint8_t>& key, const Vector<uint8_t>& data, CryptoAlgorithmAES_CBC::Padding padding)
 {
+    CCOptions options = padding == CryptoAlgorithmAES_CBC::Padding::Yes ? kCCOptionPKCS7Padding : 0;
     CCCryptorRef cryptor;
-    CCCryptorStatus status = CCCryptorCreate(operation, kCCAlgorithmAES, kCCOptionPKCS7Padding, key.data(), key.size(), iv.data(), &cryptor);
+    CCCryptorStatus status = CCCryptorCreate(operation, kCCAlgorithmAES, options, key.data(), key.size(), iv.data(), &cryptor);
     if (status)
         return Exception { OperationError };
 
@@ -49,10 +50,12 @@ static ExceptionOr<Vector<uint8_t>> transformAES_CBC(CCOperation operation, cons
         return Exception { OperationError };
 
     uint8_t* p = result.data() + bytesWritten;
-    status = CCCryptorFinal(cryptor, p, result.end() - p, &bytesWritten);
-    p += bytesWritten;
-    if (status)
-        return Exception { OperationError };
+    if (padding == CryptoAlgorithmAES_CBC::Padding::Yes) {
+        status = CCCryptorFinal(cryptor, p, result.end() - p, &bytesWritten);
+        p += bytesWritten;
+        if (status)
+            return Exception { OperationError };
+    }
 
     ASSERT(p <= result.end());
     result.shrink(p - result.begin());
@@ -62,16 +65,18 @@ static ExceptionOr<Vector<uint8_t>> transformAES_CBC(CCOperation operation, cons
     return WTFMove(result);
 }
 
-ExceptionOr<Vector<uint8_t>> CryptoAlgorithmAES_CBC::platformEncrypt(const CryptoAlgorithmAesCbcCfbParams& parameters, const CryptoKeyAES& key, const Vector<uint8_t>& plainText)
+ExceptionOr<Vector<uint8_t>> CryptoAlgorithmAES_CBC::platformEncrypt(const CryptoAlgorithmAesCbcCfbParams& parameters, const CryptoKeyAES& key, const Vector<uint8_t>& plainText, Padding padding)
 {
     ASSERT(parameters.ivVector().size() == kCCBlockSizeAES128 || parameters.ivVector().isEmpty());
-    return transformAES_CBC(kCCEncrypt, parameters.ivVector(), key.key(), plainText);
+    ASSERT(padding == Padding::Yes || !(plainText.size() % kCCBlockSizeAES128));
+    return transformAES_CBC(kCCEncrypt, parameters.ivVector(), key.key(), plainText, padding);
 }
 
-ExceptionOr<Vector<uint8_t>> CryptoAlgorithmAES_CBC::platformDecrypt(const CryptoAlgorithmAesCbcCfbParams& parameters, const CryptoKeyAES& key, const Vector<uint8_t>& cipherText)
+ExceptionOr<Vector<uint8_t>> CryptoAlgorithmAES_CBC::platformDecrypt(const CryptoAlgorithmAesCbcCfbParams& parameters, const CryptoKeyAES& key, const Vector<uint8_t>& cipherText, Padding padding)
 {
     ASSERT(parameters.ivVector().size() == kCCBlockSizeAES128 || parameters.ivVector().isEmpty());
-    return transformAES_CBC(kCCDecrypt, parameters.ivVector(), key.key(), cipherText);
+    ASSERT(padding == Padding::Yes || !(cipherText.size() % kCCBlockSizeAES128));
+    return transformAES_CBC(kCCDecrypt, parameters.ivVector(), key.key(), cipherText, padding);
 }
 
 } // namespace WebCore
index 328bb9d..629558f 100644 (file)
@@ -84,6 +84,7 @@ struct MockWebAuthenticationConfiguration {
         bool continueAfterErrorData { false };
         bool canDowngrade { false };
         bool expectCancel { false };
+        bool supportClientPin { false };
 
         template<class Encoder> void encode(Encoder&) const;
         template<class Decoder> static Optional<HidConfiguration> decode(Decoder&);
@@ -161,7 +162,7 @@ Optional<MockWebAuthenticationConfiguration::LocalConfiguration> MockWebAuthenti
 template<class Encoder>
 void MockWebAuthenticationConfiguration::HidConfiguration::encode(Encoder& encoder) const
 {
-    encoder << payloadBase64 << stage << subStage << error << isU2f << keepAlive << fastDataArrival << continueAfterErrorData << canDowngrade << expectCancel;
+    encoder << payloadBase64 << stage << subStage << error << isU2f << keepAlive << fastDataArrival << continueAfterErrorData << canDowngrade << expectCancel << supportClientPin;
 }
 
 template<class Decoder>
@@ -188,6 +189,8 @@ Optional<MockWebAuthenticationConfiguration::HidConfiguration> MockWebAuthentica
         return WTF::nullopt;
     if (!decoder.decode(result.expectCancel))
         return WTF::nullopt;
+    if (!decoder.decode(result.supportClientPin))
+        return WTF::nullopt;
     return result;
 }
 
index 7853851..ad13934 100644 (file)
@@ -92,6 +92,7 @@
     boolean continueAfterErrorData = false;
     boolean canDowngrade = false;
     boolean expectCancel = false;
+    boolean supportClientPin = false;
 };
 
 [
index 48b2f56..00d5f90 100644 (file)
@@ -1,3 +1,60 @@
+2020-01-06  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthn] Support CTAP Client Pin
+        https://bugs.webkit.org/show_bug.cgi?id=191516
+        <rdar://problem/56558558>
+
+        Reviewed by Brent Fulgham.
+
+        This patch implements authenticatorClientPIN from the spec:
+        https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorClientPIN
+        Specifically, it implements section 5.5.1, 5.5.3, 5.5.4, 5.5.7, and 5.5.8.
+
+        Here is the flow how makeCredential/getAssertion works with a PIN in our implementation:
+        1. Determine if the connected authenticator has a PIN;
+        2. If yes, send the makeCredential/getAssertion request to the authenticator with an empty pinAuth
+        such that the authenticator will wink for user gestures. This step intends to confirm the authenticator
+        is the one the user wants to use. Otherwise, we don't know which authenticator to send the PIN
+        if multiple are connected;
+        3. Once the user confirms the authetnicator, it will return either CTAP2_ERR_PIN_INVALID or
+        CTAP2_ERR_PIN_AUTH_INVALID. Some authenticators return CTAP2_ERR_PIN_AUTH_INVALID even though
+        it is not suggested by the spec;
+        4. Get retries from the authenticator;
+        5. Get key agreement from the authenticator;
+        6. Ask the UI client for the PIN and at the meantime inform it the retries;
+        7. Get pin token from the authenticator;
+        8. Resend the makeCredential/getAssertion request with the desired pinAuth.
+
+        Besides implementating the above flow, this patch also fixes some bugs within the PIN commands encoder:
+        1. pinAuth/pinProtocol are wrongly encoded for makeCredential/getAssertion;
+        2. AES CBC should be called without any padding. Therefore, CryptoAlgorithmAES_CBC adds a no padding mode;
+        3. The sharedSecret is the SHA256 digest of the ECDH key agreement instead of the raw key agreement.
+
+        * UIProcess/API/APIWebAuthenticationPanelClient.h:
+        (API::WebAuthenticationPanelClient::requestPin const):
+        * UIProcess/WebAuthentication/Authenticator.h:
+        * UIProcess/WebAuthentication/AuthenticatorManager.cpp:
+        (WebKit::AuthenticatorManager::requestPin):
+        * UIProcess/WebAuthentication/AuthenticatorManager.h:
+        * UIProcess/WebAuthentication/Cocoa/WebAuthenticationPanelClient.h:
+        * UIProcess/WebAuthentication/Cocoa/WebAuthenticationPanelClient.mm:
+        (WebKit::WebAuthenticationPanelClient::WebAuthenticationPanelClient):
+        (WebKit::WebAuthenticationPanelClient::requestPin const):
+        * UIProcess/WebAuthentication/Mock/MockHidConnection.cpp:
+        (WebKit::MockHidConnection::feedReports):
+        * UIProcess/WebAuthentication/fido/CtapAuthenticator.cpp:
+        (WebKit::CtapAuthenticator::makeCredential):
+        (WebKit::CtapAuthenticator::continueMakeCredentialAfterResponseReceived):
+        (WebKit::CtapAuthenticator::getAssertion):
+        (WebKit::CtapAuthenticator::continueGetAssertionAfterResponseReceived):
+        (WebKit::CtapAuthenticator::getRetries):
+        (WebKit::CtapAuthenticator::continueGetKeyAgreementAfterGetRetries):
+        (WebKit::CtapAuthenticator::continueRequestPinAfterGetKeyAgreement):
+        (WebKit::CtapAuthenticator::continueGetPinTokenAfterRequestPin):
+        (WebKit::CtapAuthenticator::continueRequestAfterGetPinToken):
+        (WebKit::CtapAuthenticator::continueMakeCredentialAfterResponseReceived const): Deleted.
+        * UIProcess/WebAuthentication/fido/CtapAuthenticator.h:
+
 2020-01-13  Brent Fulgham  <bfulgham@apple.com>
 
         [iOS] Remove 'com.apple.diagnosticd' from WebContent process sandbox
index 226b81e..0062f9a 100644 (file)
@@ -30,6 +30,7 @@
 #include <wtf/CompletionHandler.h>
 #include <wtf/HashSet.h>
 #include <wtf/RefCounted.h>
+#include <wtf/text/WTFString.h>
 
 namespace WebCore {
 class AuthenticatorAssertionResponse;
@@ -50,6 +51,7 @@ public:
     virtual void updatePanel(WebKit::WebAuthenticationStatus) const { }
     virtual void dismissPanel(WebKit::WebAuthenticationResult) const { }
     virtual void selectAssertionResponses(const HashSet<Ref<WebCore::AuthenticatorAssertionResponse>>& responses, CompletionHandler<void(const Ref<WebCore::AuthenticatorAssertionResponse>&)>&& completionHandler) const { completionHandler(*responses.begin()); }
+    virtual void requestPin(uint64_t, CompletionHandler<void(const WTF::String&)>&& completionHandler) const { completionHandler(emptyString()); }
 };
 
 } // namespace API
index afaedd1..32fc7d2 100644 (file)
@@ -52,6 +52,7 @@ public:
         virtual void downgrade(Authenticator* id, Ref<Authenticator>&& downgradedAuthenticator) = 0;
         virtual void authenticatorStatusUpdated(WebAuthenticationStatus) = 0;
         virtual void selectAssertionResponses(const HashSet<Ref<WebCore::AuthenticatorAssertionResponse>>&, CompletionHandler<void(const Ref<WebCore::AuthenticatorAssertionResponse>&)>&&) = 0;
+        virtual void requestPin(uint64_t retries, CompletionHandler<void(const WTF::String&)>&&) = 0;
     };
 
     virtual ~Authenticator() = default;
index eb49db0..9cd4547 100644 (file)
@@ -279,6 +279,12 @@ void AuthenticatorManager::selectAssertionResponses(const HashSet<Ref<WebCore::A
         panel->client().selectAssertionResponses(responses, WTFMove(completionHandler));
 }
 
+void AuthenticatorManager::requestPin(uint64_t retries, CompletionHandler<void(const WTF::String&)>&& completionHandler)
+{
+    if (auto* panel = m_pendingRequestData.panel.get())
+        panel->client().requestPin(retries, WTFMove(completionHandler));
+}
+
 UniqueRef<AuthenticatorTransportService> AuthenticatorManager::createService(AuthenticatorTransport transport, AuthenticatorTransportService::Observer& observer) const
 {
     return AuthenticatorTransportService::create(transport, observer);
index 97ba2c9..9fd523d 100644 (file)
@@ -82,6 +82,7 @@ private:
     void downgrade(Authenticator* id, Ref<Authenticator>&& downgradedAuthenticator) final;
     void authenticatorStatusUpdated(WebAuthenticationStatus) final;
     void selectAssertionResponses(const HashSet<Ref<WebCore::AuthenticatorAssertionResponse>>&, CompletionHandler<void(const Ref<WebCore::AuthenticatorAssertionResponse>&)>&&) final;
+    void requestPin(uint64_t retries, CompletionHandler<void(const WTF::String&)>&&) final;
 
     // Overriden by MockAuthenticatorManager.
     virtual UniqueRef<AuthenticatorTransportService> createService(WebCore::AuthenticatorTransport, AuthenticatorTransportService::Observer&) const;
index 3b0c891..1499466 100644 (file)
@@ -49,6 +49,7 @@ private:
     // API::WebAuthenticationPanelClient
     void updatePanel(WebAuthenticationStatus) const final;
     void dismissPanel(WebAuthenticationResult) const final;
+    void requestPin(uint64_t, CompletionHandler<void(const WTF::String&)>&&) const final;
 
     _WKWebAuthenticationPanel *m_panel;
     WeakObjCPtr<id <_WKWebAuthenticationPanelDelegate> > m_delegate;
@@ -56,6 +57,7 @@ private:
     struct {
         bool panelUpdateWebAuthenticationPanel : 1;
         bool panelDismissWebAuthenticationPanelWithResult : 1;
+        bool panelRequestPinWithRemainingRetriesCompletionHandler : 1;
     } m_delegateMethods;
 };
 
index 35ef65b..7f157bd 100644 (file)
 
 #if ENABLE(WEB_AUTHN)
 
+#import "CompletionHandlerCallChecker.h"
 #import "WebAuthenticationFlags.h"
 #import "_WKWebAuthenticationPanel.h"
+#import <wtf/BlockPtr.h>
 #import <wtf/RunLoop.h>
 
 namespace WebKit {
@@ -40,6 +42,7 @@ WebAuthenticationPanelClient::WebAuthenticationPanelClient(_WKWebAuthenticationP
 {
     m_delegateMethods.panelUpdateWebAuthenticationPanel = [delegate respondsToSelector:@selector(panel:updateWebAuthenticationPanel:)];
     m_delegateMethods.panelDismissWebAuthenticationPanelWithResult = [delegate respondsToSelector:@selector(panel:dismissWebAuthenticationPanelWithResult:)];
+    m_delegateMethods.panelRequestPinWithRemainingRetriesCompletionHandler = [delegate respondsToSelector:@selector(panel:requestPINWithRemainingRetries:completionHandler:)];
 }
 
 RetainPtr<id <_WKWebAuthenticationPanelDelegate> > WebAuthenticationPanelClient::delegate()
@@ -107,6 +110,37 @@ void WebAuthenticationPanelClient::dismissPanel(WebAuthenticationResult result)
     });
 }
 
+void WebAuthenticationPanelClient::requestPin(uint64_t retries, CompletionHandler<void(const WTF::String&)>&& completionHandler) const
+{
+    // Call delegates in the next run loop to prevent clients' reentrance that would potentially modify the state
+    // of the current run loop in unexpected ways.
+    RunLoop::main().dispatch([weakThis = makeWeakPtr(*this), this, retries, completionHandler = WTFMove(completionHandler)] () mutable {
+        if (!weakThis) {
+            completionHandler(emptyString());
+            return;
+        }
+
+        if (!m_delegateMethods.panelRequestPinWithRemainingRetriesCompletionHandler) {
+            completionHandler(emptyString());
+            return;
+        }
+
+        auto delegate = m_delegate.get();
+        if (!delegate) {
+            completionHandler(emptyString());
+            return;
+        }
+
+        auto checker = CompletionHandlerCallChecker::create(delegate.get(), @selector(panel:requestPINWithRemainingRetries:completionHandler:));
+        [delegate panel:m_panel requestPINWithRemainingRetries:retries completionHandler:makeBlockPtr([completionHandler = WTFMove(completionHandler), checker = WTFMove(checker)](NSString *pin) mutable {
+            if (checker->completionHandlerHasBeenCalled())
+                return;
+            checker->didCallCompletionHandler();
+            completionHandler(pin);
+        }).get()];
+    });
+}
+
 } // namespace WebKit
 
 #endif // ENABLE(WEB_AUTHN)
index 90c1bb0..4395767 100644 (file)
@@ -31,6 +31,7 @@
 #include <WebCore/AuthenticatorGetInfoResponse.h>
 #include <WebCore/CBORReader.h>
 #include <WebCore/FidoConstants.h>
+#include <WebCore/Pin.h>
 #include <WebCore/WebAuthenticationConstants.h>
 #include <wtf/BlockPtr.h>
 #include <wtf/CryptographicallyRandomNumber.h>
@@ -223,10 +224,18 @@ void MockHidConnection::feedReports()
 
     Optional<FidoHidMessage> message;
     if (m_stage == Mock::HidStage::Info && m_subStage == Mock::HidSubStage::Msg) {
+        // FIXME(205839):
         Vector<uint8_t> infoData;
         if (m_configuration.hid->canDowngrade)
             infoData = encodeAsCBOR(AuthenticatorGetInfoResponse({ ProtocolVersion::kCtap, ProtocolVersion::kU2f }, Vector<uint8_t>(aaguidLength, 0u)));
-        else
+        else if (m_configuration.hid->supportClientPin) {
+            AuthenticatorGetInfoResponse infoResponse({ ProtocolVersion::kCtap }, Vector<uint8_t>(aaguidLength, 0u));
+            infoResponse.setPinProtocols({ pin::kProtocolVersion });
+            AuthenticatorSupportedOptions options;
+            options.setClientPinAvailability(AuthenticatorSupportedOptions::ClientPinAvailability::kSupportedAndPinSet);
+            infoResponse.setOptions(WTFMove(options));
+            infoData = encodeAsCBOR(infoResponse);
+        } else
             infoData = encodeAsCBOR(AuthenticatorGetInfoResponse({ ProtocolVersion::kCtap }, Vector<uint8_t>(aaguidLength, 0u)));
         infoData.insert(0, static_cast<uint8_t>(CtapDeviceResponseCode::kSuccess)); // Prepend status code.
         if (stagesMatch() && m_configuration.hid->error == Mock::HidError::WrongChannelId)
index e73995e..3c3f962 100644 (file)
 #include "CtapDriver.h"
 #include "CtapHidDriver.h"
 #include "U2fAuthenticator.h"
+#include <WebCore/CryptoKeyAES.h>
+#include <WebCore/CryptoKeyEC.h>
+#include <WebCore/CryptoKeyHMAC.h>
 #include <WebCore/DeviceRequestConverter.h>
 #include <WebCore/DeviceResponseConverter.h>
 #include <WebCore/ExceptionData.h>
+#include <WebCore/Pin.h>
 #include <WebCore/U2fCommandConstructor.h>
 #include <wtf/RunLoop.h>
 #include <wtf/text/StringConcatenateNumbers.h>
@@ -53,7 +57,11 @@ void CtapAuthenticator::makeCredential()
     ASSERT(!m_isDowngraded);
     if (processGoogleLegacyAppIdSupportExtension())
         return;
-    auto cborCmd = encodeMakeCredenitalRequestAsCBOR(requestData().hash, WTF::get<PublicKeyCredentialCreationOptions>(requestData().options), m_info.options().userVerificationAvailability());
+    Vector<uint8_t> cborCmd;
+    if (m_info.options().clientPinAvailability() == AuthenticatorSupportedOptions::ClientPinAvailability::kSupportedAndPinSet)
+        cborCmd = encodeMakeCredenitalRequestAsCBOR(requestData().hash, WTF::get<PublicKeyCredentialCreationOptions>(requestData().options), m_info.options().userVerificationAvailability(), PinParameters { pin::kProtocolVersion, m_pinAuth });
+    else
+        cborCmd = encodeMakeCredenitalRequestAsCBOR(requestData().hash, WTF::get<PublicKeyCredentialCreationOptions>(requestData().options), m_info.options().userVerificationAvailability());
     driver().transact(WTFMove(cborCmd), [weakThis = makeWeakPtr(*this)](Vector<uint8_t>&& data) {
         ASSERT(RunLoop::isMain());
         if (!weakThis)
@@ -62,13 +70,16 @@ void CtapAuthenticator::makeCredential()
     });
 }
 
-void CtapAuthenticator::continueMakeCredentialAfterResponseReceived(Vector<uint8_t>&& data) const
+void CtapAuthenticator::continueMakeCredentialAfterResponseReceived(Vector<uint8_t>&& data)
 {
     auto response = readCTAPMakeCredentialResponse(data, WTF::get<PublicKeyCredentialCreationOptions>(requestData().options).attestation);
     if (!response) {
         auto error = getResponseCode(data);
         if (error == CtapDeviceResponseCode::kCtap2ErrCredentialExcluded)
             receiveRespond(ExceptionData { InvalidStateError, "At least one credential matches an entry of the excludeCredentials list in the authenticator."_s });
+        // FIXME(205837)
+        else if (error == CtapDeviceResponseCode::kCtap2ErrPinInvalid || error == CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid)
+            getRetries();
         else
             receiveRespond(ExceptionData { UnknownError, makeString("Unknown internal error. Error code: ", static_cast<uint8_t>(error)) });
         return;
@@ -79,7 +90,11 @@ void CtapAuthenticator::continueMakeCredentialAfterResponseReceived(Vector<uint8
 void CtapAuthenticator::getAssertion()
 {
     ASSERT(!m_isDowngraded);
-    auto cborCmd = encodeGetAssertionRequestAsCBOR(requestData().hash, WTF::get<PublicKeyCredentialRequestOptions>(requestData().options), m_info.options().userVerificationAvailability());
+    Vector<uint8_t> cborCmd;
+    if (m_info.options().clientPinAvailability() == AuthenticatorSupportedOptions::ClientPinAvailability::kSupportedAndPinSet)
+        cborCmd = encodeGetAssertionRequestAsCBOR(requestData().hash, WTF::get<PublicKeyCredentialRequestOptions>(requestData().options), m_info.options().userVerificationAvailability(), PinParameters { pin::kProtocolVersion, m_pinAuth });
+    else
+        cborCmd = encodeGetAssertionRequestAsCBOR(requestData().hash, WTF::get<PublicKeyCredentialRequestOptions>(requestData().options), m_info.options().userVerificationAvailability());
     driver().transact(WTFMove(cborCmd), [weakThis = makeWeakPtr(*this)](Vector<uint8_t>&& data) {
         ASSERT(RunLoop::isMain());
         if (!weakThis)
@@ -93,6 +108,11 @@ void CtapAuthenticator::continueGetAssertionAfterResponseReceived(Vector<uint8_t
     auto response = readCTAPGetAssertionResponse(data);
     if (!response) {
         auto error = getResponseCode(data);
+        // FIXME(205837)
+        if (error == CtapDeviceResponseCode::kCtap2ErrPinInvalid || error == CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid) {
+            getRetries();
+            return;
+        }
         if (error != CtapDeviceResponseCode::kCtap2ErrInvalidCBOR && tryDowngrade())
             return;
         if (error == CtapDeviceResponseCode::kCtap2ErrNoCredentials && observer())
@@ -152,6 +172,93 @@ void CtapAuthenticator::continueGetNextAssertionAfterResponseReceived(Vector<uin
     });
 }
 
+void CtapAuthenticator::getRetries()
+{
+    auto cborCmd = encodeAsCBOR(pin::RetriesRequest { });
+    driver().transact(WTFMove(cborCmd), [weakThis = makeWeakPtr(*this)](Vector<uint8_t>&& data) {
+        ASSERT(RunLoop::isMain());
+        if (!weakThis)
+            return;
+        weakThis->continueGetKeyAgreementAfterGetRetries(WTFMove(data));
+    });
+}
+
+void CtapAuthenticator::continueGetKeyAgreementAfterGetRetries(Vector<uint8_t>&& data)
+{
+    auto retries = pin::RetriesResponse::parse(data);
+    if (!retries) {
+        auto error = getResponseCode(data);
+        receiveRespond(ExceptionData { UnknownError, makeString("Unknown internal error. Error code: ", static_cast<uint8_t>(error)) });
+        return;
+    }
+
+    auto cborCmd = encodeAsCBOR(pin::KeyAgreementRequest { });
+    driver().transact(WTFMove(cborCmd), [weakThis = makeWeakPtr(*this), retries = retries->retries] (Vector<uint8_t>&& data) {
+        ASSERT(RunLoop::isMain());
+        if (!weakThis)
+            return;
+        weakThis->continueRequestPinAfterGetKeyAgreement(WTFMove(data), retries);
+    });
+}
+
+void CtapAuthenticator::continueRequestPinAfterGetKeyAgreement(Vector<uint8_t>&& data, uint64_t retries)
+{
+    auto keyAgreement = pin::KeyAgreementResponse::parse(data);
+    if (!keyAgreement) {
+        auto error = getResponseCode(data);
+        receiveRespond(ExceptionData { UnknownError, makeString("Unknown internal error. Error code: ", static_cast<uint8_t>(error)) });
+        return;
+    }
+
+    if (auto* observer = this->observer()) {
+        observer->requestPin(retries, [weakThis = makeWeakPtr(*this), keyAgreement = WTFMove(*keyAgreement)] (const String& pin) {
+            ASSERT(RunLoop::isMain());
+            if (!weakThis)
+                return;
+            weakThis->continueGetPinTokenAfterRequestPin(pin, keyAgreement.peerKey);
+        });
+    }
+}
+
+void CtapAuthenticator::continueGetPinTokenAfterRequestPin(const String& pin, const CryptoKeyEC& peerKey)
+{
+    auto pinUtf8 = pin::validateAndConvertToUTF8(pin);
+    if (!pinUtf8) {
+        receiveRespond(ExceptionData { UnknownError, makeString("Pin is not valid: ", pin) });
+        return;
+    }
+    auto tokenRequest = pin::TokenRequest::tryCreate(*pinUtf8, peerKey);
+    if (!tokenRequest) {
+        receiveRespond(ExceptionData { UnknownError, "Cannot create a TokenRequest."_s });
+        return;
+    }
+
+    auto cborCmd = encodeAsCBOR(*tokenRequest);
+    driver().transact(WTFMove(cborCmd), [weakThis = makeWeakPtr(*this), tokenRequest = WTFMove(*tokenRequest)] (Vector<uint8_t>&& data) {
+        ASSERT(RunLoop::isMain());
+        if (!weakThis)
+            return;
+        weakThis->continueRequestAfterGetPinToken(WTFMove(data), tokenRequest);
+    });
+}
+
+void CtapAuthenticator::continueRequestAfterGetPinToken(Vector<uint8_t>&& data, const fido::pin::TokenRequest& tokenRequest)
+{
+    auto token = pin::TokenResponse::parse(tokenRequest.sharedKey(), data);
+    if (!token) {
+        auto error = getResponseCode(data);
+        receiveRespond(ExceptionData { UnknownError, makeString("Unknown internal error. Error code: ", static_cast<uint8_t>(error)) });
+        return;
+    }
+
+    m_pinAuth = token->pinAuth(requestData().hash);
+    WTF::switchOn(requestData().options, [&](const PublicKeyCredentialCreationOptions& options) {
+        makeCredential();
+    }, [&](const PublicKeyCredentialRequestOptions& options) {
+        getAssertion();
+    });
+}
+
 bool CtapAuthenticator::tryDowngrade()
 {
     if (m_info.versions().find(ProtocolVersion::kU2f) == m_info.versions().end())
index 54ff494..25fe903 100644 (file)
 #include "FidoAuthenticator.h"
 #include <WebCore/AuthenticatorGetInfoResponse.h>
 
+namespace fido {
+namespace pin {
+class TokenRequest;
+}
+}
+
+namespace WebCore {
+class CryptoKeyEC;
+}
+
 namespace WebKit {
 
 class CtapDriver;
@@ -45,11 +55,17 @@ private:
     explicit CtapAuthenticator(std::unique_ptr<CtapDriver>&&, fido::AuthenticatorGetInfoResponse&&);
 
     void makeCredential() final;
-    void continueMakeCredentialAfterResponseReceived(Vector<uint8_t>&&) const;
+    void continueMakeCredentialAfterResponseReceived(Vector<uint8_t>&&);
     void getAssertion() final;
     void continueGetAssertionAfterResponseReceived(Vector<uint8_t>&&);
     void continueGetNextAssertionAfterResponseReceived(Vector<uint8_t>&&);
 
+    void getRetries();
+    void continueGetKeyAgreementAfterGetRetries(Vector<uint8_t>&&);
+    void continueRequestPinAfterGetKeyAgreement(Vector<uint8_t>&&, uint64_t retries);
+    void continueGetPinTokenAfterRequestPin(const String& pin, const CryptoKeyEC&);
+    void continueRequestAfterGetPinToken(Vector<uint8_t>&&, const fido::pin::TokenRequest&);
+
     bool tryDowngrade();
     bool processGoogleLegacyAppIdSupportExtension();
 
@@ -57,6 +73,7 @@ private:
     bool m_isDowngraded { false };
     size_t m_remainingAssertionResponses { 0 };
     HashSet<Ref<WebCore::AuthenticatorAssertionResponse>> m_assertionResponses;
+    Vector<uint8_t> m_pinAuth;
 };
 
 } // namespace WebKit
index 163fb0f..da96b20 100644 (file)
@@ -1,3 +1,24 @@
+2020-01-06  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthn] Support CTAP Client Pin
+        https://bugs.webkit.org/show_bug.cgi?id=191516
+        <rdar://problem/56558558>
+
+        Reviewed by Brent Fulgham.
+
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/WebCore/CtapPinTest.cpp:
+        (TestWebKitAPI::TEST):
+        * TestWebKitAPI/Tests/WebCore/FidoTestData.h:
+        * TestWebKitAPI/Tests/WebKitCocoa/_WKWebAuthenticationPanel.mm:
+        (-[TestWebAuthenticationPanelDelegate panel:requestPINWithRemainingRetries:completionHandler:]):
+        (TestWebKitAPI::TEST):
+        * TestWebKitAPI/Tests/WebKitCocoa/web-authentication-get-assertion-hid-pin.html: Added.
+        * TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-key-agreement-error.html: Added.
+        * TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-pin-token-error.html: Added.
+        * TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-retries-error.html: Added.
+        * TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin.html: Added.
+
 2020-01-11  Alex Christensen  <achristensen@webkit.org>
 
         Expose frame information on _WKResourceLoadInfo
index d4d434f..91ebeed 100644 (file)
                55A817FF2181021A0004A39A /* 100x100-red.tga in Copy Resources */ = {isa = PBXBuildFile; fileRef = 55A817FE218101DF0004A39A /* 100x100-red.tga */; };
                55A81800218102210004A39A /* 400x400-green.png in Copy Resources */ = {isa = PBXBuildFile; fileRef = 55A817FD218101DF0004A39A /* 400x400-green.png */; };
                55F9D2E52205031800A9AB38 /* AdditionalSupportedImageTypes.mm in Sources */ = {isa = PBXBuildFile; fileRef = 55F9D2E42205031800A9AB38 /* AdditionalSupportedImageTypes.mm */; };
+               570D26F423C3CA6A00D5CF67 /* web-authentication-make-credential-hid-pin-get-key-agreement-error.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 570D26F323C3CA5500D5CF67 /* web-authentication-make-credential-hid-pin-get-key-agreement-error.html */; };
+               570D26F623C3D33000D5CF67 /* web-authentication-make-credential-hid-pin.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 570D26F523C3D32700D5CF67 /* web-authentication-make-credential-hid-pin.html */; };
+               570D26FA23C3F25100D5CF67 /* web-authentication-make-credential-hid-pin-get-pin-token-error.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 570D26F923C3F24500D5CF67 /* web-authentication-make-credential-hid-pin-get-pin-token-error.html */; };
+               570D26FC23C3F87000D5CF67 /* web-authentication-get-assertion-hid-pin.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 570D26FB23C3F86500D5CF67 /* web-authentication-get-assertion-hid-pin.html */; };
                5714ECB91CA8B5B000051AC8 /* DownloadRequestOriginalURL.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 5714ECB81CA8B58800051AC8 /* DownloadRequestOriginalURL.html */; };
                5714ECBB1CA8BFE400051AC8 /* DownloadRequestOriginalURLFrame.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 5714ECBA1CA8BFD100051AC8 /* DownloadRequestOriginalURLFrame.html */; };
                5714ECBD1CA8C22A00051AC8 /* DownloadRequestOriginalURL2.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 5714ECBC1CA8C21800051AC8 /* DownloadRequestOriginalURL2.html */; };
                573255A722139BC700396AE8 /* load-web-archive-2.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 573255A322139B9000396AE8 /* load-web-archive-2.html */; };
                574F55D2204D47F0002948C6 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 574F55D0204D471C002948C6 /* Security.framework */; };
                5758597F23A2527A00C74572 /* CtapPinTest.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 5758597E23A2527A00C74572 /* CtapPinTest.cpp */; };
+               5758598423C3C3A400C74572 /* web-authentication-make-credential-hid-pin-get-retries-error.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 5758598323C3C36200C74572 /* web-authentication-make-credential-hid-pin-get-retries-error.html */; };
                57599E211F07191900A3FB8C /* IndexedDBStructuredCloneBackwardCompatibility.mm in Sources */ = {isa = PBXBuildFile; fileRef = 57599E201F07191700A3FB8C /* IndexedDBStructuredCloneBackwardCompatibility.mm */; };
                57599E271F071AA000A3FB8C /* IndexedDBStructuredCloneBackwardCompatibility.sqlite3 in Copy Resources */ = {isa = PBXBuildFile; fileRef = 57599E241F07192C00A3FB8C /* IndexedDBStructuredCloneBackwardCompatibility.sqlite3 */; };
                57599E281F071AA000A3FB8C /* IndexedDBStructuredCloneBackwardCompatibility.sqlite3-shm in Copy Resources */ = {isa = PBXBuildFile; fileRef = 57599E261F07192C00A3FB8C /* IndexedDBStructuredCloneBackwardCompatibility.sqlite3-shm */; };
                                CD577799211CE0E4001B371E /* web-audio-only.html in Copy Resources */,
                                57663DF32357E48900E85E09 /* web-authentication-get-assertion-hid-cancel.html in Copy Resources */,
                                577454D02359B378008E1ED7 /* web-authentication-get-assertion-hid-no-credentials.html in Copy Resources */,
+                               570D26FC23C3F87000D5CF67 /* web-authentication-get-assertion-hid-pin.html in Copy Resources */,
                                57663DEC234F1F9300E85E09 /* web-authentication-get-assertion-hid.html in Copy Resources */,
                                579833922368FA37008E5547 /* web-authentication-get-assertion-nfc-multiple-tags.html in Copy Resources */,
                                57663DEA234EA66D00E85E09 /* web-authentication-get-assertion-nfc.html in Copy Resources */,
                                577454D22359BB01008E1ED7 /* web-authentication-get-assertion-u2f-no-credentials.html in Copy Resources */,
                                57C624502346C21E00383FE7 /* web-authentication-get-assertion.html in Copy Resources */,
+                               570D26F423C3CA6A00D5CF67 /* web-authentication-make-credential-hid-pin-get-key-agreement-error.html in Copy Resources */,
+                               570D26FA23C3F25100D5CF67 /* web-authentication-make-credential-hid-pin-get-pin-token-error.html in Copy Resources */,
+                               5758598423C3C3A400C74572 /* web-authentication-make-credential-hid-pin-get-retries-error.html in Copy Resources */,
+                               570D26F623C3D33000D5CF67 /* web-authentication-make-credential-hid-pin.html in Copy Resources */,
                                5798337E236019A4008E5547 /* web-authentication-make-credential-hid.html in Copy Resources */,
                                1C2B81861C89259D00A5529F /* webfont.html in Copy Resources */,
                                51714EB41CF8C78C004723C4 /* WebProcessKillIDBCleanup-1.html in Copy Resources */,
                55A817FD218101DF0004A39A /* 400x400-green.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "400x400-green.png"; sourceTree = "<group>"; };
                55A817FE218101DF0004A39A /* 100x100-red.tga */ = {isa = PBXFileReference; lastKnownFileType = file; path = "100x100-red.tga"; sourceTree = "<group>"; };
                55F9D2E42205031800A9AB38 /* AdditionalSupportedImageTypes.mm */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; path = AdditionalSupportedImageTypes.mm; sourceTree = "<group>"; };
+               570D26F323C3CA5500D5CF67 /* web-authentication-make-credential-hid-pin-get-key-agreement-error.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "web-authentication-make-credential-hid-pin-get-key-agreement-error.html"; sourceTree = "<group>"; };
+               570D26F523C3D32700D5CF67 /* web-authentication-make-credential-hid-pin.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "web-authentication-make-credential-hid-pin.html"; sourceTree = "<group>"; };
+               570D26F923C3F24500D5CF67 /* web-authentication-make-credential-hid-pin-get-pin-token-error.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "web-authentication-make-credential-hid-pin-get-pin-token-error.html"; sourceTree = "<group>"; };
+               570D26FB23C3F86500D5CF67 /* web-authentication-get-assertion-hid-pin.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "web-authentication-get-assertion-hid-pin.html"; sourceTree = "<group>"; };
                5714ECB81CA8B58800051AC8 /* DownloadRequestOriginalURL.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = DownloadRequestOriginalURL.html; sourceTree = "<group>"; };
                5714ECBA1CA8BFD100051AC8 /* DownloadRequestOriginalURLFrame.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = DownloadRequestOriginalURLFrame.html; sourceTree = "<group>"; };
                5714ECBC1CA8C21800051AC8 /* DownloadRequestOriginalURL2.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = DownloadRequestOriginalURL2.html; sourceTree = "<group>"; };
                574F55D0204D471C002948C6 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
                5758597D23A2527A00C74572 /* CtapPinTest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CtapPinTest.h; sourceTree = "<group>"; };
                5758597E23A2527A00C74572 /* CtapPinTest.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CtapPinTest.cpp; sourceTree = "<group>"; };
+               5758598323C3C36200C74572 /* web-authentication-make-credential-hid-pin-get-retries-error.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "web-authentication-make-credential-hid-pin-get-retries-error.html"; sourceTree = "<group>"; };
                57599E201F07191700A3FB8C /* IndexedDBStructuredCloneBackwardCompatibility.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = IndexedDBStructuredCloneBackwardCompatibility.mm; sourceTree = "<group>"; };
                57599E231F07192C00A3FB8C /* IndexedDBStructuredCloneBackwardCompatibilityWrite.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = IndexedDBStructuredCloneBackwardCompatibilityWrite.html; sourceTree = "<group>"; };
                57599E241F07192C00A3FB8C /* IndexedDBStructuredCloneBackwardCompatibility.sqlite3 */ = {isa = PBXFileReference; lastKnownFileType = file; path = IndexedDBStructuredCloneBackwardCompatibility.sqlite3; sourceTree = "<group>"; };
                                CD577798211CDE8F001B371E /* web-audio-only.html */,
                                57663DF22357E45D00E85E09 /* web-authentication-get-assertion-hid-cancel.html */,
                                577454CF2359B338008E1ED7 /* web-authentication-get-assertion-hid-no-credentials.html */,
+                               570D26FB23C3F86500D5CF67 /* web-authentication-get-assertion-hid-pin.html */,
                                57663DEB234F1F8000E85E09 /* web-authentication-get-assertion-hid.html */,
                                5798337B235EB65C008E5547 /* web-authentication-get-assertion-nfc-multiple-tags.html */,
                                57663DE9234EA60B00E85E09 /* web-authentication-get-assertion-nfc.html */,
                                577454D12359BAD5008E1ED7 /* web-authentication-get-assertion-u2f-no-credentials.html */,
                                57C6244F2346C1EC00383FE7 /* web-authentication-get-assertion.html */,
+                               570D26F323C3CA5500D5CF67 /* web-authentication-make-credential-hid-pin-get-key-agreement-error.html */,
+                               570D26F923C3F24500D5CF67 /* web-authentication-make-credential-hid-pin-get-pin-token-error.html */,
+                               5758598323C3C36200C74572 /* web-authentication-make-credential-hid-pin-get-retries-error.html */,
+                               570D26F523C3D32700D5CF67 /* web-authentication-make-credential-hid-pin.html */,
                                5798337D2360196D008E5547 /* web-authentication-make-credential-hid.html */,
                                51714EB21CF8C761004723C4 /* WebProcessKillIDBCleanup-1.html */,
                                51714EB31CF8C761004723C4 /* WebProcessKillIDBCleanup-2.html */,
index a63aa66..36528ea 100644 (file)
@@ -41,7 +41,7 @@
 #include <WebCore/Pin.h>
 #include <WebCore/WebAuthenticationConstants.h>
 #include <WebCore/WebAuthenticationUtils.h>
-#include <wtf/text/Base64.h>
+#include <pal/crypto/CryptoDigest.h>
 
 namespace TestWebKitAPI {
 using namespace WebCore;
@@ -98,7 +98,7 @@ TEST(CtapPinTest, TestRetriesResponse)
     // Success cases
     result = RetriesResponse::parse(convertBytesToVector(TestData::kCtapClientPinRetriesResponse, sizeof(TestData::kCtapClientPinRetriesResponse)));
     EXPECT_TRUE(result);
-    EXPECT_EQ(result->retries, 8);
+    EXPECT_EQ(result->retries, 8u);
 }
 
 TEST(CtapPinTest, TestKeyAgreementRequest)
@@ -125,6 +125,11 @@ TEST(CtapPinTest, TestKeyAgreementResponse)
     result = KeyAgreementResponse::parse(convertBytesToVector(TestData::kCtapClientPinTokenResponse, sizeof(TestData::kCtapClientPinTokenResponse))); // wrong response
     EXPECT_FALSE(result);
 
+#if (PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101400) || PLATFORM(IOS)
+    result = KeyAgreementResponse::parse(convertBytesToVector(TestData::kCtapClientPinInvalidKeyAgreementResponse, sizeof(TestData::kCtapClientPinInvalidKeyAgreementResponse))); // The point is not on the curve.
+    EXPECT_FALSE(result);
+#endif
+
     // Test COSE
     auto coseKey = encodeCOSEPublicKey(Vector<uint8_t>(65));
     coseKey[CBORValue(COSE::kty)] = CBORValue(0); // wrong kty
@@ -176,7 +181,7 @@ TEST(CtapPinTest, TestTokenRequest)
     auto token = TokenRequest::tryCreate(pin, downcast<CryptoKeyEC>(*keyPair.publicKey));
     EXPECT_TRUE(token);
     auto result = encodeAsCBOR(*token);
-    EXPECT_EQ(result.size(), 120u);
+    EXPECT_EQ(result.size(), 103u);
     EXPECT_EQ(result[0], static_cast<uint8_t>(CtapRequestCommand::kAuthenticatorClientPin));
 
     // Decode the CBOR binary to check if each field is encoded correctly.
@@ -224,12 +229,17 @@ TEST(CtapPinTest, TestTokenRequest)
     // Check the encrypted Pin.
     auto sharedKeyResult = CryptoAlgorithmECDH::platformDeriveBits(downcast<CryptoKeyEC>(*keyPair.privateKey), *cosePublicKey);
     EXPECT_TRUE(sharedKeyResult);
-    auto aesKey = CryptoKeyAES::importRaw(CryptoAlgorithmIdentifier::AES_CBC, WTFMove(*sharedKeyResult), true, CryptoKeyUsageDecrypt);
+
+    auto crypto = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256);
+    crypto->addBytes(sharedKeyResult->data(), sharedKeyResult->size());
+    auto sharedKeyHash = crypto->computeHash();
+
+    auto aesKey = CryptoKeyAES::importRaw(CryptoAlgorithmIdentifier::AES_CBC, WTFMove(sharedKeyHash), true, CryptoKeyUsageDecrypt);
     EXPECT_TRUE(aesKey);
 
     const auto& it6 = responseMap.find(CBORValue(static_cast<uint8_t>(RequestKey::kPinHashEnc)));
     EXPECT_NE(it6, responseMap.end());
-    auto pinHashResult = CryptoAlgorithmAES_CBC::platformDecrypt({ }, *aesKey, it6->second.getByteString());
+    auto pinHashResult = CryptoAlgorithmAES_CBC::platformDecrypt({ }, *aesKey, it6->second.getByteString(), CryptoAlgorithmAES_CBC::Padding::No);
     EXPECT_FALSE(pinHashResult.hasException());
     auto pinHash = pinHashResult.releaseReturnValue();
     const uint8_t expectedPinHash[] = { 0x03, 0xac, 0x67, 0x42, 0x16, 0xf3, 0xe1, 0x5c, 0x76, 0x1e, 0xe1, 0xa5, 0xe2, 0x55, 0xf0, 0x67 };
@@ -240,10 +250,10 @@ TEST(CtapPinTest, TestTokenRequest)
 TEST(CtapPinTest, TestTokenResponse)
 {
     const uint8_t sharedKeyData[] = {
-        0x03, 0xac, 0x67, 0x42, 0x16, 0xf3, 0xe1, 0x5c,
-        0x76, 0x1e, 0xe1, 0xa5, 0xe2, 0x55, 0xf0, 0x67,
-        0x95, 0x36, 0x23, 0xc8, 0xb3, 0x88, 0xb4, 0x45,
-        0x9e, 0x13, 0xf9, 0x78, 0xd7, 0xc8, 0x46, 0xf4, };
+        0x29, 0x9E, 0x65, 0xB8, 0xE7, 0x71, 0xB8, 0x1D,
+        0xB1, 0xC4, 0x8D, 0xBE, 0xCE, 0x50, 0x2A, 0x84,
+        0x05, 0x44, 0x7F, 0x46, 0x2D, 0xE6, 0x81, 0xFA,
+        0xEF, 0x0A, 0x6C, 0x67, 0xA7, 0x2B, 0xB5, 0x0F, };
     auto sharedKey = CryptoKeyAES::importRaw(CryptoAlgorithmIdentifier::AES_CBC, convertBytesToVector(sharedKeyData, sizeof(sharedKeyData)), true, CryptoKeyUsageEncrypt | CryptoKeyUsageDecrypt);
     ASSERT_TRUE(sharedKey);
 
@@ -274,10 +284,10 @@ TEST(CtapPinTest, TestPinAuth)
 {
     // 1. Generate the token.
     const uint8_t sharedKeyData[] = {
-        0x03, 0xac, 0x67, 0x42, 0x16, 0xf3, 0xe1, 0x5c,
-        0x76, 0x1e, 0xe1, 0xa5, 0xe2, 0x55, 0xf0, 0x67,
-        0x95, 0x36, 0x23, 0xc8, 0xb3, 0x88, 0xb4, 0x45,
-        0x9e, 0x13, 0xf9, 0x78, 0xd7, 0xc8, 0x46, 0xf4, };
+        0x29, 0x9E, 0x65, 0xB8, 0xE7, 0x71, 0xB8, 0x1D,
+        0xB1, 0xC4, 0x8D, 0xBE, 0xCE, 0x50, 0x2A, 0x84,
+        0x05, 0x44, 0x7F, 0x46, 0x2D, 0xE6, 0x81, 0xFA,
+        0xEF, 0x0A, 0x6C, 0x67, 0xA7, 0x2B, 0xB5, 0x0F, };
     auto sharedKey = CryptoKeyAES::importRaw(CryptoAlgorithmIdentifier::AES_CBC, convertBytesToVector(sharedKeyData, sizeof(sharedKeyData)), true, CryptoKeyUsageEncrypt | CryptoKeyUsageDecrypt);
     ASSERT_TRUE(sharedKey);
     auto result = TokenResponse::parse(*sharedKey, convertBytesToVector(TestData::kCtapClientPinTokenResponse, sizeof(TestData::kCtapClientPinTokenResponse)));
@@ -285,7 +295,7 @@ TEST(CtapPinTest, TestPinAuth)
 
     // 2. Generate the pinAuth.
     auto pinAuth = result->pinAuth(convertBytesToVector(sharedKeyData, sizeof(sharedKeyData))); // sharedKeyData pretends to be clientDataHash
-    const uint8_t expectedPinAuth[] = { 0xb3, 0xc2, 0x65, 0x1c, 0xfd, 0xc8, 0x42, 0xb4, 0x60, 0x16, 0xed, 0x20, 0x64, 0x53, 0xaf, 0x84 };
+    const uint8_t expectedPinAuth[] = { 0x0b, 0xec, 0x9d, 0xba, 0x69, 0xb0, 0x0f, 0x45, 0x0b, 0xec, 0x66, 0xb4, 0x75, 0x7f, 0x93, 0x85 };
     EXPECT_EQ(pinAuth.size(), 16u);
     EXPECT_EQ(memcmp(pinAuth.data(), expectedPinAuth, pinAuth.size()), 0);
 }
index 9a70441..fc38dab 100644 (file)
@@ -612,15 +612,15 @@ constexpr uint8_t kCtapMakeCredentialRequestWithPin[] = {
     0x62, 0x75, 0x76,
     // True(21)
     0xf5,
-    // key(8) - pinProtocol
+    // key(8) - pinAuth
     0x08,
-    // value - 1
-    0x01,
-    // key(9) - pinAuth
-    0x09,
     // bytes(16)
     0x50, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
-    0x0b, 0x0c, 0x0d, 0x0e, 0x0f
+    0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+    // key(9) - pinProtocol
+    0x09,
+    // value - 1
+    0x01,
 };
 
 constexpr uint8_t kTestComplexCtapGetAssertionRequest[] = {
@@ -745,15 +745,15 @@ constexpr uint8_t kTestComplexCtapGetAssertionRequestWithPin[] = {
     0x62, 0x75, 0x76,
     // value - True(21)
     0xf5,
-    // key(8) - pinProtocol
-    0x08,
-    // value - 1
-    0x01,
-    // key(9) - pinAuth
-    0x09,
+    // key(6) - pinAuth
+    0x06,
     // bytes(16)
     0x50, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
-    0x0b, 0x0c, 0x0d, 0x0e, 0x0f
+    0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+    // key(7) - pinProtocol
+    0x07,
+    // value - 1
+    0x01,
 };
 
 // CTAP responses --------------------------------------------------------------
@@ -1227,12 +1227,11 @@ constexpr uint8_t kCtapClientPinTokenResponse[] = {
     0xa1,
     // key(2) - pinToken
     0x02,
-    // bytes(32)
-    0x58, 0x20,
+    // bytes(16)
+    0x50,
     // encrypted token
-    0x48, 0xb9, 0x5d, 0x3d, 0x9e, 0xee, 0xe5, 0x00, 0xab, 0x51, 0x13, 0x2b,
-    0x35, 0x77, 0x66, 0x5c, 0x41, 0x7a, 0x56, 0x3e, 0xb3, 0xe2, 0x3c, 0xb7,
-    0x6a, 0x37, 0xb3, 0xde, 0x57, 0x2b, 0xca, 0xaa,
+    0x13, 0xA4, 0xEE, 0xB7, 0x0E, 0xC9, 0x1A, 0xEA, 0x00, 0x1E, 0x93, 0x16,
+    0xF6, 0x1E, 0x41, 0xF7,
 };
 
 constexpr uint8_t kCtapClientPinKeyAgreementResponse[] = {
@@ -1268,6 +1267,39 @@ constexpr uint8_t kCtapClientPinKeyAgreementResponse[] = {
     0x70, 0x45, 0xF4, 0x61, 0x2F, 0xB2, 0x0C, 0x91,
 };
 
+constexpr uint8_t kCtapClientPinInvalidKeyAgreementResponse[] = {
+    // Success
+    0x00,
+    // map(1)
+    0xA1,
+    // key(1) - keyAgreement
+    0x01,
+    // Map(5)
+    0xA5,
+    // kty: EC key type
+    0x01, 0x02,
+    // alg: ECDH256 signature algorithm
+    0x03, 0x38, 0x18,
+    // crv: P-256 curve
+    0x20, 0x01,
+    // x-coordinate
+    0x21,
+    // Bytes(32)
+    0x58, 0x20,
+    // Byte array content
+    0xE8, 0x76, 0x25, 0x89, 0x6E, 0xE4, 0xE4, 0x66, 0xC0, 0x32, 0x76, 0x6E,
+    0x80, 0x87, 0x96, 0x2F, 0x36, 0xDF, 0x9D, 0xFF, 0x8B, 0x56, 0x7F, 0x37,
+    0x63, 0x01, 0x5B, 0x19, 0x90, 0xA6, 0x0E, 0x14,
+    // y-coordinate
+    0x22,
+    // Bytes(32)
+    0x58, 0x20,
+    // Byte array content
+    0x27, 0xDE, 0x61, 0x2D, 0x66, 0x41, 0x8B, 0xDA, 0x19, 0x50, 0x58, 0x1E,
+    0xBC, 0x5C, 0x8C, 0x1D, 0xAD, 0x71, 0x0C, 0xB1, 0x4C, 0x22, 0xF8, 0xC9,
+    0x70, 0x45, 0xF4, 0x61, 0x2F, 0xB2, 0x0C, 0x91,
+};
+
 constexpr uint8_t kCtapClientPinRetriesResponse[] = {
     // Success
     0x00,
index 66f74d3..0cb2478 100644 (file)
@@ -45,6 +45,7 @@ static bool webAuthenticationPanelSucceded = false;
 static bool webAuthenticationPanelUpdateMultipleNFCTagsPresent = false;
 static bool webAuthenticationPanelUpdateNoCredentialsFound = false;
 static bool webAuthenticationPanelCancelImmediately = false;
+static String webAuthenticationPanelPin;
 
 @interface TestWebAuthenticationPanelDelegate : NSObject <_WKWebAuthenticationPanelDelegate>
 @end
@@ -83,6 +84,13 @@ static bool webAuthenticationPanelCancelImmediately = false;
     }
 }
 
+- (void)panel:(_WKWebAuthenticationPanel *)panel requestPINWithRemainingRetries:(NSUInteger)retries completionHandler:(void (^)(NSString *))completionHandler
+{
+    ASSERT_NE(panel, nil);
+    EXPECT_EQ(retries, 8ul);
+    completionHandler(webAuthenticationPanelPin);
+}
+
 @end
 
 @interface TestWebAuthenticationPanelFakeDelegate : NSObject <_WKWebAuthenticationPanelDelegate>
@@ -740,6 +748,130 @@ TEST(WebAuthenticationPanel, PanelHidCtapNoCredentialsFoundCancelNoCrash)
     Util::run(&webAuthenticationPanelUpdateNoCredentialsFound);
 }
 
+TEST(WebAuthenticationPanel, PinGetRetriesError)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-make-credential-hid-pin-get-retries-error" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    [webView waitForMessage:@"Unknown internal error. Error code: 2"];
+}
+
+TEST(WebAuthenticationPanel, PinGetKeyAgreementError)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-make-credential-hid-pin-get-key-agreement-error" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    [webView waitForMessage:@"Unknown internal error. Error code: 2"];
+}
+
+TEST(WebAuthenticationPanel, PinRequestPinErrorNoDelegate)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-make-credential-hid-pin" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    [webView waitForMessage:@"Pin is not valid: "];
+}
+
+TEST(WebAuthenticationPanel, PinRequestPinErrorNullDelegate)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-make-credential-hid-pin" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    auto delegate = adoptNS([[TestWebAuthenticationPanelUIDelegate alloc] init]);
+    [delegate setIsNull:true];
+    [webView setUIDelegate:delegate.get()];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    [webView waitForMessage:@"Pin is not valid: "];
+}
+
+TEST(WebAuthenticationPanel, PinRequestPinError)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-make-credential-hid-pin" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    auto delegate = adoptNS([[TestWebAuthenticationPanelUIDelegate alloc] init]);
+    [webView setUIDelegate:delegate.get()];
+
+    webAuthenticationPanelPin = "123";
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    [webView waitForMessage:@"Pin is not valid: 123"];
+}
+
+TEST(WebAuthenticationPanel, PinGetPinTokenError)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-make-credential-hid-pin-get-pin-token-error" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    auto delegate = adoptNS([[TestWebAuthenticationPanelUIDelegate alloc] init]);
+    [webView setUIDelegate:delegate.get()];
+
+    webAuthenticationPanelPin = "1234";
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    [webView waitForMessage:@"Unknown internal error. Error code: 2"];
+}
+
+TEST(WebAuthenticationPanel, MakeCredentialPin)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-make-credential-hid-pin" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    auto delegate = adoptNS([[TestWebAuthenticationPanelUIDelegate alloc] init]);
+    [webView setUIDelegate:delegate.get()];
+
+    webAuthenticationPanelPin = "1234";
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    [webView waitForMessage:@"Succeeded!"];
+}
+
+TEST(WebAuthenticationPanel, GetAssertionPin)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-get-assertion-hid-pin" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    auto delegate = adoptNS([[TestWebAuthenticationPanelUIDelegate alloc] init]);
+    [webView setUIDelegate:delegate.get()];
+
+    webAuthenticationPanelPin = "1234";
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    [webView waitForMessage:@"Succeeded!"];
+}
+
 } // namespace TestWebKitAPI
 
 #endif // ENABLE(WEB_AUTHN)
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-get-assertion-hid-pin.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-get-assertion-hid-pin.html
new file mode 100644 (file)
index 0000000..e31eb3b
--- /dev/null
@@ -0,0 +1,32 @@
+<input type="text" id="input">
+<script>
+    const testCtapPinAuthInvalidErrorBase64 = "Mw==";
+    const testPinGetRetriesResponseBase64 = "AKEDCA==";
+    const testPinGetKeyAgreementResponseBase64 = "AKEBpQECAzgYIAEhWCDodiWJbuTkbcAydm6Ah5YvNt+d/otWfzdjAVsZkKYOFCJYICfeYS1mQYvaGVBYHrxcjB2tcQyxTCL4yXBF9GEvsgyR";
+    const testPinGetPinTokenResponseBase64 = "AKECUBOk7rcOyRrqAB6TFvYeQfc=";
+    const testAssertionMessageBase64 =
+        "AKMBomJpZFhAKAitzuj+Tslzelf3/vZwIGtDQNgoKeFd5oEieYzhyzA65saf0tK2" +
+        "w/mooa7tQtGgDdwZIjOhjcuZ0pQ1ajoE4GR0eXBlanB1YmxpYy1rZXkCWCVGzH+5" +
+        "Z51VstuQkuHI2eXh0Ct1gPC0gSx3CWLh5I9a2AEAAABQA1hHMEUCIQCSFTuuBWgB" +
+        "4/F0VB7DlUVM09IHPmxe1MzHUwRoCRZbCAIgGKov6xoAx2MEf6/6qNs8OutzhP2C" +
+        "QoJ1L7Fe64G9uBc=";
+    if (window.internals) {
+        internals.setMockWebAuthenticationConfiguration({ hid: { supportClientPin: true, payloadBase64: [testCtapPinAuthInvalidErrorBase64, testPinGetRetriesResponseBase64, testPinGetKeyAgreementResponseBase64, testPinGetPinTokenResponseBase64, testAssertionMessageBase64] } });
+        internals.withUserGesture(() => { input.focus(); });
+    }
+
+    const options = {
+        publicKey: {
+            challenge: new Uint8Array(16),
+            timeout: 100
+        }
+    };
+
+    navigator.credentials.get(options).then(credential => {
+        // console.log("Succeeded!");
+        window.webkit.messageHandlers.testHandler.postMessage("Succeeded!");
+    }, error => {
+        // console.log(error.message);
+        window.webkit.messageHandlers.testHandler.postMessage(error.message);
+    });
+</script>
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-key-agreement-error.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-key-agreement-error.html
new file mode 100644 (file)
index 0000000..83aca98
--- /dev/null
@@ -0,0 +1,33 @@
+<input type="text" id="input">
+<script>
+    const testCtapPinInvalidErrorBase64 = "MQ==";
+    const testPinGetRetriesResponseBase64 = "AKEDCA==";
+    const testCtapInvalidParameterErrorBase64 = "Ag==";
+    if (window.internals) {
+        internals.setMockWebAuthenticationConfiguration({ hid: { supportClientPin: true, payloadBase64: [testCtapPinInvalidErrorBase64, testPinGetRetriesResponseBase64, testCtapInvalidParameterErrorBase64] } });
+        internals.withUserGesture(() => { input.focus(); });
+    }
+
+    const options = {
+        publicKey: {
+            rp: {
+                name: "localhost",
+            },
+            user: {
+                name: "John Appleseed",
+                id: new Uint8Array(16),
+                displayName: "Appleseed",
+            },
+            challenge: new Uint8Array(16),
+            pubKeyCredParams: [{ type: "public-key", alg: -7 }]
+        }
+    };
+
+    navigator.credentials.create(options).then(credential => {
+        // console.log("Succeeded!");
+        window.webkit.messageHandlers.testHandler.postMessage("Succeeded!");
+    }, error => {
+        // console.log(error.message);
+        window.webkit.messageHandlers.testHandler.postMessage(error.message);
+    });
+</script>
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-pin-token-error.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-pin-token-error.html
new file mode 100644 (file)
index 0000000..49915b3
--- /dev/null
@@ -0,0 +1,34 @@
+<input type="text" id="input">
+<script>
+    const testCtapPinInvalidErrorBase64 = "MQ==";
+    const testPinGetRetriesResponseBase64 = "AKEDCA==";
+    const testPinGetKeyAgreementResponseBase64 = "AKEBpQECAzgYIAEhWCDodiWJbuTkbcAydm6Ah5YvNt+d/otWfzdjAVsZkKYOFCJYICfeYS1mQYvaGVBYHrxcjB2tcQyxTCL4yXBF9GEvsgyR";
+    const testCtapInvalidParameterErrorBase64 = "Ag==";
+    if (window.internals) {
+        internals.setMockWebAuthenticationConfiguration({ hid: { supportClientPin: true, payloadBase64: [testCtapPinInvalidErrorBase64, testPinGetRetriesResponseBase64, testPinGetKeyAgreementResponseBase64, testCtapInvalidParameterErrorBase64] } });
+        internals.withUserGesture(() => { input.focus(); });
+    }
+
+    const options = {
+        publicKey: {
+            rp: {
+                name: "localhost",
+            },
+            user: {
+                name: "John Appleseed",
+                id: new Uint8Array(16),
+                displayName: "Appleseed",
+            },
+            challenge: new Uint8Array(16),
+            pubKeyCredParams: [{ type: "public-key", alg: -7 }]
+        }
+    };
+
+    navigator.credentials.create(options).then(credential => {
+        // console.log("Succeeded!");
+        window.webkit.messageHandlers.testHandler.postMessage("Succeeded!");
+    }, error => {
+        // console.log(error.message);
+        window.webkit.messageHandlers.testHandler.postMessage(error.message);
+    });
+</script>
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-retries-error.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin-get-retries-error.html
new file mode 100644 (file)
index 0000000..cbd1157
--- /dev/null
@@ -0,0 +1,32 @@
+<input type="text" id="input">
+<script>
+    const testCtapPinInvalidErrorBase64 = "MQ==";
+    const testCtapInvalidParameterErrorBase64 = "Ag==";
+    if (window.internals) {
+        internals.setMockWebAuthenticationConfiguration({ hid: { supportClientPin: true, payloadBase64: [testCtapPinInvalidErrorBase64, testCtapInvalidParameterErrorBase64] } });
+        internals.withUserGesture(() => { input.focus(); });
+    }
+
+    const options = {
+        publicKey: {
+            rp: {
+                name: "localhost",
+            },
+            user: {
+                name: "John Appleseed",
+                id: new Uint8Array(16),
+                displayName: "Appleseed",
+            },
+            challenge: new Uint8Array(16),
+            pubKeyCredParams: [{ type: "public-key", alg: -7 }]
+        }
+    };
+
+    navigator.credentials.create(options).then(credential => {
+        // console.log("Succeeded!");
+        window.webkit.messageHandlers.testHandler.postMessage("Succeeded!");
+    }, error => {
+        // console.log(error.message);
+        window.webkit.messageHandlers.testHandler.postMessage(error.message);
+    });
+</script>
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-hid-pin.html
new file mode 100644 (file)
index 0000000..715a0ff
--- /dev/null
@@ -0,0 +1,56 @@
+<input type="text" id="input">
+<script>
+    const testCtapPinAuthInvalidErrorBase64 = "Mw==";
+    const testPinGetRetriesResponseBase64 = "AKEDCA==";
+    const testPinGetKeyAgreementResponseBase64 = "AKEBpQECAzgYIAEhWCDodiWJbuTkbcAydm6Ah5YvNt+d/otWfzdjAVsZkKYOFCJYICfeYS1mQYvaGVBYHrxcjB2tcQyxTCL4yXBF9GEvsgyR";
+    const testPinGetPinTokenResponseBase64 = "AKECUBOk7rcOyRrqAB6TFvYeQfc=";
+    const testCreationMessageBase64 =
+        "AKMBZnBhY2tlZAJYxEbMf7lnnVWy25CS4cjZ5eHQK3WA8LSBLHcJYuHkj1rYQQAA" +
+        "AE74oBHzjApNFYAGFxEfntx9AEAoCK3O6P5OyXN6V/f+9nAga0NA2Cgp4V3mgSJ5" +
+        "jOHLMDrmxp/S0rbD+aihru1C0aAN3BkiM6GNy5nSlDVqOgTgpQECAyYgASFYIEFb" +
+        "he3RkNud6sgyraBGjlh1pzTlCZehQlL/b18HZ6WGIlggJgfUd/en9p5AIqMQbUni" +
+        "nEeXdFLkvW0/zV5BpEjjNxADo2NhbGcmY3NpZ1hHMEUCIQDKg+ZBmEBtf0lWq4Re" +
+        "dH4/i/LOYqOR4uR2NAj2zQmw9QIgbTXb4hvFbj4T27bv/rGrc+y+0puoYOBkBk9P" +
+        "mCewWlNjeDVjgVkCwjCCAr4wggGmoAMCAQICBHSG/cIwDQYJKoZIhvcNAQELBQAw" +
+        "LjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEw" +
+        "IBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG8xCzAJBgNVBAYTAlNF" +
+        "MRIwEAYDVQQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0" +
+        "ZXN0YXRpb24xKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE5NTUwMDM4" +
+        "NDIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASVXfOt9yR9MXXv/ZzE8xpOh466" +
+        "4YEJVmFQ+ziLLl9lJ79XQJqlgaUNCsUvGERcChNUihNTyKTlmnBOUjvATevto2ww" +
+        "ajAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuMTATBgsrBgEEAYLl" +
+        "HAIBAQQEAwIFIDAhBgsrBgEEAYLlHAEBBAQSBBD4oBHzjApNFYAGFxEfntx9MAwG" +
+        "A1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggEBADFcSIDmmlJ+OGaJvWn9Cqhv" +
+        "SeueToVFQVVvqtALOgCKHdwB+Wx29mg2GpHiMsgQp5xjB0ybbnpG6x212FxESJ+G" +
+        "inZD0ipchi7APwPlhIvjgH16zVX44a4e4hOsc6tLIOP71SaMsHuHgCcdH0vg5d2s" +
+        "c006WJe9TXO6fzV+ogjJnYpNKQLmCXoAXE3JBNwKGBIOCvfQDPyWmiiG5bGxYfPt" +
+        "y8Z3pnjX+1MDnM2hhr40ulMxlSNDnX/ZSnDyMGIbk8TOQmjTF02UO8auP8k3wt5D" +
+        "1rROIRU9+FCSX5WQYi68RuDrGMZB8P5+byoJqbKQdxn2LmE1oZAyohPAmLcoPO4=";
+    if (window.internals) {
+        internals.setMockWebAuthenticationConfiguration({ hid: { supportClientPin: true, payloadBase64: [testCtapPinAuthInvalidErrorBase64, testPinGetRetriesResponseBase64, testPinGetKeyAgreementResponseBase64, testPinGetPinTokenResponseBase64, testCreationMessageBase64] } });
+        internals.withUserGesture(() => { input.focus(); });
+    }
+
+    const options = {
+        publicKey: {
+            rp: {
+                name: "localhost",
+            },
+            user: {
+                name: "John Appleseed",
+                id: new Uint8Array(16),
+                displayName: "Appleseed",
+            },
+            challenge: new Uint8Array(16),
+            pubKeyCredParams: [{ type: "public-key", alg: -7 }]
+        }
+    };
+
+    navigator.credentials.create(options).then(credential => {
+        // console.log("Succeeded!");
+        window.webkit.messageHandlers.testHandler.postMessage("Succeeded!");
+    }, error => {
+        // console.log(error.message);
+        window.webkit.messageHandlers.testHandler.postMessage(error.message);
+    });
+</script>