[WebAuthn] Formalize the Keychain schema
authorjiewen_tan@apple.com <jiewen_tan@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 11 Mar 2020 22:42:08 +0000 (22:42 +0000)
committerjiewen_tan@apple.com <jiewen_tan@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 11 Mar 2020 22:42:08 +0000 (22:42 +0000)
https://bugs.webkit.org/show_bug.cgi?id=183533
<rdar://problem/43347926>

Reviewed by Brent Fulgham.

Source/WebCore:

Covered by new test contents within existing files.

* Modules/webauthn/AuthenticatorAssertionResponse.cpp:
(WebCore::AuthenticatorAssertionResponse::create):
(WebCore::AuthenticatorAssertionResponse::AuthenticatorAssertionResponse):
* Modules/webauthn/AuthenticatorAssertionResponse.h:
Modifies the constructors to accept userEntity.name.

* Modules/webauthn/cbor/CBORValue.h:
Adds a FIXME.

* testing/MockWebAuthenticationConfiguration.h:
(WebCore::MockWebAuthenticationConfiguration::LocalConfiguration::encode const):
(WebCore::MockWebAuthenticationConfiguration::LocalConfiguration::decode):
* testing/MockWebAuthenticationConfiguration.idl:
Modifies the test infra to use Credential ID as the unique identifier for a credential instead of
the original combination of RP ID and user handle.

Source/WebKit:

This patch formalizes the schema for the Keychain as follows:
kSecAttrLabel: RP ID
kSecAttrApplicationLabel: Credential ID (auto-gen by Keychain)
kSecAttrApplicationTag: { "id": UserEntity.id, "name": UserEntity.name } (CBOR encoded)
Noted, the vale of kSecAttrApplicationLabel is automatically generated by the Keychain, which is a SHA-1 hash of
the public key.

According to the Step 7. from https://www.w3.org/TR/webauthn/#op-make-cred, the following fields are mandatory
1. rpId (rpEntity.id);
2. userHandle (userEntity.id), this is required for authenticators that support resident keys;
3. credentialId.

Some other optional fields are:
(from https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity)
1. rpEntity.name;
2. rpEnitty.icon;
(from https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity)
3. userEntity.displayName;
4. userEntity.name;
5. userEntity.icon;
(from https://www.w3.org/TR/webauthn/#sign-counter)
6. signature counter.

Among the six possible fields, only 4. is chosen to store. Here is why:
For rpEntity, rpEntity.id which is either the domain or the eTLD + 1 of the website is
sufficient enough to either classify the credential or serving the UI. Also, this is the only
trustworthy information that the UserAgent produce. Others could potentially be used by
malicious websites for attacking the Keychain or spoofing/phishing users when being displayed
in the UI. Also, rpEnitty.icon is a URL to the website's favicon, which if not implemented
correctly can be used for tracking.

For userEntity, userEntity.name is the human readable version of userEntity.id, and therefore
is chosen to store such that later on WebKit can pass it to UI client to help users disambiguate
different credentials. And it is necessary as userEntity.id is not guaranteed to be human
readable. Others are abandoned for the very same reason as above.

We hard code a zero value for 'signature counter'. While this is a theoretically interesting
technique for a RP to detect private key cloning, it is unlikely to be useful in practice.
We store the private keys in our SEP. This counter would only be a meaningful protection if
adversaries were able to extract private key data from the SEP without Apple noticing, but
were not able to manipulate this counter to fool the RP.

In terms of the schema,
1) RP ID is needed to query all credentials related, and therefore it needs a column and kSecAttrLabel
is supposed to be human readable;
2) kSecAttrApplicationLabel is the auto generated programmatical identifier for a SecItem, and
therefore is suitable as the credential ID. Given the input to the SHA-1 is generated by us, and
it is only needed to be powerful enough to be unique across the keychain within a device, and potentially
to be unique across different other credential ID for the same user. The SHA-1 collision attack
doesn't seem valid here.
3) kSecAttrApplicationTag is the only other column Keychain allows applications to modify. Therefore,
UserEntity.id and UserEntity.name is bundled to use this slot. The reason to use CBOR here is that
it is more friendly then JSON to encode binaries, and it is used widely in WebAuthn.

* UIProcess/WebAuthentication/Cocoa/LocalAuthenticator.h:
* UIProcess/WebAuthentication/Cocoa/LocalAuthenticator.mm:
(WebKit::LocalAuthenticatorInternal::toArrayBuffer):
(WebKit::LocalAuthenticatorInternal::getExistingCredentials):
(WebKit::LocalAuthenticator::makeCredential):
(WebKit::LocalAuthenticator::continueMakeCredentialAfterUserVerification):
(WebKit::LocalAuthenticator::continueMakeCredentialAfterAttested):
(WebKit::LocalAuthenticator::getAssertion):
(WebKit::LocalAuthenticator::deleteDuplicateCredential const):
* UIProcess/WebAuthentication/Mock/MockLocalConnection.mm:
(WebKit::MockLocalConnection::filterResponses const):

Tools:

Modifies the test infra to use Credential ID as the unique identifier for a credential instead of
the original combination of RP ID and user handle.

* WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl:
* WebKitTestRunner/InjectedBundle/TestRunner.cpp:
(WTR::TestRunner::cleanUpKeychain):
(WTR::TestRunner::keyExistsInKeychain):
* WebKitTestRunner/InjectedBundle/TestRunner.h:
* WebKitTestRunner/TestController.h:
* WebKitTestRunner/TestInvocation.cpp:
(WTR::TestInvocation::didReceiveSynchronousMessageFromInjectedBundle):
* WebKitTestRunner/cocoa/TestControllerCocoa.mm:
(WTR::TestController::cleanUpKeychain):
(WTR::TestController::keyExistsInKeychain):

LayoutTests:

New tests are added and all tests are modified to use Credential ID to identify a credential instead
of { RP ID, user handle }.

* http/wpt/webauthn/public-key-credential-create-failure-local-silent.https-expected.txt:
* http/wpt/webauthn/public-key-credential-create-failure-local-silent.https.html:
* http/wpt/webauthn/public-key-credential-create-failure-local.https-expected.txt:
* http/wpt/webauthn/public-key-credential-create-failure-local.https.html:
* http/wpt/webauthn/public-key-credential-create-success-local.https-expected.txt:
* http/wpt/webauthn/public-key-credential-create-success-local.https.html:
* http/wpt/webauthn/public-key-credential-get-failure-local-silent.https-expected.txt:
* http/wpt/webauthn/public-key-credential-get-failure-local-silent.https.html:
* http/wpt/webauthn/public-key-credential-get-failure-local.https.html:
* http/wpt/webauthn/public-key-credential-get-success-local.https.html:
* http/wpt/webauthn/resources/util.js:

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

30 files changed:
LayoutTests/ChangeLog
LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-local-silent.https-expected.txt
LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-local-silent.https.html
LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-local.https-expected.txt
LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-local.https.html
LayoutTests/http/wpt/webauthn/public-key-credential-create-success-local.https-expected.txt
LayoutTests/http/wpt/webauthn/public-key-credential-create-success-local.https.html
LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-local-silent.https-expected.txt
LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-local-silent.https.html
LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-local.https.html
LayoutTests/http/wpt/webauthn/public-key-credential-get-success-local.https.html
LayoutTests/http/wpt/webauthn/resources/util.js
Source/WebCore/ChangeLog
Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.cpp
Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.h
Source/WebCore/Modules/webauthn/cbor/CBORValue.h
Source/WebCore/testing/MockWebAuthenticationConfiguration.h
Source/WebCore/testing/MockWebAuthenticationConfiguration.idl
Source/WebKit/ChangeLog
Source/WebKit/UIProcess/WebAuthentication/Cocoa/LocalAuthenticator.h
Source/WebKit/UIProcess/WebAuthentication/Cocoa/LocalAuthenticator.mm
Source/WebKit/UIProcess/WebAuthentication/Mock/MockLocalConnection.mm
Tools/ChangeLog
Tools/TestWebKitAPI/Tests/WebKitCocoa/_WKWebAuthenticationPanel.mm
Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl
Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp
Tools/WebKitTestRunner/InjectedBundle/TestRunner.h
Tools/WebKitTestRunner/TestController.h
Tools/WebKitTestRunner/TestInvocation.cpp
Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm

index 915d373..f9a735d 100644 (file)
@@ -1,3 +1,26 @@
+2020-03-11  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthn] Formalize the Keychain schema
+        https://bugs.webkit.org/show_bug.cgi?id=183533
+        <rdar://problem/43347926>
+
+        Reviewed by Brent Fulgham.
+
+        New tests are added and all tests are modified to use Credential ID to identify a credential instead
+        of { RP ID, user handle }.
+
+        * http/wpt/webauthn/public-key-credential-create-failure-local-silent.https-expected.txt:
+        * http/wpt/webauthn/public-key-credential-create-failure-local-silent.https.html:
+        * http/wpt/webauthn/public-key-credential-create-failure-local.https-expected.txt:
+        * http/wpt/webauthn/public-key-credential-create-failure-local.https.html:
+        * http/wpt/webauthn/public-key-credential-create-success-local.https-expected.txt:
+        * http/wpt/webauthn/public-key-credential-create-success-local.https.html:
+        * http/wpt/webauthn/public-key-credential-get-failure-local-silent.https-expected.txt:
+        * http/wpt/webauthn/public-key-credential-get-failure-local-silent.https.html:
+        * http/wpt/webauthn/public-key-credential-get-failure-local.https.html:
+        * http/wpt/webauthn/public-key-credential-get-success-local.https.html:
+        * http/wpt/webauthn/resources/util.js:
+
 2020-03-11  Myles C. Maxfield  <mmaxfield@apple.com>
 
         icloud.com Notes text in titles and headings is distorted
index 667941f..b63fd74 100644 (file)
@@ -1,8 +1,8 @@
 
-PASS PublicKeyCredential's [[create]] with silent failure in a mock local authenticator. 
-PASS PublicKeyCredential's [[create]] with silent failure in a mock local authenticator. 2 
-PASS PublicKeyCredential's [[create]] with silent failure in a mock local authenticator. 3 
-PASS PublicKeyCredential's [[create]] with silent failure in a mock local authenticator. 4 
-PASS PublicKeyCredential's [[create]] with silent failure in a mock local authenticator. 5 
-PASS PublicKeyCredential's [[create]] with silent failure in a mock local authenticator. 6 
+PASS PublicKeyCredential's [[create]] with unsupported public key credential parameters in a mock local authenticator. 
+PASS PublicKeyCredential's [[create]] with matched exclude credentials in a mock local authenticator. 
+PASS PublicKeyCredential's [[create]] with matched exclude credentials in a mock local authenticator. 2nd 
+PASS PublicKeyCredential's [[create]] without user consent in a mock local authenticator. 
+PASS PublicKeyCredential's [[create]] without private keys in a mock local authenticator. 
+PASS PublicKeyCredential's [[create]] without attestation in a mock local authenticator. 
 
index 4dcf4a2..cb2d026 100644 (file)
 <script src="/resources/testharnessreport.js"></script>
 <script src="./resources/util.js"></script>
 <script>
-    (async function() {
-        const userhandleBase64 = generateUserhandleBase64();
+    // Default mock configuration. Tests need to override if they need different configuration.
+    if (window.internals)
+        internals.setMockWebAuthenticationConfiguration({ silentFailure: true, local: { acceptAuthentication: false, acceptAttestation: false } });
+
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -35 }, { type: "public-key", alg: -257 }], // ES384, RS256
+                timeout: 10
+            }
+        };
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[create]] with unsupported public key credential parameters in a mock local authenticator.");
+
+    promise_test(async t => {
         const privateKeyBase64 = await generatePrivateKeyBase64();
         const credentialID = await calculateCredentialID(privateKeyBase64);
-        // Default mock configuration. Tests need to override if they need different configuration.
-        if (window.internals)
-            internals.setMockWebAuthenticationConfiguration({ silentFailure: true, local: { acceptAuthentication: false, acceptAttestation: false } });
-
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: "John Appleseed",
-                        id: Base64URL.parse(testUserhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -35 }, { type: "public-key", alg: -257 }], // ES384, RS256
-                    timeout: 10
-                }
-            };
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
-        }, "PublicKeyCredential's [[create]] with silent failure in a mock local authenticator.");
+        const credentialIDBase64 = base64encode(credentialID);
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: "John Appleseed",
-                        id: Base64URL.parse(userhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    excludeCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }],
-                    timeout: 10
-                }
-            };
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [{ type: "public-key", id: credentialID }],
+                timeout: 10
+            }
+        };
+        if (window.testRunner)
+            testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, testUserEntityBundleBase64);
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.").then(() => {
             if (window.testRunner)
-                testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, userhandleBase64);
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.").then(() => {
-                if (window.testRunner)
-                    testRunner.cleanUpKeychain(testRpId, userhandleBase64);
-            });
-        }, "PublicKeyCredential's [[create]] with silent failure in a mock local authenticator. 2");
+                testRunner.cleanUpKeychain(testRpId, credentialIDBase64);
+        });
+    }, "PublicKeyCredential's [[create]] with matched exclude credentials in a mock local authenticator.");
+
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const credentialIDBase64 = base64encode(credentialID);
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: "John Appleseed",
-                        id: Base64URL.parse(userhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    excludeCredentials: [
-                        { type: "public-key", id: credentialID, transports: ["usb"] },
-                        { type: "public-key", id: credentialID, transports: ["nfc"] },
-                        { type: "public-key", id: credentialID, transports: ["ble"] },
-                        { type: "public-key", id: credentialID, transports: ["internal"] }
-                    ],
-                    timeout: 10
-                }
-            };
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [
+                    { type: "public-key", id: credentialID, transports: ["usb"] },
+                    { type: "public-key", id: credentialID, transports: ["nfc"] },
+                    { type: "public-key", id: credentialID, transports: ["ble"] },
+                    { type: "public-key", id: credentialID, transports: ["internal"] }
+                ],
+                timeout: 10
+            }
+        };
+        if (window.testRunner)
+            testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, testUserEntityBundleBase64);
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.").then(() => {
             if (window.testRunner)
