Skip to content
Permalink
Browse files
[WebAuthn] Reject too long or too short user id
https://bugs.webkit.org/show_bug.cgi?id=242073
rdar://90281685

Reviewed by Brent Fulgham.

The creation option's user.id should be between 1-64 bytes inclusive
per the spec: https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-id

* LayoutTests/http/wpt/webauthn/public-key-credential-create-success-hid.https-expected.txt:
* LayoutTests/http/wpt/webauthn/public-key-credential-create-success-hid.https.html:
* LayoutTests/http/wpt/webauthn/public-key-credential-create-with-invalid-parameters.https-expected.txt:
* LayoutTests/http/wpt/webauthn/public-key-credential-create-with-invalid-parameters.https.html:
* Source/WebCore/Modules/webauthn/AuthenticatorCoordinator.cpp:
(WebCore::AuthenticatorCoordinator::create const):

Canonical link: https://commits.webkit.org/251938@main
  • Loading branch information
pascoej committed Jun 29, 2022
1 parent fb5a266 commit 8f49569a327273a80904ab7d3cf11b31f5fcb4cc
Showing 5 changed files with 66 additions and 8 deletions.
@@ -14,8 +14,12 @@ CONSOLE MESSAGE: User gesture is not detected. To use the WebAuthn API, call 'na
CONSOLE MESSAGE: User gesture is not detected. To use the WebAuthn API, call 'navigator.credentials.create' or 'navigator.credentials.get' within user activated events.
CONSOLE MESSAGE: User gesture is not detected. To use the WebAuthn API, call 'navigator.credentials.create' or 'navigator.credentials.get' within user activated events.
CONSOLE MESSAGE: User gesture is not detected. To use the WebAuthn API, call 'navigator.credentials.create' or 'navigator.credentials.get' within user activated events.
CONSOLE MESSAGE: User gesture is not detected. To use the WebAuthn API, call 'navigator.credentials.create' or 'navigator.credentials.get' within user activated events.
CONSOLE MESSAGE: User gesture is not detected. To use the WebAuthn API, call 'navigator.credentials.create' or 'navigator.credentials.get' within user activated events.

PASS PublicKeyCredential's [[create]] with minimum options in a mock hid authenticator.
PASS PublicKeyCredential's [[create]] with user handle of length=1 in a mock hid authenticator.
PASS PublicKeyCredential's [[create]] with user handle of length=64 in a mock hid authenticator.
PASS PublicKeyCredential's [[create]] with empty pubKeyCredParams in a mock hid authenticator.
PASS PublicKeyCredential's [[create]] with authenticatorSelection { 'cross-platform' } in a mock hid authenticator.
PASS PublicKeyCredential's [[create]] with requireResidentKey { false } in a mock hid authenticator.
@@ -31,6 +31,50 @@
});
}, "PublicKeyCredential's [[create]] with minimum options in a mock hid authenticator.");

promise_test(t => {
const options = {
publicKey: {
rp: {
name: "localhost",
},
user: {
name: "John Appleseed",
id: asciiToUint8Array("1"),
displayName: "Appleseed",
},
challenge: Base64URL.parse("MTIzNDU2"),
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
timeout: 100
}
};

return navigator.credentials.create(options).then(credential => {
checkCtapMakeCredentialResult(credential, true, ["usb"]);
});
}, "PublicKeyCredential's [[create]] with user handle of length=1 in a mock hid authenticator.");

promise_test(t => {
const options = {
publicKey: {
rp: {
name: "localhost",
},
user: {
name: "John Appleseed",
id: asciiToUint8Array("1234567812345678123456781234567812345678123456781234567812345678"),
displayName: "Appleseed",
},
challenge: Base64URL.parse("MTIzNDU2"),
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
timeout: 100
}
};

return navigator.credentials.create(options).then(credential => {
checkCtapMakeCredentialResult(credential, true, ["usb"]);
});
}, "PublicKeyCredential's [[create]] with user handle of length=64 in a mock hid authenticator.");