-                testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, userhandleBase64);
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.").then(() => {
-                if (window.testRunner)
-                    testRunner.cleanUpKeychain(testRpId, userhandleBase64);
-            });
-        }, "PublicKeyCredential's [[create]] with silent failure in a mock local authenticator. 3");
+                testRunner.cleanUpKeychain(testRpId, credentialIDBase64);
+        });
+    }, "PublicKeyCredential's [[create]] with matched exclude credentials in a mock local authenticator. 2nd");
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: "John Appleseed",
-                        id: Base64URL.parse(testUserhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    timeout: 10
-                }
-            };
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
-        }, "PublicKeyCredential's [[create]] with silent failure in a mock local authenticator. 4");
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                timeout: 10
+            }
+        };
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[create]] without user consent in a mock local authenticator.");
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: "John Appleseed",
-                        id: Base64URL.parse(testUserhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    timeout: 10
-                }
-            };
-            if (window.internals)
-                internals.setMockWebAuthenticationConfiguration({ silentFailure: true, local: { acceptAuthentication: true, acceptAttestation: false } });
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
-        }, "PublicKeyCredential's [[create]] with silent failure in a mock local authenticator. 5");
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                timeout: 10
+            }
+        };
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({ silentFailure: true, local: { acceptAuthentication: true, acceptAttestation: false } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[create]] without private keys in a mock local authenticator.");
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: "John Appleseed",
-                        id: Base64URL.parse(userhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    timeout: 10
-                }
-            };
-            if (window.internals) {
-                internals.setMockWebAuthenticationConfiguration({ silentFailure: true, local: { acceptAuthentication: true, acceptAttestation: false } });
-                testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, userhandleBase64);
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const credentialIDBase64 = base64encode(credentialID);
+
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                attestation: "direct",
+                timeout: 10
             }
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.").then(() => {
-                if (window.testRunner)
-                    assert_false(testRunner.keyExistsInKeychain(testRpId, userhandleBase64));
-            });
-        }, "PublicKeyCredential's [[create]] with silent failure in a mock local authenticator. 6");
-    })();
+        };
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({ silentFailure: true, local: { acceptAuthentication: true, acceptAttestation: false, privateKeyBase64: privateKeyBase64 } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.").then(() => {
+            if (window.testRunner)
+                testRunner.cleanUpKeychain(testRpId, credentialIDBase64);
+        });
+    }, "PublicKeyCredential's [[create]] without attestation in a mock local authenticator.");
 </script>
index 8634f2c..6a1025a 100644 (file)
@@ -5,6 +5,6 @@ PASS PublicKeyCredential's [[create]] with matched exclude credentials in a mock
 PASS PublicKeyCredential's [[create]] without user consent in a mock local authenticator. 
 PASS PublicKeyCredential's [[create]] without private keys in a mock local authenticator. 
 PASS PublicKeyCredential's [[create]] without attestation in a mock local authenticator. 
-PASS PublicKeyCredential's [[create]] deleting old credential in a mock local authenticator. 
+PASS PublicKeyCredential's [[create]] not deleting old credential in a mock local authenticator. 
 PASS PublicKeyCredential's [[create]] with timeout in a mock local authenticator. 
 
index 0a5abe2..570ac08 100644 (file)
 <script src="/resources/testharnessreport.js"></script>
 <script src="./resources/util.js"></script>
 <script>
-    (async function() {
-        const userhandleBase64 = generateUserhandleBase64();
+    // Default mock configuration. Tests need to override if they need different configuration.
+    if (window.internals)
+        internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: false, acceptAttestation: false } });
+
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -35 }, { type: "public-key", alg: -257 }], // ES384, RS256
+            }
+        };
+        return promiseRejects(t, "NotSupportedError", navigator.credentials.create(options), "The platform attached authenticator doesn't support any provided PublicKeyCredentialParameters.");
+    }, "PublicKeyCredential's [[create]] with unsupported public key credential parameters in a mock local authenticator.");
+
+    promise_test(async t => {
         const privateKeyBase64 = await generatePrivateKeyBase64();
         const credentialID = await calculateCredentialID(privateKeyBase64);
-        // Default mock configuration. Tests need to override if they need different configuration.
-        if (window.internals)
-            internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: false, acceptAttestation: false } });
-
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: "John Appleseed",
-                        id: Base64URL.parse(testUserhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -35 }, { type: "public-key", alg: -257 }], // ES384, RS256
-                }
-            };
-            return promiseRejects(t, "NotSupportedError", navigator.credentials.create(options), "The platform attached authenticator doesn't support any provided PublicKeyCredentialParameters.");
-        }, "PublicKeyCredential's [[create]] with unsupported public key credential parameters in a mock local authenticator.");
+        const credentialIDBase64 = base64encode(credentialID);
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: "John Appleseed",
-                        id: Base64URL.parse(userhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    excludeCredentials: [{ type: "public-key", id: credentialID }]
-                }
-            };
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [{ type: "public-key", id: credentialID }]
+            }
+        };
+        if (window.testRunner)
+            testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, testUserEntityBundleBase64);
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator.").then(() => {
             if (window.testRunner)
-                testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, userhandleBase64);
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator.").then(() => {
-                if (window.testRunner)
-                    testRunner.cleanUpKeychain(testRpId, userhandleBase64);
-            });
-        }, "PublicKeyCredential's [[create]] with matched exclude credentials in a mock local authenticator.");
+                testRunner.cleanUpKeychain(testRpId, credentialIDBase64);
+        });
+    }, "PublicKeyCredential's [[create]] with matched exclude credentials in a mock local authenticator.");
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: "John Appleseed",
-                        id: Base64URL.parse(userhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    excludeCredentials: [
-                        { type: "public-key", id: credentialID, transports: ["usb"] },
-                        { type: "public-key", id: credentialID, transports: ["nfc"] },
-                        { type: "public-key", id: credentialID, transports: ["ble"] },
-                        { type: "public-key", id: credentialID, transports: ["internal"] }
-                    ]
-                }
-            };
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const credentialIDBase64 = base64encode(credentialID);
+
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [
+                    { type: "public-key", id: credentialID, transports: ["usb"] },
+                    { type: "public-key", id: credentialID, transports: ["nfc"] },
+                    { type: "public-key", id: credentialID, transports: ["ble"] },
+                    { type: "public-key", id: credentialID, transports: ["internal"] }
+                ]
+            }
+        };
+        if (window.testRunner)
+            testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, testUserEntityBundleBase64);
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator.").then(() => {
             if (window.testRunner)
-                testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, userhandleBase64);
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator.").then(() => {
-                if (window.testRunner)
-                    testRunner.cleanUpKeychain(testRpId, userhandleBase64);
-            });
-        }, "PublicKeyCredential's [[create]] with matched exclude credentials in a mock local authenticator. 2nd");
+                testRunner.cleanUpKeychain(testRpId, credentialIDBase64);
+        });
+    }, "PublicKeyCredential's [[create]] with matched exclude credentials in a mock local authenticator. 2nd");
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: "John Appleseed",
-                        id: Base64URL.parse(testUserhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }]
-                }
-            };
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Couldn't verify user.");
-        }, "PublicKeyCredential's [[create]] without user consent in a mock local authenticator.");
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }]
+            }
+        };
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Couldn't verify user.");
+    }, "PublicKeyCredential's [[create]] without user consent in a mock local authenticator.");
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: "John Appleseed",
-                        id: Base64URL.parse(testUserhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }]
-                }
-            };
-            if (window.internals)
-                internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: true, acceptAttestation: false } });
-            return promiseRejects(t, "UnknownError", navigator.credentials.create(options), "Couldn't create private key.");
-        }, "PublicKeyCredential's [[create]] without private keys in a mock local authenticator.");
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }]
+            }
+        };
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: true, acceptAttestation: false } });
+        return promiseRejects(t, "UnknownError", navigator.credentials.create(options), "Couldn't create private key.");
+    }, "PublicKeyCredential's [[create]] without private keys in a mock local authenticator.");
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: "John Appleseed",
-                        id: Base64URL.parse(userhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    attestation: "direct"
-                }
-            };
-            if (window.internals)
-                internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: true, acceptAttestation: false, privateKeyBase64: privateKeyBase64 } });
-            return promiseRejects(t, "UnknownError", navigator.credentials.create(options), "Couldn't attest: The operation couldn't complete.").then(() => {
-                if (window.testRunner)
-                    testRunner.cleanUpKeychain(testRpId, userhandleBase64);
-            });
-        }, "PublicKeyCredential's [[create]] without attestation in a mock local authenticator.");
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const credentialIDBase64 = base64encode(credentialID);
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: userhandleBase64,
-                        id: Base64URL.parse(userhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }]
-                }
-            };
-            if (window.internals) {
-                internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: true, acceptAttestation: false } });
-                testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, userhandleBase64);
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                attestation: "direct"
             }
-            return promiseRejects(t, "UnknownError", navigator.credentials.create(options), "Couldn't create private key.").then(() => {
-                if (window.testRunner)
-                    assert_false(testRunner.keyExistsInKeychain(testRpId, userhandleBase64));
-            });
-        }, "PublicKeyCredential's [[create]] deleting old credential in a mock local authenticator.");
+        };
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: true, acceptAttestation: false, privateKeyBase64: privateKeyBase64 } });
+        return promiseRejects(t, "UnknownError", navigator.credentials.create(options), "Couldn't attest: The operation couldn't complete.").then(() => {
+            if (window.testRunner)
+                testRunner.cleanUpKeychain(testRpId, credentialIDBase64);
+        });
+    }, "PublicKeyCredential's [[create]] without attestation in a mock local authenticator.");
 
-        promise_test(function(t) {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: "John Appleseed",
-                        id: asciiToUint8Array("123456"),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    timeout: 10,
-                    authenticatorSelection: { authenticatorAttachment: "cross-platform" }
-                }
-            };
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const credentialIDBase64 = base64encode(credentialID);
 
-            if (window.internals)
-                internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: false, acceptAttestation: false } });
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
-        }, "PublicKeyCredential's [[create]] with timeout in a mock local authenticator.");
-    })();
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: testUserhandleBase64,
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }]
+            }
+        };
+        if (window.internals) {
+            internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: true, acceptAttestation: false } });
+            testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, testUserEntityBundleBase64);
+        }
+        return promiseRejects(t, "UnknownError", navigator.credentials.create(options), "Couldn't create private key.").then(() => {
+            if (window.testRunner)
+                assert_true(testRunner.keyExistsInKeychain(testRpId, credentialIDBase64));
+                testRunner.cleanUpKeychain(testRpId, credentialIDBase64);
+        });
+    }, "PublicKeyCredential's [[create]] not deleting old credential in a mock local authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                timeout: 10,
+                authenticatorSelection: { authenticatorAttachment: "cross-platform" }
+            }
+        };
+
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: false, acceptAttestation: false } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[create]] with timeout in a mock local authenticator.");
 </script>
index fadac21..6510980 100644 (file)
@@ -5,4 +5,6 @@ PASS PublicKeyCredential's [[create]] with matched exclude credential ids but no
 PASS PublicKeyCredential's [[create]] with none attestation in a mock local authenticator. 
 PASS PublicKeyCredential's [[create]] with indirect attestation in a mock local authenticator. 
 PASS PublicKeyCredential's [[create]] with direct attestation in a mock local authenticator. 
+PASS PublicKeyCredential's [[create]] with duplicate credential in a mock local authenticator. 
+PASS PublicKeyCredential's [[create]] with duplicate credential in a mock local authenticator. 2 
 
index 8aaf5af..bd30761 100644 (file)
 <script src="./resources/util.js"></script>
 <script src="./resources/cbor.js"></script>
 <script>
-    (async function() {
-        const userhandleBase64 = generateUserhandleBase64();
+    function checkResult(credential, credentialID, isNoneAttestation = true)
+    {
+        // Check keychain
+        if (window.testRunner) {
+            assert_true(testRunner.keyExistsInKeychain(testRpId, base64encode(credentialID)));
+            testRunner.cleanUpKeychain(testRpId, base64encode(credentialID));
+        }
+
+        // Check respond
+        assert_array_equals(Base64URL.parse(credential.id), credentialID);
+        assert_equals(credential.type, 'public-key');
+        assert_array_equals(new Uint8Array(credential.rawId), credentialID);
+        assert_equals(bytesToASCIIString(credential.response.clientDataJSON), '{"type":"webauthn.create","challenge":"MTIzNDU2","origin":"https://localhost:9443"}');
+        assert_not_own_property(credential.getClientExtensionResults(), "appid");
+
+        // Check attestation
+        const attestationObject = CBOR.decode(credential.response.attestationObject);
+        if (isNoneAttestation)
+            assert_equals(attestationObject.fmt, "none");
+        else
+            assert_equals(attestationObject.fmt, "apple");
+        // Check authData
+        const authData = decodeAuthData(attestationObject.authData);
+        assert_equals(bytesToHexString(authData.rpIdHash), "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763");
+        assert_equals(authData.flags, 69);
+        assert_equals(authData.counter, 0);
+        assert_equals(bytesToHexString(authData.aaguid), "00000000000000000000000000000000");
+        assert_array_equals(authData.credentialID, credentialID);
+        // Check self attestation
+        assert_true(checkPublicKey(authData.publicKey));
+        if (isNoneAttestation)
+            assert_object_equals(attestationObject.attStmt, { });
+        else {
+            assert_equals(attestationObject.attStmt.alg, -7);
+            assert_equals(attestationObject.attStmt.x5c.length, 2);
+            assert_array_equals(attestationObject.attStmt.x5c[0], Base64URL.parse(testAttestationCertificateBase64));
+            assert_array_equals(attestationObject.attStmt.x5c[1], Base64URL.parse(testAttestationIssuingCACertificateBase64));
+        }
+    }
+
+    promise_test(async t => {
         const privateKeyBase64 = await generatePrivateKeyBase64();
         const credentialID = await calculateCredentialID(privateKeyBase64);
-        // Default mock configuration. Tests need to override if they need different configuration.
+        const userhandleBase64 = generateUserhandleBase64();
         if (window.internals)
             internals.setMockWebAuthenticationConfiguration({
                 local: {
                     acceptAuthentication: true,
                     acceptAttestation: false,
                     privateKeyBase64: privateKeyBase64,
-                    userCertificateBase64: testAttestationCertificateBase64,
-                    intermediateCACertificateBase64: testAttestationIssuingCACertificateBase64
                 }
             });
 
-        function checkResult(credential, isNoneAttestation = true)
-        {
-            // Check keychain
-            if (window.testRunner) {
-                assert_true(testRunner.keyExistsInKeychain(testRpId, userhandleBase64));
-                testRunner.cleanUpKeychain(testRpId, userhandleBase64);
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "localhost",
+                },
+                user: {
+                    name: userhandleBase64,
+                    id: Base64URL.parse(userhandleBase64),
+                    displayName: "Appleseed",
+                },
+                challenge: Base64URL.parse("MTIzNDU2"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
             }
+        };
 
-            // Check respond
-            assert_array_equals(Base64URL.parse(credential.id), credentialID);
-            assert_equals(credential.type, 'public-key');
-            assert_array_equals(new Uint8Array(credential.rawId), credentialID);
-            assert_equals(bytesToASCIIString(credential.response.clientDataJSON), '{"type":"webauthn.create","challenge":"MTIzNDU2","origin":"https://localhost:9443"}');
-            assert_not_own_property(credential.getClientExtensionResults(), "appid");
-
-            // Check attestation
-            const attestationObject = CBOR.decode(credential.response.attestationObject);
-            if (isNoneAttestation)
-                assert_equals(attestationObject.fmt, "none");
-            else
-                assert_equals(attestationObject.fmt, "apple");
-            // Check authData
-            const authData = decodeAuthData(attestationObject.authData);
-            assert_equals(bytesToHexString(authData.rpIdHash), "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763");
-            assert_equals(authData.flags, 69);
-            assert_equals(authData.counter, 0);
-            assert_equals(bytesToHexString(authData.aaguid), "00000000000000000000000000000000");
-            assert_array_equals(authData.credentialID, credentialID);
-            // Check self attestation
-            assert_true(checkPublicKey(authData.publicKey));
-            if (isNoneAttestation)
-                assert_object_equals(attestationObject.attStmt, { });
-            else {
-                assert_equals(attestationObject.attStmt.alg, -7);
-                assert_equals(attestationObject.attStmt.x5c.length, 2);
-                assert_array_equals(attestationObject.attStmt.x5c[0], Base64URL.parse(testAttestationCertificateBase64));
-                assert_array_equals(attestationObject.attStmt.x5c[1], Base64URL.parse(testAttestationIssuingCACertificateBase64));
-            }
-        }
+        return navigator.credentials.create(options).then(credential => {
+            checkResult(credential, credentialID);
+        });
+    }, "PublicKeyCredential's [[create]] with minimum options in a mock local authenticator.");
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "localhost",
-                    },
-                    user: {
-                        name: userhandleBase64,
-                        id: Base64URL.parse(userhandleBase64),
-                        displayName: "Appleseed",
-                    },
-                    challenge: Base64URL.parse("MTIzNDU2"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const userhandleBase64 = generateUserhandleBase64();
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({
+                local: {
+                    acceptAuthentication: true,
+                    acceptAttestation: false,
+                    privateKeyBase64: privateKeyBase64,
                 }
-            };
-
-            return navigator.credentials.create(options).then(credential => {
-                checkResult(credential);
             });
-        }, "PublicKeyCredential's [[create]] with minimum options in a mock local authenticator.");
-
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "localhost",
-                    },
-                    user: {
-                        name: userhandleBase64,
-                        id: Base64URL.parse(userhandleBase64),
-                        displayName: "Appleseed",
-                    },
-                    challenge: Base64URL.parse("MTIzNDU2"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    authenticatorSelection: { authenticatorAttachment: "platform" }
-                }
-            };
 
-            return navigator.credentials.create(options).then(credential => {
-                checkResult(credential);
-            });
-        }, "PublicKeyCredential's [[create]] with authenticatorSelection { 'platform' } in a mock local authenticator.");
-
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "example.com"
-                    },
-                    user: {
-                        name: userhandleBase64,
-                        id: Base64URL.parse(userhandleBase64),
-                        displayName: "John",
-                    },
-                    challenge: asciiToUint8Array("123456"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    excludeCredentials: [
-                        { type: "public-key", id: credentialID, transports: ["usb"] },
-                        { type: "public-key", id: credentialID, transports: ["nfc"] },
-                        { type: "public-key", id: credentialID, transports: ["ble"] }
-                    ]
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "localhost",
+                },
+                user: {
+                    name: userhandleBase64,
+                    id: Base64URL.parse(userhandleBase64),
+                    displayName: "Appleseed",
+                },
+                challenge: Base64URL.parse("MTIzNDU2"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                authenticatorSelection: { authenticatorAttachment: "platform" }
+            }
+        };
+
+        return navigator.credentials.create(options).then(credential => {
+            checkResult(credential, credentialID);
+        });
+    }, "PublicKeyCredential's [[create]] with authenticatorSelection { 'platform' } in a mock local authenticator.");
+
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const userhandleBase64 = generateUserhandleBase64();
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({
+                local: {
+                    acceptAuthentication: true,
+                    acceptAttestation: false,
+                    privateKeyBase64: privateKeyBase64,
                 }
-            };
+            });
+
+        const anotherPrivateKeyBase64 = await generatePrivateKeyBase64();
+        const anotherCredentialID = await calculateCredentialID(anotherPrivateKeyBase64);
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: userhandleBase64,
+                    id: Base64URL.parse(userhandleBase64),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [
+                    { type: "public-key", id: anotherCredentialID, transports: ["usb"] },
+                    { type: "public-key", id: anotherCredentialID, transports: ["nfc"] },
+                    { type: "public-key", id: anotherCredentialID, transports: ["ble"] }
+                ]
+            }
+        };
+        if (window.testRunner)
+            testRunner.addTestKeyToKeychain(anotherPrivateKeyBase64, testRpId, testUserEntityBundleBase64);
+
+        return navigator.credentials.create(options).then(credential => {
+            checkResult(credential, credentialID);
             if (window.testRunner)
-                testRunner.addTestKeyToKeychain(testES256PrivateKeyBase64, testRpId, userhandleBase64);
+                testRunner.cleanUpKeychain(testRpId, base64encode(anotherCredentialID));
+        });
+    }, "PublicKeyCredential's [[create]] with matched exclude credential ids but not transports in a mock local authenticator.");
 
-            return navigator.credentials.create(options).then(credential => {
-                checkResult(credential);
-            });
-        }, "PublicKeyCredential's [[create]] with matched exclude credential ids but not transports in a mock local authenticator.");
-
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "localhost",
-                    },
-                    user: {
-                        name: userhandleBase64,
-                        id: Base64URL.parse(userhandleBase64),
-                        displayName: "Appleseed",
-                    },
-                    challenge: Base64URL.parse("MTIzNDU2"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    attestation: "none"
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const userhandleBase64 = generateUserhandleBase64();
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({
+                local: {
+                    acceptAuthentication: true,
+                    acceptAttestation: false,
+                    privateKeyBase64: privateKeyBase64,
                 }
-            };
+            });
 
-            return navigator.credentials.create(options).then(credential => {
-                checkResult(credential);
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "localhost",
+                },
+                user: {
+                    name: userhandleBase64,
+                    id: Base64URL.parse(userhandleBase64),
+                    displayName: "Appleseed",
+                },
+                challenge: Base64URL.parse("MTIzNDU2"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                attestation: "none"
+            }
+        };
+
+        return navigator.credentials.create(options).then(credential => {
+            checkResult(credential, credentialID);
+        });
+    }, "PublicKeyCredential's [[create]] with none attestation in a mock local authenticator.");
+
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const userhandleBase64 = generateUserhandleBase64();
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({
+                local: {
+                    acceptAuthentication: true,
+                    acceptAttestation: true,
+                    privateKeyBase64: privateKeyBase64,
+                    userCertificateBase64: testAttestationCertificateBase64,
+                    intermediateCACertificateBase64: testAttestationIssuingCACertificateBase64
+                }
             });
-        }, "PublicKeyCredential's [[create]] with none attestation in a mock local authenticator.");
-
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "localhost",
-                    },
-                    user: {
-                        name: userhandleBase64,
-                        id: Base64URL.parse(userhandleBase64),
-                        displayName: "Appleseed",
-                    },
-                    challenge: Base64URL.parse("MTIzNDU2"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    attestation: "indirect"
+
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "localhost",
+                },
+                user: {
+                    name: userhandleBase64,
+                    id: Base64URL.parse(userhandleBase64),
+                    displayName: "Appleseed",
+                },
+                challenge: Base64URL.parse("MTIzNDU2"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                attestation: "indirect"
+            }
+        };
+
+        return navigator.credentials.create(options).then(credential => {
+            checkResult(credential, credentialID, false);
+        });
+    }, "PublicKeyCredential's [[create]] with indirect attestation in a mock local authenticator.");
+
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const userhandleBase64 = generateUserhandleBase64();
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({
+                local: {
+                    acceptAuthentication: true,
+                    acceptAttestation: true,
+                    privateKeyBase64: privateKeyBase64,
+                    userCertificateBase64: testAttestationCertificateBase64,
+                    intermediateCACertificateBase64: testAttestationIssuingCACertificateBase64
                 }
-            };
-
-            if (window.internals)
-                internals.setMockWebAuthenticationConfiguration({
-                    local: {
-                        acceptAuthentication: true,
-                        acceptAttestation: true,
-                        privateKeyBase64: privateKeyBase64,
-                        userCertificateBase64: testAttestationCertificateBase64,
-                        intermediateCACertificateBase64: testAttestationIssuingCACertificateBase64
-                    }
-                });
-
-            return navigator.credentials.create(options).then(credential => {
-                checkResult(credential, false);
             });
-        }, "PublicKeyCredential's [[create]] with indirect attestation in a mock local authenticator.");
-
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    rp: {
-                        name: "localhost",
-                    },
-                    user: {
-                        name: userhandleBase64,
-                        id: Base64URL.parse(userhandleBase64),
-                        displayName: "Appleseed",
-                    },
-                    challenge: Base64URL.parse("MTIzNDU2"),
-                    pubKeyCredParams: [{ type: "public-key", alg: -7 }],
-                    attestation: "direct"
+
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "localhost",
+                },
+                user: {
+                    name: userhandleBase64,
+                    id: Base64URL.parse(userhandleBase64),
+                    displayName: "Appleseed",
+                },
+                challenge: Base64URL.parse("MTIzNDU2"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                attestation: "direct"
+            }
+        };
+
+        return navigator.credentials.create(options).then(credential => {
+            checkResult(credential, credentialID, false);
+        });
+    }, "PublicKeyCredential's [[create]] with direct attestation in a mock local authenticator.");
+
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const userhandleBase64 = generateUserhandleBase64();
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({
+                local: {
+                    acceptAuthentication: true,
+                    acceptAttestation: false,
+                    privateKeyBase64: privateKeyBase64,
                 }
-            };
+            });
 
-            return navigator.credentials.create(options).then(credential => {
-                checkResult(credential, false);
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "localhost",
+                },
+                user: {
+                    name: userhandleBase64,
+                    id: Base64URL.parse(userhandleBase64),
+                    displayName: "Appleseed",
+                },
+                challenge: Base64URL.parse("MTIzNDU2"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+            }
+        };
+
+        const anotherPrivateKeyBase64 = await generatePrivateKeyBase64();
+        const anotherCredentialID = await calculateCredentialID(anotherPrivateKeyBase64);
+        if (window.internals)
+            testRunner.addTestKeyToKeychain(anotherPrivateKeyBase64, testRpId, base64encode(CBOR.encode({ "id": Base64URL.parse(userhandleBase64), "name": userhandleBase64 })));
+
+        return navigator.credentials.create(options).then(credential => {
+            checkResult(credential, credentialID);
+            assert_false(testRunner.keyExistsInKeychain(testRpId, base64encode(anotherCredentialID)));
+        });
+    }, "PublicKeyCredential's [[create]] with duplicate credential in a mock local authenticator.");
+
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const userhandleBase64 = generateUserhandleBase64();
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({
+                local: {
+                    acceptAuthentication: true,
+                    acceptAttestation: true,
+                    privateKeyBase64: privateKeyBase64,
+                    userCertificateBase64: testAttestationCertificateBase64,
+                    intermediateCACertificateBase64: testAttestationIssuingCACertificateBase64
+                }
             });
-        }, "PublicKeyCredential's [[create]] with direct attestation in a mock local authenticator.");
-    })();
+
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "localhost",
+                },
+                user: {
+                    name: userhandleBase64,
+                    id: Base64URL.parse(userhandleBase64),
+                    displayName: "Appleseed",
+                },
+                challenge: Base64URL.parse("MTIzNDU2"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                attestation: "direct"
+            }
+        };
+
+        const anotherPrivateKeyBase64 = await generatePrivateKeyBase64();
+        const anotherCredentialID = await calculateCredentialID(anotherPrivateKeyBase64);
+        if (window.internals)
+            testRunner.addTestKeyToKeychain(anotherPrivateKeyBase64, testRpId, base64encode(CBOR.encode({ "id": Base64URL.parse(userhandleBase64), "name": userhandleBase64 })));
+
+        return navigator.credentials.create(options).then(credential => {
+            checkResult(credential, credentialID, false);
+            assert_false(testRunner.keyExistsInKeychain(testRpId, base64encode(anotherCredentialID)));
+        });
+    }, "PublicKeyCredential's [[create]] with duplicate credential in a mock local authenticator. 2");
 </script>