promise_test(t => {
const options = {
publicKey: {
@@ -54,4 +54,6 @@ PASS PublicKeyCredential's [[create]] with with invalid parameters. 52
PASS PublicKeyCredential's [[create]] with with invalid parameters. 53
PASS PublicKeyCredential's [[create]] with with invalid parameters. 54
PASS PublicKeyCredential's [[create]] with with invalid parameters. 55
PASS PublicKeyCredential's [[create]] with with invalid parameters. 56
PASS PublicKeyCredential's [[create]] with with invalid parameters. 57

@@ -43,6 +43,8 @@
[{ name: rp.name, id: Symbol() }, user, challenge, [pubKeyCredParam]],
// wrong user attribute type
[rp, { name: Symbol(), id: user.id, displayName: user.displayName}, challenge, [pubKeyCredParam]],
[rp, { name: user.name, id: asciiToUint8Array(""), displayName: user.displayName}, challenge, [pubKeyCredParam]],
[rp, { name: user.name, id: asciiToUint8Array("12345678123456781234567812345678123456781234567812345678123456781"), displayName: user.displayName}, challenge, [pubKeyCredParam]],
[rp, { name: user.name, id: 1, displayName: user.displayName}, challenge, [pubKeyCredParam]],
[rp, { name: user.name, id: true, displayName: user.displayName}, challenge, [pubKeyCredParam]],
[rp, { name: user.name, id: null, displayName: user.displayName}, challenge, [pubKeyCredParam]],
@@ -114,31 +114,37 @@ void AuthenticatorCoordinator::create(const Document& document, const PublicKeyC
const auto& callerOrigin = document.securityOrigin();
auto* frame = document.frame();
ASSERT(frame);
// The following implements https://www.w3.org/TR/webauthn/#createCredential as of 5 December 2017.
// The following implements https://www.w3.org/TR/webauthn-2/#createCredential as of 28 June 2022.
// Step 1, 3, 16 are handled by the caller.
// Step 2.
if (scope != WebAuthn::Scope::SameOrigin) {
promise.reject(Exception { NotAllowedError, "The origin of the document is not the same as its ancestors."_s });
return;
}

// Step 5. Skipped since SecurityOrigin doesn't have the concept of "opaque origin".
// Step 6. The effective domain may be represented in various manners, such as a domain or an ip address.
// Step 5.
if (options.user.id.length() < 1 || options.user.id.length() > 64) {
promise.reject(Exception { TypeError, "The length options.user.id must be between 1-64 bytes."_s });
return;
}

// Step 6. Skipped since SecurityOrigin doesn't have the concept of "opaque origin".
// Step 7. The effective domain may be represented in various manners, such as a domain or an ip address.
// Only the domain format of host is permitted in WebAuthN.
if (URL::hostIsIPAddress(callerOrigin.domain())) {
promise.reject(Exception { SecurityError, "The effective domain of the document is not a valid domain."_s });
return;
}

// Step 7.
// Step 8.
if (!options.rp.id.isEmpty() && !callerOrigin.isMatchingRegistrableDomainSuffix(options.rp.id)) {
promise.reject(Exception { SecurityError, "The provided RP ID is not a registrable domain suffix of the effective domain of the document."_s });
return;
}
if (options.rp.id.isEmpty())
options.rp.id = callerOrigin.domain();

// Step 8-10.
// Step 9-11.
// Most of the jobs are done by bindings.
if (options.pubKeyCredParams.isEmpty()) {
options.pubKeyCredParams.append({ PublicKeyCredentialType::PublicKey, COSE::ES256 });
@@ -153,15 +159,15 @@ void AuthenticatorCoordinator::create(const Document& document, const PublicKeyC
}
}

// Step 11-12.
// Step 12-13.
// Only Google Legacy AppID Support Extension is supported.
options.extensions = AuthenticationExtensionsClientInputs { String(), processGoogleLegacyAppIdSupportExtension(options.extensions, options.rp.id), options.extensions && options.extensions->credProps };

// Step 13-15.
// Step 14-16.
auto clientDataJson = buildClientDataJson(ClientDataType::Create, options.challenge, callerOrigin, scope);
auto clientDataJsonHash = buildClientDataJsonHash(clientDataJson);

// Step 4, 17-21.
// Step 4, 18-22.
if (!m_client) {
promise.reject(Exception { UnknownError, "Unknown internal error."_s });
return;

0 comments on commit 8f49569

Please sign in to comment.