index d7986c8..d3f4f5d 100644 (file)
@@ -1,5 +1,5 @@
 
-PASS PublicKeyCredential's [[get]] with silent failure in a mock local authenticator. 
-PASS PublicKeyCredential's [[get]] with silent failure in a mock local authenticator. 2 
-PASS PublicKeyCredential's [[get]] with silent failure in a mock local authenticator. 3 
+PASS PublicKeyCredential's [[get]] with no matched credentials in a mock local authenticator. 
+PASS PublicKeyCredential's [[get]] with no matched credentials in a mock local authenticator. 2nd 
+PASS PublicKeyCredential's [[get]] without user consent in a mock local authenticator. 
 
index f02c255..83eb6b6 100644 (file)
@@ -4,64 +4,72 @@
 <script src="/resources/testharnessreport.js"></script>
 <script src="./resources/util.js"></script>
 <script>
-    (async function() {
-        const userhandleBase64 = generateUserhandleBase64();
+    promise_test(t => {
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({ silentFailure: true, local: { acceptAuthentication: false, acceptAttestation: false } });
+
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                allowCredentials: [
+                    { type: "public-key", id: Base64URL.parse(testCredentialIdBase64), transports: ["usb"] },
+                    { type: "public-key", id: Base64URL.parse(testCredentialIdBase64), transports: ["nfc"] },
+                    { type: "public-key", id: Base64URL.parse(testCredentialIdBase64), transports: ["ble"] },
+                    { type: "public-key", id: Base64URL.parse(testCredentialIdBase64), transports: ["internal"] }
+                ],
+                timeout: 10
+            }
+        };
+
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[get]] with no matched credentials in a mock local authenticator.");
+
+    promise_test(async t => {
         const privateKeyBase64 = await generatePrivateKeyBase64();
         const credentialID = await calculateCredentialID(privateKeyBase64);
+        const credentialIDBase64 = btoa(String.fromCharCode.apply(0, credentialID));
         // Default mock configuration. Tests need to override if they need different configuration.
         if (window.internals)
-            internals.setMockWebAuthenticationConfiguration({ silentFailure: true, local: { acceptAuthentication: false, acceptAttestation: false, preferredUserhandleBase64: userhandleBase64 } });
+            internals.setMockWebAuthenticationConfiguration({ silentFailure: true, local: { acceptAuthentication: false, acceptAttestation: false, preferredCredentialIdBase64: credentialIDBase64 } });
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    challenge: asciiToUint8Array("123456"),
-                    allowCredentials: [
-                        { type: "public-key", id: credentialID, transports: ["usb"] },
-                        { type: "public-key", id: credentialID, transports: ["nfc"] },
-                        { type: "public-key", id: credentialID, transports: ["ble"] },
-                        { type: "public-key", id: credentialID, transports: ["internal"] }
-                    ],
-                    timeout: 10
-                }
-            };
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                allowCredentials: [
+                    { type: "public-key", id: Base64URL.parse(testUserhandleBase64) }
+                ],
+                timeout: 10
+            }
+        };
 
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.");
-        }, "PublicKeyCredential's [[get]] with silent failure in a mock local authenticator.");
-
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    challenge: asciiToUint8Array("123456"),
-                    allowCredentials: [
-                        { type: "public-key", id: Base64URL.parse(userhandleBase64) }
-                    ],
-                    timeout: 10
-                }
-            };
+        if (window.testRunner)
+            testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, testUserEntityBundleBase64);
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.").then(() => {
+                if (window.testRunner)
+                    testRunner.cleanUpKeychain(testRpId, credentialIDBase64);
+            });
+    }, "PublicKeyCredential's [[get]] with no matched credentials in a mock local authenticator. 2nd");
 
-            if (window.testRunner)
-                testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, userhandleBase64);
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.").then(() => {
-                    if (window.testRunner)
-                        testRunner.cleanUpKeychain(testRpId, userhandleBase64);
-                });
-        }, "PublicKeyCredential's [[get]] with silent failure in a mock local authenticator. 2");
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const credentialIDBase64 = btoa(String.fromCharCode.apply(0, credentialID));
+        // Default mock configuration. Tests need to override if they need different configuration.
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({ silentFailure: true, local: { acceptAuthentication: false, acceptAttestation: false, preferredCredentialIdBase64: credentialIDBase64 } });
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    challenge: asciiToUint8Array("123456"),
-                    timeout: 10
-                }
-            };
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                timeout: 10
+            }
+        };
 
+        if (window.testRunner)
+            testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, testUserEntityBundleBase64);
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.").then(() => {
             if (window.testRunner)
-                testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, userhandleBase64);
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.").then(() => {
-                if (window.testRunner)
-                    testRunner.cleanUpKeychain(testRpId, userhandleBase64);
-            });
-        }, "PublicKeyCredential's [[get]] with silent failure in a mock local authenticator. 3");
-    })();
+                testRunner.cleanUpKeychain(testRpId, credentialIDBase64);
+        });
+    }, "PublicKeyCredential's [[get]] without user consent in a mock local authenticator.");
 </script>
index f27f711..bdb6076 100644 (file)
@@ -4,77 +4,85 @@
 <script src="/resources/testharnessreport.js"></script>
 <script src="./resources/util.js"></script>
 <script>
-    (async function() {
-        const userhandleBase64 = generateUserhandleBase64();
+    promise_test(t => {
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: false, acceptAttestation: false } });
+
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                allowCredentials: [
+                    { type: "public-key", id: Base64URL.parse(testCredentialIdBase64), transports: ["usb"] },
+                    { type: "public-key", id: Base64URL.parse(testCredentialIdBase64), transports: ["nfc"] },
+                    { type: "public-key", id: Base64URL.parse(testCredentialIdBase64), transports: ["ble"] },
+                    { type: "public-key", id: Base64URL.parse(testCredentialIdBase64), transports: ["internal"] }
+                ]
+            }
+        };
+
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "No matched credentials are found in the platform attached authenticator.");
+    }, "PublicKeyCredential's [[get]] with no matched credentials in a mock local authenticator.");
+
+    promise_test(async t => {
         const privateKeyBase64 = await generatePrivateKeyBase64();
         const credentialID = await calculateCredentialID(privateKeyBase64);
+        const credentialIDBase64 = btoa(String.fromCharCode.apply(0, credentialID));
         // Default mock configuration. Tests need to override if they need different configuration.
         if (window.internals)
-            internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: false, acceptAttestation: false, preferredUserhandleBase64: userhandleBase64 } });
-
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    challenge: asciiToUint8Array("123456"),
-                    allowCredentials: [
-                        { type: "public-key", id: credentialID, transports: ["usb"] },
-                        { type: "public-key", id: credentialID, transports: ["nfc"] },
-                        { type: "public-key", id: credentialID, transports: ["ble"] },
-                        { type: "public-key", id: credentialID, transports: ["internal"] }
-                    ]
-                }
-            };
-
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "No matched credentials are found in the platform attached authenticator.");
-        }, "PublicKeyCredential's [[get]] with no matched credentials in a mock local authenticator.");
+            internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: false, acceptAttestation: false, preferredCredentialIdBase64: credentialIDBase64 } });
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    challenge: asciiToUint8Array("123456"),
-                    allowCredentials: [
-                        { type: "public-key", id: Base64URL.parse(userhandleBase64) }
-                    ]
-                }
-            };
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                allowCredentials: [
+                    { type: "public-key", id: Base64URL.parse(testUserhandleBase64) }
+                ]
+            }
+        };
 
+        if (window.testRunner)
+            testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, testUserEntityBundleBase64);
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "No matched credentials are found in the platform attached authenticator.").then(() => {
             if (window.testRunner)
-                testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, userhandleBase64);
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "No matched credentials are found in the platform attached authenticator.").then(() => {
-                if (window.testRunner)
-                    testRunner.cleanUpKeychain(testRpId, userhandleBase64);
-            });
-        }, "PublicKeyCredential's [[get]] with no matched credentials in a mock local authenticator. 2nd");
+                testRunner.cleanUpKeychain(testRpId, credentialIDBase64);
+        });
+    }, "PublicKeyCredential's [[get]] with no matched credentials in a mock local authenticator. 2nd");
+
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const credentialIDBase64 = btoa(String.fromCharCode.apply(0, credentialID));
+        // Default mock configuration. Tests need to override if they need different configuration.
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: false, acceptAttestation: false, preferredCredentialIdBase64: credentialIDBase64 } });
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    challenge: asciiToUint8Array("123456")
-                }
-            };
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456")
+            }
+        };
 
+        if (window.testRunner)
+            testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, testUserEntityBundleBase64);
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Couldn't verify user.").then(() => {
             if (window.testRunner)
-                testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, userhandleBase64);
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Couldn't verify user.").then(() => {
-                if (window.testRunner)
-                    testRunner.cleanUpKeychain(testRpId, userhandleBase64);
-            });
-        }, "PublicKeyCredential's [[get]] without user consent in a mock local authenticator.");
+                testRunner.cleanUpKeychain(testRpId, credentialIDBase64);
+        });
+    }, "PublicKeyCredential's [[get]] without user consent in a mock local authenticator.");
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    challenge: asciiToUint8Array("123456"),
-                    allowCredentials: [
-                        { type: "public-key", id: credentialID, transports: ["usb"] },
-                        { type: "public-key", id: credentialID, transports: ["nfc"] },
-                        { type: "public-key", id: credentialID, transports: ["ble"] }
-                    ],
-                    timeout: 10
-                }
-            };
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                allowCredentials: [
+                    { type: "public-key", id: Base64URL.parse(testCredentialIdBase64), transports: ["usb"] },
+                    { type: "public-key", id: Base64URL.parse(testCredentialIdBase64), transports: ["nfc"] },
+                    { type: "public-key", id: Base64URL.parse(testCredentialIdBase64), transports: ["ble"] }
+                ],
+                timeout: 10
+            }
+        };
 
-            return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.");
-        }, "PublicKeyCredential's [[get]] with timeout in a mock local authenticator.");
-    })();
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[get]] with timeout in a mock local authenticator.");
 </script>
index 9360afb..78d4d7b 100644 (file)
@@ -4,75 +4,80 @@
 <script src="/resources/testharnessreport.js"></script>
 <script src="./resources/util.js"></script>
 <script>
-    (async function() {
-        const userhandleBase64 = generateUserhandleBase64();
-        const privateKeyBase64 = await generatePrivateKeyBase64();
-        const credentialID = await calculateCredentialID(privateKeyBase64);
-        // Default mock configuration. Tests need to override if they need different configuration.
-        if (window.internals)
-            internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: true, acceptAttestation: false, preferredUserhandleBase64: userhandleBase64 } });
-
-        function checkResult(credential)
-        {
-            if (window.testRunner)
-                testRunner.cleanUpKeychain(testRpId, userhandleBase64);
+    function checkResult(credential, credentialID, privateKeyBase64)
+    {
+        if (window.testRunner)
+            testRunner.cleanUpKeychain(testRpId, base64encode(credentialID));
 
-             // Check respond
-            assert_array_equals(Base64URL.parse(credential.id), credentialID);
-            assert_equals(credential.type, 'public-key');
-            assert_array_equals(new Uint8Array(credential.rawId), credentialID);
-            assert_equals(bytesToASCIIString(credential.response.clientDataJSON), '{"type":"webauthn.get","challenge":"MTIzNDU2","origin":"https://localhost:9443"}');
-            assert_array_equals(new Uint8Array(credential.response.userHandle), Base64URL.parse(userhandleBase64));
-            assert_not_own_property(credential.getClientExtensionResults(), "appid");
+         // Check respond
+        assert_array_equals(Base64URL.parse(credential.id), credentialID);
+        assert_equals(credential.type, 'public-key');
+        assert_array_equals(new Uint8Array(credential.rawId), credentialID);
+        assert_equals(bytesToASCIIString(credential.response.clientDataJSON), '{"type":"webauthn.get","challenge":"MTIzNDU2","origin":"https://localhost:9443"}');
+        assert_array_equals(new Uint8Array(credential.response.userHandle), Base64URL.parse(testUserhandleBase64));
+        assert_not_own_property(credential.getClientExtensionResults(), "appid");
 
-            // Check authData
-            const authData = decodeAuthData(new Uint8Array(credential.response.authenticatorData));
-            assert_equals(bytesToHexString(authData.rpIdHash), "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763");
-            assert_equals(authData.flags, 5);
-            assert_equals(authData.counter, 0);
+        // Check authData
+        const authData = decodeAuthData(new Uint8Array(credential.response.authenticatorData));
+        assert_equals(bytesToHexString(authData.rpIdHash), "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763");
+        assert_equals(authData.flags, 5);
+        assert_equals(authData.counter, 0);
 
-            // Check signature
-            return crypto.subtle.importKey("raw", Base64URL.parse(privateKeyBase64).slice(0, 65), { name: "ECDSA", namedCurve: "P-256" }, false, ['verify']).then( publicKey => {
-                return crypto.subtle.digest("sha-256", credential.response.clientDataJSON).then ( hash => {
-                    // credential.response.signature is in ASN.1 and WebCrypto expect signatures provides in r|s.
-                    return crypto.subtle.verify({name: "ECDSA", hash: "SHA-256"}, publicKey, extractRawSignature(credential.response.signature), concatenateBuffers(credential.response.authenticatorData, hash)).then( verified => {
-                        assert_true(verified);
-                        assert_not_own_property(credential.getClientExtensionResults(), "appid");
-                    });
+        // Check signature
+        return crypto.subtle.importKey("raw", Base64URL.parse(privateKeyBase64).slice(0, 65), { name: "ECDSA", namedCurve: "P-256" }, false, ['verify']).then( publicKey => {
+            return crypto.subtle.digest("sha-256", credential.response.clientDataJSON).then ( hash => {
+                // credential.response.signature is in ASN.1 and WebCrypto expect signatures provides in r|s.
+                return crypto.subtle.verify({name: "ECDSA", hash: "SHA-256"}, publicKey, extractRawSignature(credential.response.signature), concatenateBuffers(credential.response.authenticatorData, hash)).then( verified => {
+                    assert_true(verified);
+                    assert_not_own_property(credential.getClientExtensionResults(), "appid");
                 });
             });
-        }
+        });
+    }
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    challenge: Base64URL.parse("MTIzNDU2")
-                }
-            };
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const credentialIDBase64 = base64encode(credentialID);
+        // Default mock configuration. Tests need to override if they need different configuration.
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: true, acceptAttestation: false, preferredCredentialIdBase64: credentialIDBase64 } });
 
-            if (window.testRunner)
-                testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, userhandleBase64);
-            return navigator.credentials.get(options).then(credential => {
-                return checkResult(credential);
-            });
-        }, "PublicKeyCredential's [[get]] with minimum options in a mock local authenticator.");
+        const options = {
+            publicKey: {
+                challenge: Base64URL.parse("MTIzNDU2")
+            }
+        };
 
-        promise_test(t => {
-            const options = {
-                publicKey: {
-                    challenge: Base64URL.parse("MTIzNDU2"),
-                    allowCredentials: [
-                        { type: "public-key", id: Base64URL.parse(userhandleBase64), transports: ["internal"] },
-                        { type: "public-key", id: credentialID, transports: ["internal"] }
-                    ]
-                }
-            };
+        if (window.testRunner)
+            testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, testUserEntityBundleBase64);
+        return navigator.credentials.get(options).then(credential => {
+            return checkResult(credential, credentialID, privateKeyBase64);
+        });
+    }, "PublicKeyCredential's [[get]] with minimum options in a mock local authenticator.");
 
-            if (window.testRunner)
-                testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, userhandleBase64);
-            return navigator.credentials.get(options).then(credential => {
-                return checkResult(credential);
-            });
-        }, "PublicKeyCredential's [[get]] with matched allow credentials in a mock local authenticator.");
-    })();
+    promise_test(async t => {
+        const privateKeyBase64 = await generatePrivateKeyBase64();
+        const credentialID = await calculateCredentialID(privateKeyBase64);
+        const credentialIDBase64 = base64encode(credentialID);
+        // Default mock configuration. Tests need to override if they need different configuration.
+        if (window.internals)
+            internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: true, acceptAttestation: false, preferredCredentialIdBase64: credentialIDBase64 } });
+
+        const options = {
+            publicKey: {
+                challenge: Base64URL.parse("MTIzNDU2"),
+                allowCredentials: [
+                    { type: "public-key", id: Base64URL.parse(testUserhandleBase64), transports: ["internal"] },
+                    { type: "public-key", id: credentialID, transports: ["internal"] }
+                ]
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.addTestKeyToKeychain(privateKeyBase64, testRpId, testUserEntityBundleBase64);
+        return navigator.credentials.get(options).then(credential => {
+            return checkResult(credential, credentialID, privateKeyBase64);
+        });
+    }, "PublicKeyCredential's [[get]] with matched allow credentials in a mock local authenticator.");
 </script>
index 61d8f72..b655e2b 100644 (file)
@@ -5,6 +5,7 @@ const testES256PrivateKeyBase64 =
     "RQ==";
 const testRpId = "localhost";
 const testUserhandleBase64 = "AAECAwQFBgcICQ==";
+const testUserEntityBundleBase64 = "omJpZEoAAQIDBAUGBwgJZG5hbWVwQUFFQ0F3UUZCZ2NJQ1E9PQ==";
 const testAttestationCertificateBase64 =
     "MIIB6jCCAZCgAwIBAgIGAWHAxcjvMAoGCCqGSM49BAMCMFMxJzAlBgNVBAMMHkJh" +
     "c2ljIEF0dGVzdGF0aW9uIFVzZXIgU3ViIENBMTETMBEGA1UECgwKQXBwbGUgSW5j" +
@@ -449,7 +450,7 @@ function checkU2fGetAssertionResult(credential, isAppID = false, appIDHash = "c2
 
 function generateUserhandleBase64()
 {
-    let buffer = new Uint8Array(16);
+    let buffer = new Uint8Array(8);
     crypto.getRandomValues(buffer);
     return btoa(String.fromCharCode.apply(0, buffer));
 }
@@ -486,3 +487,9 @@ async function calculateCredentialID(privateKeyBase64) {
     const publicKey = privateKey.slice(0, 65);
     return new Uint8Array(await crypto.subtle.digest("sha-1", publicKey));
 }
+
+function base64encode(binary)
+{
+    const unit8Array = new Uint8Array(binary);
+    return btoa(String.fromCharCode.apply(0, unit8Array));
+}
index 5552aa6..48d6dd5 100644 (file)
@@ -1,3 +1,29 @@
+2020-03-11  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthn] Formalize the Keychain schema
+        https://bugs.webkit.org/show_bug.cgi?id=183533
+        <rdar://problem/43347926>
+
+        Reviewed by Brent Fulgham.
+
+        Covered by new test contents within existing files.
+
+        * Modules/webauthn/AuthenticatorAssertionResponse.cpp:
+        (WebCore::AuthenticatorAssertionResponse::create):
+        (WebCore::AuthenticatorAssertionResponse::AuthenticatorAssertionResponse):
+        * Modules/webauthn/AuthenticatorAssertionResponse.h:
+        Modifies the constructors to accept userEntity.name.
+
+        * Modules/webauthn/cbor/CBORValue.h:
+        Adds a FIXME.
+
+        * testing/MockWebAuthenticationConfiguration.h:
+        (WebCore::MockWebAuthenticationConfiguration::LocalConfiguration::encode const):
+        (WebCore::MockWebAuthenticationConfiguration::LocalConfiguration::decode):
+        * testing/MockWebAuthenticationConfiguration.idl:
+        Modifies the test infra to use Credential ID as the unique identifier for a credential instead of
+        the original combination of RP ID and user handle.
+
 2020-03-11  Daniel Bates  <dabates@apple.com>
 
         REGRESSION (r257502): HitTestLocation::HitTestLocation(const FloatPoint&, const FloatQuad&) should set m_isRectBased to true
index 38893ee..1c0c041 100644 (file)
@@ -48,9 +48,9 @@ Ref<AuthenticatorAssertionResponse> AuthenticatorAssertionResponse::create(const
     return create(ArrayBuffer::create(rawId.data(), rawId.size()), ArrayBuffer::create(authenticatorData.data(), authenticatorData.size()), ArrayBuffer::create(signature.data(), signature.size()), WTFMove(userhandleBuffer), WTF::nullopt);
 }
 
-Ref<AuthenticatorAssertionResponse> AuthenticatorAssertionResponse::create(Ref<ArrayBuffer>&& rawId, Ref<ArrayBuffer>&& userHandle, SecAccessControlRef accessControl)
+Ref<AuthenticatorAssertionResponse> AuthenticatorAssertionResponse::create(Ref<ArrayBuffer>&& rawId, Ref<ArrayBuffer>&& userHandle, String&& name, SecAccessControlRef accessControl)
 {
-    return adoptRef(*new AuthenticatorAssertionResponse(WTFMove(rawId), WTFMove(userHandle), accessControl));
+    return adoptRef(*new AuthenticatorAssertionResponse(WTFMove(rawId), WTFMove(userHandle), WTFMove(name), accessControl));
 }
 
 void AuthenticatorAssertionResponse::setAuthenticatorData(Vector<uint8_t>&& authenticatorData)
@@ -66,9 +66,10 @@ AuthenticatorAssertionResponse::AuthenticatorAssertionResponse(Ref<ArrayBuffer>&
 {
 }
 
-AuthenticatorAssertionResponse::AuthenticatorAssertionResponse(Ref<ArrayBuffer>&& rawId, Ref<ArrayBuffer>&& userHandle, SecAccessControlRef accessControl)
+AuthenticatorAssertionResponse::AuthenticatorAssertionResponse(Ref<ArrayBuffer>&& rawId, Ref<ArrayBuffer>&& userHandle, String&& name, SecAccessControlRef accessControl)
     : AuthenticatorResponse(WTFMove(rawId))
     , m_userHandle(WTFMove(userHandle))
+    , m_name(WTFMove(name))
     , m_accessControl(accessControl)
 {
 }
index 2b309cf..5dcd037 100644 (file)
@@ -37,7 +37,7 @@ class AuthenticatorAssertionResponse : public AuthenticatorResponse {
 public:
     static Ref<AuthenticatorAssertionResponse> create(Ref<ArrayBuffer>&& rawId, Ref<ArrayBuffer>&& authenticatorData, Ref<ArrayBuffer>&& signature, RefPtr<ArrayBuffer>&& userHandle, Optional<AuthenticationExtensionsClientOutputs>&&);
     WEBCORE_EXPORT static Ref<AuthenticatorAssertionResponse> create(const Vector<uint8_t>& rawId, const Vector<uint8_t>& authenticatorData, const Vector<uint8_t>& signature,  const Vector<uint8_t>& userHandle);
-    WEBCORE_EXPORT static Ref<AuthenticatorAssertionResponse> create(Ref<ArrayBuffer>&& rawId, Ref<ArrayBuffer>&& userHandle, SecAccessControlRef);
+    WEBCORE_EXPORT static Ref<AuthenticatorAssertionResponse> create(Ref<ArrayBuffer>&& rawId, Ref<ArrayBuffer>&& userHandle, String&& name, SecAccessControlRef);
     virtual ~AuthenticatorAssertionResponse() = default;
 
     ArrayBuffer* authenticatorData() const { return m_authenticatorData.get(); }
@@ -56,7 +56,7 @@ public:
 
 private:
     AuthenticatorAssertionResponse(Ref<ArrayBuffer>&&, Ref<ArrayBuffer>&&, Ref<ArrayBuffer>&&, RefPtr<ArrayBuffer>&&);
-    AuthenticatorAssertionResponse(Ref<ArrayBuffer>&&, Ref<ArrayBuffer>&&, SecAccessControlRef);
+    AuthenticatorAssertionResponse(Ref<ArrayBuffer>&&, Ref<ArrayBuffer>&&, String&&, SecAccessControlRef);
 
     Type type() const final { return Type::Assertion; }
     AuthenticatorResponseData data() const final;
index 6a4c83a..5e5383f 100644 (file)
@@ -164,6 +164,7 @@ public:
     bool isSimple() const { return type() == Type::SimpleValue; }
     bool isBool() const { return isSimple() && (m_simpleValue == SimpleValue::TrueValue || m_simpleValue == SimpleValue::FalseValue); }
 
+    // FIXME(183535): Considering adding && getter for better performance.
     // These will all fatally assert if the type doesn't match.
     SimpleValue getSimpleValue() const;
     bool getBool() const;
index 629558f..79cf45f 100644 (file)
@@ -67,7 +67,7 @@ struct MockWebAuthenticationConfiguration {
         String privateKeyBase64;
         String userCertificateBase64;
         String intermediateCACertificateBase64;
-        String preferredUserhandleBase64;
+        String preferredCredentialIdBase64;
 
         template<class Encoder> void encode(Encoder&) const;
         template<class Decoder> static Optional<LocalConfiguration> decode(Decoder&);
@@ -112,7 +112,7 @@ struct MockWebAuthenticationConfiguration {
 template<class Encoder>
 void MockWebAuthenticationConfiguration::LocalConfiguration::encode(Encoder& encoder) const
 {
-    encoder << acceptAuthentication << acceptAttestation << privateKeyBase64 << userCertificateBase64 << intermediateCACertificateBase64 << preferredUserhandleBase64;
+    encoder << acceptAuthentication << acceptAttestation << privateKeyBase64 << userCertificateBase64 << intermediateCACertificateBase64 << preferredCredentialIdBase64;
 }
 
 template<class Decoder>
@@ -150,11 +150,11 @@ Optional<MockWebAuthenticationConfiguration::LocalConfiguration> MockWebAuthenti
         return WTF::nullopt;
     result.intermediateCACertificateBase64 = WTFMove(*intermediateCACertificateBase64);
 
-    Optional<String> preferredUserhandleBase64;
-    decoder >> preferredUserhandleBase64;
-    if (!preferredUserhandleBase64)
+    Optional<String> preferredCredentialIdBase64;
+    decoder >> preferredCredentialIdBase64;
+    if (!preferredCredentialIdBase64)
         return WTF::nullopt;
-    result.preferredUserhandleBase64 = WTFMove(*preferredUserhandleBase64);
+    result.preferredCredentialIdBase64 = WTFMove(*preferredCredentialIdBase64);
 
     return result;
 }
index ad13934..2099431 100644 (file)
@@ -76,7 +76,7 @@
     DOMString privateKeyBase64;
     DOMString userCertificateBase64;
     DOMString intermediateCACertificateBase64;
-    DOMString preferredUserhandleBase64;
+    DOMString preferredCredentialIdBase64;
 };
 
 [
index f393a2d..885a293 100644 (file)
@@ -1,3 +1,77 @@
+2020-03-11  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthn] Formalize the Keychain schema
+        https://bugs.webkit.org/show_bug.cgi?id=183533
+        <rdar://problem/43347926>
+
+        Reviewed by Brent Fulgham.
+
+        This patch formalizes the schema for the Keychain as follows:
+        kSecAttrLabel: RP ID
+        kSecAttrApplicationLabel: Credential ID (auto-gen by Keychain)
+        kSecAttrApplicationTag: { "id": UserEntity.id, "name": UserEntity.name } (CBOR encoded)
+        Noted, the vale of kSecAttrApplicationLabel is automatically generated by the Keychain, which is a SHA-1 hash of
+        the public key.
+
+        According to the Step 7. from https://www.w3.org/TR/webauthn/#op-make-cred, the following fields are mandatory
+        1. rpId (rpEntity.id);
+        2. userHandle (userEntity.id), this is required for authenticators that support resident keys;
+        3. credentialId.
+
+        Some other optional fields are:
+        (from https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity)
+        1. rpEntity.name;
+        2. rpEnitty.icon;
+        (from https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity)
+        3. userEntity.displayName;
+        4. userEntity.name;
+        5. userEntity.icon;
+        (from https://www.w3.org/TR/webauthn/#sign-counter)
+        6. signature counter.
+
+        Among the six possible fields, only 4. is chosen to store. Here is why:
+        For rpEntity, rpEntity.id which is either the domain or the eTLD + 1 of the website is
+        sufficient enough to either classify the credential or serving the UI. Also, this is the only
+        trustworthy information that the UserAgent produce. Others could potentially be used by
+        malicious websites for attacking the Keychain or spoofing/phishing users when being displayed
+        in the UI. Also, rpEnitty.icon is a URL to the website's favicon, which if not implemented
+        correctly can be used for tracking.
+
+        For userEntity, userEntity.name is the human readable version of userEntity.id, and therefore
+        is chosen to store such that later on WebKit can pass it to UI client to help users disambiguate
+        different credentials. And it is necessary as userEntity.id is not guaranteed to be human
+        readable. Others are abandoned for the very same reason as above.
+
+        We hard code a zero value for 'signature counter'. While this is a theoretically interesting
+        technique for a RP to detect private key cloning, it is unlikely to be useful in practice.
+        We store the private keys in our SEP. This counter would only be a meaningful protection if
+        adversaries were able to extract private key data from the SEP without Apple noticing, but
+        were not able to manipulate this counter to fool the RP.
+
+        In terms of the schema,
+        1) RP ID is needed to query all credentials related, and therefore it needs a column and kSecAttrLabel
+        is supposed to be human readable;
+        2) kSecAttrApplicationLabel is the auto generated programmatical identifier for a SecItem, and
+        therefore is suitable as the credential ID. Given the input to the SHA-1 is generated by us, and
+        it is only needed to be powerful enough to be unique across the keychain within a device, and potentially
+        to be unique across different other credential ID for the same user. The SHA-1 collision attack
+        doesn't seem valid here.
+        3) kSecAttrApplicationTag is the only other column Keychain allows applications to modify. Therefore,
+        UserEntity.id and UserEntity.name is bundled to use this slot. The reason to use CBOR here is that
+        it is more friendly then JSON to encode binaries, and it is used widely in WebAuthn.
+
+        * UIProcess/WebAuthentication/Cocoa/LocalAuthenticator.h:
+        * UIProcess/WebAuthentication/Cocoa/LocalAuthenticator.mm:
+        (WebKit::LocalAuthenticatorInternal::toArrayBuffer):
+        (WebKit::LocalAuthenticatorInternal::getExistingCredentials):
+        (WebKit::LocalAuthenticator::makeCredential):
+        (WebKit::LocalAuthenticator::continueMakeCredentialAfterUserVerification):
+        (WebKit::LocalAuthenticator::continueMakeCredentialAfterAttested):
+        (WebKit::LocalAuthenticator::getAssertion):
+        (WebKit::LocalAuthenticator::deleteDuplicateCredential const):
+        * UIProcess/WebAuthentication/Mock/MockLocalConnection.mm:
+        (WebKit::MockLocalConnection::filterResponses const):
+
 2020-03-11  Per Arne Vollan  <pvollan@apple.com>
 
         [macOS] Crash under WebKit::WebProcessPool::platformInitialize()
index 28aeec8..3518c9a 100644 (file)
@@ -67,10 +67,13 @@ private:
     void continueGetAssertionAfterUserVerification(Ref<WebCore::AuthenticatorAssertionResponse>&&, LocalConnection::UserVerification, LAContext *);
 
     void receiveException(WebCore::ExceptionData&&, WebAuthenticationStatus = WebAuthenticationStatus::LAError) const;
+    void deleteDuplicateCredential() const;
 
     State m_state { State::Init };
     UniqueRef<LocalConnection> m_connection;
+    // FIXME(183534): Combine these two.
     HashSet<Ref<WebCore::AuthenticatorAssertionResponse>> m_assertionResponses;
+    Vector<Ref<WebCore::AuthenticatorAssertionResponse>> m_existingCredentials;
 };
 
 } // namespace WebKit
index 477d3d3..178c112 100644 (file)
@@ -31,6 +31,7 @@
 #import <Security/SecItem.h>
 #import <WebCore/AuthenticatorAssertionResponse.h>
 #import <WebCore/AuthenticatorAttestationResponse.h>
+#import <WebCore/CBORReader.h>
 #import <WebCore/CBORWriter.h>
 #import <WebCore/ExceptionData.h>
 #import <WebCore/PublicKeyCredentialCreationOptions.h>
@@ -47,6 +48,7 @@
 
 namespace WebKit {
 using namespace WebCore;
+using CBOR = cbor::CBORValue;
 
 namespace LocalAuthenticatorInternal {
 
@@ -55,12 +57,16 @@ const uint8_t makeCredentialFlags = 0b01000101; // UP, UV and AT are set.
 const uint8_t getAssertionFlags = 0b00000101; // UP and UV are set.
 // Credential ID is currently SHA-1 of the corresponding public key.
 const uint16_t credentialIdLength = 20;
+const char* const userEntityIdKey = "id";
+const char* const userEntityNameKey = "name";
+const uint64_t counter = 0;
 
 static inline bool emptyTransportsOrContain(const Vector<AuthenticatorTransport>& transports, AuthenticatorTransport target)
 {
     return transports.isEmpty() ? true : transports.contains(target);
 }
 
+// FIXME(183534): Find a better way of comparing credential id. Doing it with array seems fine given the list should be small.
 static inline HashSet<String> produceHashSet(const Vector<PublicKeyCredentialDescriptor>& credentialDescriptors)
 {
     HashSet<String> result;
@@ -98,6 +104,11 @@ static inline Ref<ArrayBuffer> toArrayBuffer(NSData *data)
     return ArrayBuffer::create(reinterpret_cast<const uint8_t*>(data.bytes), data.length);
 }
 
+static inline Ref<ArrayBuffer> toArrayBuffer(const Vector<uint8_t>& data)
+{
+    return ArrayBuffer::create(data.data(), data.size());
+}
+
 // FIXME(<rdar://problem/60108131>): Remove this whitelist once testing is complete.
 static const HashSet<String>& whitelistedRpId()
 {
@@ -109,6 +120,57 @@ static const HashSet<String>& whitelistedRpId()
     return whitelistedRpId;
 }
 
+static Optional<Vector<Ref<AuthenticatorAssertionResponse>>> getExistingCredentials(const String& rpId)
+{
+    // Search Keychain for existing credential matched the RP ID.
+    NSDictionary *query = @{
+        (id)kSecClass: (id)kSecClassKey,
+        (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
+        (id)kSecAttrLabel: rpId,
+        (id)kSecReturnAttributes: @YES,
+        (id)kSecMatchLimit: (id)kSecMatchLimitAll,
+#if HAVE(DATA_PROTECTION_KEYCHAIN)
+        (id)kSecUseDataProtectionKeychain: @YES
+#else
+        (id)kSecAttrNoLegacy: @YES
+#endif
+    };
+    CFTypeRef attributesArrayRef = nullptr;
+    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &attributesArrayRef);
+    if (status && status != errSecItemNotFound)
+        return WTF::nullopt;
+    auto retainAttributesArray = adoptCF(attributesArrayRef);
+    NSArray *nsAttributesArray = (NSArray *)attributesArrayRef;
+
+    Vector<Ref<AuthenticatorAssertionResponse>> result;
+    result.reserveInitialCapacity(nsAttributesArray.count);
+    for (NSDictionary *attributes in nsAttributesArray) {
+        auto decodedResponse = cbor::CBORReader::read(toVector(attributes[(id)kSecAttrApplicationTag]));
+        if (!decodedResponse || !decodedResponse->isMap()) {
+            ASSERT_NOT_REACHED();
+            return WTF::nullopt;
+        }
+        auto& responseMap = decodedResponse->getMap();
+
+        auto it = responseMap.find(CBOR(userEntityIdKey));
+        if (it == responseMap.end() || !it->second.isByteString()) {
+            ASSERT_NOT_REACHED();
+            return WTF::nullopt;
+        }
+        auto& userHandle = it->second.getByteString();
+
+        it = responseMap.find(CBOR(userEntityNameKey));
+        if (it == responseMap.end() || !it->second.isString()) {
+            ASSERT_NOT_REACHED();
+            return WTF::nullopt;
+        }
+        auto& username = it->second.getString();
+
+        result.uncheckedAppend(AuthenticatorAssertionResponse::create(toArrayBuffer(attributes[(id)kSecAttrApplicationLabel]), toArrayBuffer(userHandle), String(username), (__bridge SecAccessControlRef)attributes[(id)kSecAttrAccessControl]));
+    }
+    return result;
+}
+
 } // LocalAuthenticatorInternal
 
 LocalAuthenticator::LocalAuthenticator(UniqueRef<LocalConnection>&& connection)
@@ -127,6 +189,7 @@ void LocalAuthenticator::makeCredential()
     // Skip Step 4-5 as requireResidentKey and requireUserVerification are enforced.
     // Skip Step 9 as extensions are not supported yet.
     // Step 8 is implicitly captured by all UnknownError exception receiveResponds.
+    // Skip Step 10 as counter is constantly 0.
     // Step 2.
     if (notFound == creationOptions.pubKeyCredParams.findMatching([] (auto& pubKeyCredParam) {
         return pubKeyCredParam.type == PublicKeyCredentialType::PublicKey && pubKeyCredParam.alg == COSE::ES256;
@@ -136,37 +199,23 @@ void LocalAuthenticator::makeCredential()
     }
 
     // Step 3.
+    auto existingCredentials = getExistingCredentials(creationOptions.rp.id);
+    if (!existingCredentials) {
+        receiveException({ UnknownError, makeString("Couldn't get existing credentials") });
+        return;
+    }
+    m_existingCredentials = WTFMove(*existingCredentials);
+
     auto excludeCredentialIds = produceHashSet(creationOptions.excludeCredentials);
     if (!excludeCredentialIds.isEmpty()) {
-        // Search Keychain for the RP ID.
-        NSDictionary *query = @{
-            (id)kSecClass: (id)kSecClassKey,
-            (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
-            (id)kSecAttrLabel: creationOptions.rp.id,
-            (id)kSecReturnAttributes: @YES,
-            (id)kSecMatchLimit: (id)kSecMatchLimitAll,
-#if HAVE(DATA_PROTECTION_KEYCHAIN)
-            (id)kSecUseDataProtectionKeychain: @YES
-#else
-            (id)kSecAttrNoLegacy: @YES
-#endif
-        };
-        CFTypeRef attributesArrayRef = nullptr;
-        OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &attributesArrayRef);
-        if (status && status != errSecItemNotFound) {
-            receiveException({ UnknownError, makeString("Couldn't query Keychain: ", status) });
+        if (notFound != m_existingCredentials.findMatching([&excludeCredentialIds] (auto& credential) {
+            auto* rawId = credential->rawId();
+            ASSERT(rawId);
+            return excludeCredentialIds.contains(String(reinterpret_cast<const char*>(rawId->data()), rawId->byteLength()));
+        })) {
+            receiveException({ NotAllowedError, "At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator."_s }, WebAuthenticationStatus::LAExcludeCredentialsMatched);
             return;
         }
-        auto retainAttributesArray = adoptCF(attributesArrayRef);
-
-        // FIXME: Need to obtain user consent and then return different error according to the result.
-        for (NSDictionary *nsAttributes in (NSArray *)attributesArrayRef) {
-            NSData *nsCredentialId = nsAttributes[(id)kSecAttrApplicationLabel];
-            if (excludeCredentialIds.contains(String(reinterpret_cast<const char*>(nsCredentialId.bytes), nsCredentialId.length))) {
-                receiveException({ NotAllowedError, "At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator."_s }, WebAuthenticationStatus::LAExcludeCredentialsMatched);
-                return;
-            }
-        }
     }
 
     // Step 6.
@@ -228,35 +277,22 @@ void LocalAuthenticator::continueMakeCredentialAfterUserVerification(SecAccessCo
         return;
     }
 
-    // FIXME(183533): A single kSecClassKey item couldn't store all meta data. The following schema is a tentative solution
-    // to accommodate the most important meta data, i.e. RP ID, Credential ID, and userhandle.
+    // Here is the keychain schema.
     // kSecAttrLabel: RP ID
     // kSecAttrApplicationLabel: Credential ID (auto-gen by Keychain)
-    // kSecAttrApplicationTag: userhandle
+    // kSecAttrApplicationTag: { "id": UserEntity.id, "name": UserEntity.name } (CBOR encoded)
     // Noted, the vale of kSecAttrApplicationLabel is automatically generated by the Keychain, which is a SHA-1 hash of
-    // the public key. We borrow it directly for now to workaround the stated limitations.
+    // the public key.
     const auto& secAttrLabel = creationOptions.rp.id;
-    auto secAttrApplicationTag = toNSData(creationOptions.user.idVector);
 
-    // Step 7.5.
-    // Failures after this point could block users' accounts forever. Should we follow the spec?
-    NSDictionary* deleteQuery = @{
-        (id)kSecClass: (id)kSecClassKey,
-        (id)kSecAttrLabel: secAttrLabel,
-        (id)kSecAttrApplicationTag: secAttrApplicationTag.get(),
-#if HAVE(DATA_PROTECTION_KEYCHAIN)
-        (id)kSecUseDataProtectionKeychain: @YES
-#else
-        (id)kSecAttrNoLegacy: @YES
-#endif
-    };
-    OSStatus status = SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
-    if (status && status != errSecItemNotFound) {
-        receiveException({ UnknownError, makeString("Couldn't delete older credential: ", status) });
-        return;
-    }
+    cbor::CBORValue::MapValue userEntityMap;
+    userEntityMap[cbor::CBORValue(userEntityIdKey)] = cbor::CBORValue(creationOptions.user.idVector);
+    userEntityMap[cbor::CBORValue(userEntityNameKey)] = cbor::CBORValue(creationOptions.user.name);
+    auto userEntity = cbor::CBORWriter::write(cbor::CBORValue(WTFMove(userEntityMap)));
+    ASSERT(userEntity);
+    auto secAttrApplicationTag = toNSData(*userEntity);
 
-    // Step 7.1-7.4.
+    // Step 7.
     // The above-to-create private key will be inserted into keychain while using SEP.
     auto privateKey = m_connection->createCredentialPrivateKey(context, accessControlRef, secAttrLabel, secAttrApplicationTag.get());
     if (!privateKey) {
@@ -264,58 +300,54 @@ void LocalAuthenticator::continueMakeCredentialAfterUserVerification(SecAccessCo
         return;
     }
 
+    RetainPtr<CFDataRef> publicKeyDataRef;
+    {
+        auto publicKey = adoptCF(SecKeyCopyPublicKey(privateKey.get()));
+        CFErrorRef errorRef = nullptr;
+        publicKeyDataRef = adoptCF(SecKeyCopyExternalRepresentation(publicKey.get(), &errorRef));
+        auto retainError = adoptCF(errorRef);
+        if (errorRef) {
+            receiveException({ UnknownError, makeString("Couldn't export the public key: ", String(((NSError*)errorRef).localizedDescription)) });
+            return;
+        }
+        ASSERT(((NSData *)publicKeyDataRef.get()).length == (1 + 2 * ES256FieldElementLength)); // 04 | X | Y
+    }
+    NSData *nsPublicKeyData = (NSData *)publicKeyDataRef.get();
+
+    // Query credentialId in the keychain could be racy as it is the only unique identifier
+    // of the key item. Instead we calculate that, and examine its equaity in DEBUG build.
     Vector<uint8_t> credentialId;
     {
+        auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_1);
+        digest->addBytes(nsPublicKeyData.bytes, nsPublicKeyData.length);
+        credentialId = digest->computeHash();
+
+#ifndef NDEBUG
         NSDictionary *credentialIdQuery = @{
             (id)kSecClass: (id)kSecClassKey,
             (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
             (id)kSecAttrLabel: secAttrLabel,
-            (id)kSecAttrApplicationTag: secAttrApplicationTag.get(),
-            (id)kSecReturnAttributes: @YES,
+            (id)kSecAttrApplicationLabel: toNSData(credentialId).get(),
 #if HAVE(DATA_PROTECTION_KEYCHAIN)
             (id)kSecUseDataProtectionKeychain: @YES
 #else
             (id)kSecAttrNoLegacy: @YES
 #endif
         };
-        CFTypeRef attributesRef = nullptr;
-        OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)credentialIdQuery, &attributesRef);
-        if (status) {
-            receiveException({ UnknownError, makeString("Couldn't get Credential ID: ", status) });
-            return;
-        }
-        auto retainAttributes = adoptCF(attributesRef);
-
-        NSDictionary *nsAttributes = (NSDictionary *)attributesRef;
-        credentialId = toVector(nsAttributes[(id)kSecAttrApplicationLabel]);
+        OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)credentialIdQuery, nullptr);
+        ASSERT(!status);
+#endif // NDEBUG
     }
 
-    // Step 10.
-    // FIXME(183533): store the counter.
-    uint32_t counter = 0;
-
     // Step 11. https://www.w3.org/TR/webauthn/#attested-credential-data
     // credentialPublicKey
     Vector<uint8_t> cosePublicKey;
     {
-        RetainPtr<CFDataRef> publicKeyDataRef;
-        {
-            auto publicKey = adoptCF(SecKeyCopyPublicKey(privateKey.get()));
-            CFErrorRef errorRef = nullptr;
-            publicKeyDataRef = adoptCF(SecKeyCopyExternalRepresentation(publicKey.get(), &errorRef));
-            auto retainError = adoptCF(errorRef);
-            if (errorRef) {
-                receiveException({ UnknownError, makeString("Couldn't export the public key: ", String(((NSError*)errorRef).localizedDescription)) });
-                return;
-            }
-            ASSERT(((NSData *)publicKeyDataRef.get()).length == (1 + 2 * ES256FieldElementLength)); // 04 | X | Y
-        }
-
         // COSE Encoding
         Vector<uint8_t> x(ES256FieldElementLength);
-        [(NSData *)publicKeyDataRef.get() getBytes: x.data() range:NSMakeRange(1, ES256FieldElementLength)];
+        [nsPublicKeyData getBytes: x.data() range:NSMakeRange(1, ES256FieldElementLength)];
         Vector<uint8_t> y(ES256FieldElementLength);
-        [(NSData *)publicKeyDataRef.get() getBytes: y.data() range:NSMakeRange(1 + ES256FieldElementLength, ES256FieldElementLength)];
+        [nsPublicKeyData getBytes: y.data() range:NSMakeRange(1 + ES256FieldElementLength, ES256FieldElementLength)];
         cosePublicKey = encodeES256PublicKeyAsCBOR(WTFMove(x), WTFMove(y));
     }
     // FIXME(rdar://problem/38320512): Define Apple AAGUID.
@@ -326,6 +358,8 @@ void LocalAuthenticator::continueMakeCredentialAfterUserVerification(SecAccessCo
 
     // Skip Apple Attestation for none attestation, and non whitelisted RP ID for now.
     if (creationOptions.attestation == AttestationConveyancePreference::None || !whitelistedRpId().contains(creationOptions.rp.id)) {
+        deleteDuplicateCredential();
+
         auto attestationObject = buildAttestationObject(WTFMove(authData), "", { }, AttestationConveyancePreference::None);
         receiveRespond(AuthenticatorAttestationResponse::create(credentialId, attestationObject));
         return;
@@ -370,6 +404,7 @@ void LocalAuthenticator::continueMakeCredentialAfterAttested(Vector<uint8_t>&& c
     }
     auto attestationObject = buildAttestationObject(WTFMove(authData), "apple", WTFMove(attestationStatementMap), creationOptions.attestation);
 
+    deleteDuplicateCredential();
     receiveRespond(AuthenticatorAttestationResponse::create(credentialId, attestationObject));
 }
 
@@ -383,6 +418,7 @@ void LocalAuthenticator::getAssertion()
     // The following implements https://www.w3.org/TR/webauthn/#op-get-assertion as of 5 December 2017.
     // Skip Step 2 as requireUserVerification is enforced.
     // Skip Step 8 as extensions are not supported yet.
+    // Skip Step 9 as counter is constantly 0.
     // Step 12 is implicitly captured by all UnknownError exception callbacks.
     // Step 3-5. Unlike the spec, if an allow list is provided and there is no intersection between existing ones and the allow list, we always return NotAllowedError.
     auto allowCredentialIds = produceHashSet(requestOptions.allowCredentials);
@@ -392,51 +428,32 @@ void LocalAuthenticator::getAssertion()
     }
 
     // Search Keychain for the RP ID.
-    NSDictionary *query = @{
-        (id)kSecClass: (id)kSecClassKey,
-        (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
-        (id)kSecAttrLabel: requestOptions.rpId,
-        (id)kSecReturnAttributes: @YES,
-        (id)kSecMatchLimit: (id)kSecMatchLimitAll,
-#if HAVE(DATA_PROTECTION_KEYCHAIN)
-        (id)kSecUseDataProtectionKeychain: @YES
-#else
-        (id)kSecAttrNoLegacy: @YES
-#endif
-    };
-    CFTypeRef attributesArrayRef = nullptr;
-    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &attributesArrayRef);
-    if (status && status != errSecItemNotFound) {
-        receiveException({ UnknownError, makeString("Couldn't query Keychain: ", status) });
+    auto existingCredentials = getExistingCredentials(requestOptions.rpId);
+    if (!existingCredentials) {
+        receiveException({ UnknownError, makeString("Couldn't get existing credentials") });
         return;
     }
-    auto retainAttributesArray = adoptCF(attributesArrayRef);
+    m_existingCredentials = WTFMove(*existingCredentials);
 
-    NSArray *intersectedCredentialsAttributes = nil;
-    if (requestOptions.allowCredentials.isEmpty())
-        intersectedCredentialsAttributes = (NSArray *)attributesArrayRef;
-    else {
-        NSMutableArray *result = [NSMutableArray arrayWithCapacity:allowCredentialIds.size()];
-        for (NSDictionary *nsAttributes in (NSArray *)attributesArrayRef) {
-            NSData *nsCredentialId = nsAttributes[(id)kSecAttrApplicationLabel];
-            if (allowCredentialIds.contains(String(reinterpret_cast<const char*>(nsCredentialId.bytes), nsCredentialId.length)))
-                [result addObject:nsAttributes];
+    for (auto& credential : m_existingCredentials) {
+        if (allowCredentialIds.isEmpty()) {
+            auto addResult = m_assertionResponses.add(credential.copyRef());
+            ASSERT_UNUSED(addResult, addResult.isNewEntry);
+            continue;
+        }
+
+        auto* rawId = credential->rawId();
+        if (allowCredentialIds.contains(String(reinterpret_cast<const char*>(rawId->data()), rawId->byteLength()))) {
+            auto addResult = m_assertionResponses.add(credential.copyRef());
+            ASSERT_UNUSED(addResult, addResult.isNewEntry);
         }
-        intersectedCredentialsAttributes = result;
     }
-    if (!intersectedCredentialsAttributes.count) {
+    if (m_assertionResponses.isEmpty()) {
         receiveException({ NotAllowedError, "No matched credentials are found in the platform attached authenticator."_s }, WebAuthenticationStatus::LANoCredential);
         return;
     }
 
     // Step 6-7. User consent is implicitly acquired by selecting responses.
-    for (NSDictionary *attribute : intersectedCredentialsAttributes) {
-        auto addResult = m_assertionResponses.add(AuthenticatorAssertionResponse::create(
-            toArrayBuffer(attribute[(id)kSecAttrApplicationLabel]),
-            toArrayBuffer(attribute[(id)kSecAttrApplicationTag]),
-            (__bridge SecAccessControlRef)attribute[(id)kSecAttrAccessControl]));
-        ASSERT_UNUSED(addResult, addResult.isNewEntry);
-    }
     m_connection->filterResponses(m_assertionResponses);
 
     if (auto* observer = this->observer()) {
@@ -484,10 +501,7 @@ void LocalAuthenticator::continueGetAssertionAfterUserVerification(Ref<WebCore::
         return;
     }
 
-    // Step 9-10.
-    // FIXME(183533): Due to the stated Keychain limitations, we can't save the counter value.
-    // Therefore, it is always zero.
-    uint32_t counter = 0;
+    // Step 10.
     auto authData = buildAuthData(WTF::get<PublicKeyCredentialRequestOptions>(requestData().options).rpId, getAssertionFlags, counter, { });
 
     // Step 11.
@@ -541,6 +555,33 @@ void LocalAuthenticator::receiveException(ExceptionData&& exception, WebAuthenti
     return;
 }
 
+void LocalAuthenticator::deleteDuplicateCredential() const
+{
+    using namespace LocalAuthenticatorInternal;
+
+    auto& creationOptions = WTF::get<PublicKeyCredentialCreationOptions>(requestData().options);
+    m_existingCredentials.findMatching([creationOptions] (auto& credential) {
+        auto* userHandle = credential->userHandle();
+        ASSERT(userHandle);
+        if (memcmp(userHandle->data(), creationOptions.user.idVector.data(), userHandle->byteLength()))
+            return false;
+
+        NSDictionary* deleteQuery = @{
+            (id)kSecClass: (id)kSecClassKey,
+            (id)kSecAttrApplicationLabel: toNSData(credential->rawId()).get(),
+#if HAVE(DATA_PROTECTION_KEYCHAIN)
+            (id)kSecUseDataProtectionKeychain: @YES
+#else
+            (id)kSecAttrNoLegacy: @YES
+#endif
+        };
+        OSStatus status = SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
+        if (status && status != errSecItemNotFound)
+            LOG_ERROR(makeString("Couldn't delete older credential: "_s, status).utf8().data());
+        return true;
+    });
+}
+
 } // namespace WebKit
 
 #endif // ENABLE(WEB_AUTHN)
index b1c9874..b5cad07 100644 (file)
@@ -115,16 +115,16 @@ void MockLocalConnection::getAttestation(SecKeyRef, NSData *, NSData *, Attestat
 
 void MockLocalConnection::filterResponses(HashSet<Ref<WebCore::AuthenticatorAssertionResponse>>& responses) const
 {
-    const auto& preferredUserhandleBase64 = m_configuration.local->preferredUserhandleBase64;
-    if (preferredUserhandleBase64.isEmpty())
+    const auto& preferredCredentialIdBase64 = m_configuration.local->preferredCredentialIdBase64;
+    if (preferredCredentialIdBase64.isEmpty())
         return;
 
     auto itr = responses.begin();
     for (; itr != responses.end(); ++itr) {
-        auto* userHandle = itr->get().userHandle();
-        ASSERT(userHandle);
-        auto userhandleBase64 = base64Encode(userHandle->data(), userHandle->byteLength());
-        if (userhandleBase64 == preferredUserhandleBase64)
+        auto* rawId = itr->get().rawId();
+        ASSERT(rawId);
+        auto rawIdBase64 = base64Encode(rawId->data(), rawId->byteLength());
+        if (rawIdBase64 == preferredCredentialIdBase64)
             break;
     }
     auto response = responses.take(itr);
index ecd77b0..cad5b06 100644 (file)
@@ -1,3 +1,26 @@
+2020-03-11  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthn] Formalize the Keychain schema
+        https://bugs.webkit.org/show_bug.cgi?id=183533
+        <rdar://problem/43347926>
+
+        Reviewed by Brent Fulgham.
+
+        Modifies the test infra to use Credential ID as the unique identifier for a credential instead of
+        the original combination of RP ID and user handle.
+
+        * WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl:
+        * WebKitTestRunner/InjectedBundle/TestRunner.cpp:
+        (WTR::TestRunner::cleanUpKeychain):
+        (WTR::TestRunner::keyExistsInKeychain):
+        * WebKitTestRunner/InjectedBundle/TestRunner.h:
+        * WebKitTestRunner/TestController.h:
+        * WebKitTestRunner/TestInvocation.cpp:
+        (WTR::TestInvocation::didReceiveSynchronousMessageFromInjectedBundle):
+        * WebKitTestRunner/cocoa/TestControllerCocoa.mm:
+        (WTR::TestController::cleanUpKeychain):
+        (WTR::TestController::keyExistsInKeychain):
+
 2020-03-11  Keith Miller  <keith_miller@apple.com>
 
         Test262-runner should always consider crashes as new failures
index 749c97f..ac11a58 100644 (file)
@@ -62,7 +62,7 @@ static String testES256PrivateKeyBase64 =
     "BDj/zxSkzKgaBuS3cdWDF558of8AaIpgFpsjF/Qm1749VBJPgqUIwfhWHJ91nb7U"
     "PH76c0+WFOzZKslPyyFse4goGIW2R7k9VHLPEZl5nfnBgEVFh5zev+/xpHQIvuq6"
     "RQ==";
-static String testUserhandleBase64 = "AAECAwQFBgcICQ==";
+static String testUserEntityBundleBase64 = "omJpZEoAAQIDBAUGBwgJZG5hbWVwQUFFQ0F3UUZCZ2NJQ1E9PQ==";
 
 @interface TestWebAuthenticationPanelDelegate : NSObject <_WKWebAuthenticationPanelDelegate>
 @end
@@ -1214,7 +1214,7 @@ TEST(WebAuthenticationPanel, LADuplicateCredential)
     auto delegate = adoptNS([[TestWebAuthenticationPanelUIDelegate alloc] init]);
     [webView setUIDelegate:delegate.get()];
 
-    ASSERT_TRUE(addKeyToKeychain(testES256PrivateKeyBase64, "", testUserhandleBase64));
+    ASSERT_TRUE(addKeyToKeychain(testES256PrivateKeyBase64, "", testUserEntityBundleBase64));
     [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
     Util::run(&webAuthenticationPanelUpdateLAExcludeCredentialsMatched);
     cleanUpKeychain("");
@@ -1305,7 +1305,7 @@ TEST(WebAuthenticationPanel, LAGetAssertion)
     auto delegate = adoptNS([[TestWebAuthenticationPanelUIDelegate alloc] init]);
     [webView setUIDelegate:delegate.get()];
 
-    ASSERT_TRUE(addKeyToKeychain(testES256PrivateKeyBase64, "", testUserhandleBase64));
+    ASSERT_TRUE(addKeyToKeychain(testES256PrivateKeyBase64, "", testUserEntityBundleBase64));
     [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
     [webView waitForMessage:@"Succeeded!"];
     checkPanel([delegate panel], @"", @[adoptNS([[NSNumber alloc] initWithInt:_WKWebAuthenticationTransportUSB]).get(), adoptNS([[NSNumber alloc] initWithInt:_WKWebAuthenticationTransportInternal]).get()], _WKWebAuthenticationTypeGet);
index 4901e08..1302710 100644 (file)
@@ -404,8 +404,8 @@ interface TestRunner {
 
     // WebAuthn
     void addTestKeyToKeychain(DOMString privateKeyBase64, DOMString attrLabel, DOMString applicationTagBase64);
-    void cleanUpKeychain(DOMString attrLabel, optional DOMString applicationTagBase64);
-    boolean keyExistsInKeychain(DOMString attrLabel, DOMString applicationTagBase64);
+    void cleanUpKeychain(DOMString attrLabel, optional DOMString applicationLabelBase64);
+    boolean keyExistsInKeychain(DOMString attrLabel, DOMString applicationLabelBase64);
 
     // Ad Click Attribution
     void clearAdClickAttribution();
index 82dd6aa..2e98cf5 100644 (file)
@@ -2788,7 +2788,7 @@ void TestRunner::addTestKeyToKeychain(JSStringRef privateKeyBase64, JSStringRef
     WKBundlePostSynchronousMessage(InjectedBundle::singleton().bundle(), messageName.get(), messageBody.get(), nullptr);
 }
 
-void TestRunner::cleanUpKeychain(JSStringRef attrLabel, JSStringRef applicationTagBase64)
+void TestRunner::cleanUpKeychain(JSStringRef attrLabel, JSStringRef applicationLabelBase64)
 {
     Vector<WKRetainPtr<WKStringRef>> keys;
     Vector<WKRetainPtr<WKTypeRef>> values;
@@ -2796,9 +2796,9 @@ void TestRunner::cleanUpKeychain(JSStringRef attrLabel, JSStringRef applicationT
     keys.append(adoptWK(WKStringCreateWithUTF8CString("AttrLabel")));
     values.append(toWK(attrLabel));
 
-    if (applicationTagBase64) {
-        keys.append(adoptWK(WKStringCreateWithUTF8CString("ApplicationTag")));
-        values.append(toWK(applicationTagBase64));
+    if (applicationLabelBase64) {
+        keys.append(adoptWK(WKStringCreateWithUTF8CString("ApplicationLabel")));
+        values.append(toWK(applicationLabelBase64));
     }
 
     Vector<WKStringRef> rawKeys;
@@ -2817,7 +2817,7 @@ void TestRunner::cleanUpKeychain(JSStringRef attrLabel, JSStringRef applicationT
     WKBundlePostSynchronousMessage(InjectedBundle::singleton().bundle(), messageName.get(), messageBody.get(), nullptr);
 }
 
-bool TestRunner::keyExistsInKeychain(JSStringRef attrLabel, JSStringRef applicationTagBase64)
+bool TestRunner::keyExistsInKeychain(JSStringRef attrLabel, JSStringRef applicationLabelBase64)
 {
     Vector<WKRetainPtr<WKStringRef>> keys;
     Vector<WKRetainPtr<WKTypeRef>> values;
@@ -2825,8 +2825,8 @@ bool TestRunner::keyExistsInKeychain(JSStringRef attrLabel, JSStringRef applicat
     keys.append(adoptWK(WKStringCreateWithUTF8CString("AttrLabel")));
     values.append(toWK(attrLabel));
 
-    keys.append(adoptWK(WKStringCreateWithUTF8CString("ApplicationTag")));
-    values.append(toWK(applicationTagBase64));
+    keys.append(adoptWK(WKStringCreateWithUTF8CString("ApplicationLabel")));
+    values.append(toWK(applicationLabelBase64));
 
     Vector<WKStringRef> rawKeys;
     Vector<WKTypeRef> rawValues;
index 0b12481..da0ad1f 100644 (file)
@@ -512,8 +512,8 @@ public:
 
     // FIXME(189876)
     void addTestKeyToKeychain(JSStringRef privateKeyBase64, JSStringRef attrLabel, JSStringRef applicationTagBase64);
-    void cleanUpKeychain(JSStringRef attrLabel, JSStringRef applicationTagBase64);
-    bool keyExistsInKeychain(JSStringRef attrLabel, JSStringRef applicationTagBase64);
+    void cleanUpKeychain(JSStringRef attrLabel, JSStringRef applicationLabelBase64);
+    bool keyExistsInKeychain(JSStringRef attrLabel, JSStringRef applicationLabelBase64);
 
     unsigned long serverTrustEvaluationCallbackCallsCount();
 
index 078991b..a8ad58f 100644 (file)
@@ -316,8 +316,8 @@ public:
     void setServiceWorkerFetchTimeoutForTesting(double seconds);
 
     void addTestKeyToKeychain(const String& privateKeyBase64, const String& attrLabel, const String& applicationTagBase64);
-    void cleanUpKeychain(const String& attrLabel, const String& applicationTagBase64);
-    bool keyExistsInKeychain(const String& attrLabel, const String& applicationTagBase64);
+    void cleanUpKeychain(const String& attrLabel, const String& applicationLabelBase64);
+    bool keyExistsInKeychain(const String& attrLabel, const String& applicationLabelBase64);
 
 #if PLATFORM(COCOA)
     NSString *overriddenCalendarIdentifier() const;
index 416f6ef..f8cce27 100644 (file)
@@ -1723,10 +1723,10 @@ WKRetainPtr<WKTypeRef> TestInvocation::didReceiveSynchronousMessageFromInjectedB
         WKRetainPtr<WKStringRef> attrLabelKey = adoptWK(WKStringCreateWithUTF8CString("AttrLabel"));
         WKStringRef attrLabelWK = static_cast<WKStringRef>(WKDictionaryGetItemForKey(testDictionary, attrLabelKey.get()));
 
-        WKRetainPtr<WKStringRef> applicationTagKey = adoptWK(WKStringCreateWithUTF8CString("ApplicationTag"));
-        WKStringRef applicationTagWK = static_cast<WKStringRef>(WKDictionaryGetItemForKey(testDictionary, applicationTagKey.get()));
+        WKRetainPtr<WKStringRef> applicationLabelKey = adoptWK(WKStringCreateWithUTF8CString("ApplicationLabel"));
+        WKStringRef applicationLabelWK = static_cast<WKStringRef>(WKDictionaryGetItemForKey(testDictionary, applicationLabelKey.get()));
 
-        TestController::singleton().cleanUpKeychain(toWTFString(attrLabelWK), applicationTagWK ? toWTFString(applicationTagWK) : String());
+        TestController::singleton().cleanUpKeychain(toWTFString(attrLabelWK), applicationLabelWK ? toWTFString(applicationLabelWK) : String());
         return nullptr;
     }
 
@@ -1737,10 +1737,10 @@ WKRetainPtr<WKTypeRef> TestInvocation::didReceiveSynchronousMessageFromInjectedB
         WKRetainPtr<WKStringRef> attrLabelKey = adoptWK(WKStringCreateWithUTF8CString("AttrLabel"));
         WKStringRef attrLabelWK = static_cast<WKStringRef>(WKDictionaryGetItemForKey(testDictionary, attrLabelKey.get()));
 
-        WKRetainPtr<WKStringRef> applicationTagKey = adoptWK(WKStringCreateWithUTF8CString("ApplicationTag"));
-        WKStringRef applicationTagWK = static_cast<WKStringRef>(WKDictionaryGetItemForKey(testDictionary, applicationTagKey.get()));
+        WKRetainPtr<WKStringRef> applicationLabelKey = adoptWK(WKStringCreateWithUTF8CString("ApplicationLabel"));
+        WKStringRef applicationLabelWK = static_cast<WKStringRef>(WKDictionaryGetItemForKey(testDictionary, applicationLabelKey.get()));
 
-        bool keyExistsInKeychain = TestController::singleton().keyExistsInKeychain(toWTFString(attrLabelWK), toWTFString(applicationTagWK));
+        bool keyExistsInKeychain = TestController::singleton().keyExistsInKeychain(toWTFString(attrLabelWK), toWTFString(applicationLabelWK));
         WKRetainPtr<WKTypeRef> result = adoptWK(WKBooleanCreate(keyExistsInKeychain));
         return result;
     }
index f2e3a66..fc1d51b 100644 (file)
@@ -430,7 +430,7 @@ void TestController::addTestKeyToKeychain(const String& privateKeyBase64, const
     ASSERT_UNUSED(status, !status);
 }
 
-void TestController::cleanUpKeychain(const String& attrLabel, const String& applicationTagBase64)
+void TestController::cleanUpKeychain(const String& attrLabel, const String& applicationLabelBase64)
 {
     auto deleteQuery = adoptNS([[NSMutableDictionary alloc] init]);
     [deleteQuery setObject:(id)kSecClassKey forKey:(id)kSecClass];
@@ -440,19 +440,19 @@ void TestController::cleanUpKeychain(const String& attrLabel, const String& appl
 #else
     [deleteQuery setObject:@YES forKey:(id)kSecAttrNoLegacy];
 #endif
-    if (!!applicationTagBase64)
-        [deleteQuery setObject:adoptNS([[NSData alloc] initWithBase64EncodedString:applicationTagBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]).get() forKey:(id)kSecAttrApplicationTag];
+    if (!!applicationLabelBase64)
+        [deleteQuery setObject:adoptNS([[NSData alloc] initWithBase64EncodedString:applicationLabelBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]).get() forKey:(id)kSecAttrApplicationLabel];
 
     SecItemDelete((__bridge CFDictionaryRef)deleteQuery.get());
 }
 
-bool TestController::keyExistsInKeychain(const String& attrLabel, const String& applicationTagBase64)
+bool TestController::keyExistsInKeychain(const String& attrLabel, const String& applicationLabelBase64)
 {
     NSDictionary *query = @{
         (id)kSecClass: (id)kSecClassKey,
         (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
         (id)kSecAttrLabel: attrLabel,
-        (id)kSecAttrApplicationTag: adoptNS([[NSData alloc] initWithBase64EncodedString:applicationTagBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]).get(),
+        (id)kSecAttrApplicationLabel: adoptNS([[NSData alloc] initWithBase64EncodedString:applicationLabelBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]).get(),
 #if HAVE(DATA_PROTECTION_KEYCHAIN)
         (id)kSecUseDataProtectionKeychain: @YES
 #else