webauthn: implement PublicKeyCredential.parseCreationOptionsFromJSON()
This adds IDL, implementation and WPT for a method that parses a
PublicKeyCredentialCreationOptions from an equivalent JSON type. Flag
guarded by the existing WebAuthenticationJSONSerialization runtime flag.
Spec: https://w3c.github.io/webauthn/#sctn-parseCreationOptionsFromJSON
Low-Coverage-Reason: Covered by WPTs
Bug: 1401128
Change-Id: Id7140d93774d39bbd51185fdb8c66448e0b1a629
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5155353
Commit-Queue: Adam Langley <agl@chromium.org>
Reviewed-by: Adam Langley <agl@chromium.org>
Auto-Submit: Martin Kreichgauer <martinkr@google.com>
Commit-Queue: Martin Kreichgauer <martinkr@google.com>
Cr-Commit-Position: refs/heads/main@{#1242064}
diff --git a/webauthn/helpers.js b/webauthn/helpers.js
index 56d941a..5b30749 100644
--- a/webauthn/helpers.js
+++ b/webauthn/helpers.js
@@ -626,3 +626,52 @@
return testCb(t);
}, name);
}
+
+function bytesEqual(a, b) {
+ if (a instanceof ArrayBuffer) {
+ a = new Uint8Array(a);
+ }
+ if (b instanceof ArrayBuffer) {
+ b = new Uint8Array(b);
+ }
+ if (a.byteLength != b.byteLength) {
+ return false;
+ }
+ for (let i = 0; i < a.byteLength; i++) {
+ if (a[i] != b[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Compares two PublicKeyCredentialUserEntity objects.
+function userEntityEquals(a, b) {
+ return bytesEqual(a.id, b.id) && a.name == b.name && a.displayName == b.displayName;
+}
+
+// Asserts that `actual` and `expected`, which are both JSON types, are equal.
+// The object key order is ignored for comparison.
+function assertJsonEquals(actual, expected, optMsg) {
+ // Returns a copy of `jsonObj`, which must be a JSON type, with object keys
+ // recursively sorted in lexicographic order; or simply `jsonObj` if it is not
+ // an instance of Object.
+ function deepSortKeys(jsonObj) {
+ if (jsonObj instanceof Array) {
+ return Array.from(jsonObj, (x) => { return deepSortKeys(x); })
+ }
+ if (typeof jsonObj !== 'object' || jsonObj === null ||
+ jsonObj.__proto__.constructor !== Object ||
+ Object.keys(jsonObj).length === 0) {
+ return jsonObj;
+ }
+ return Object.keys(jsonObj).sort().reduce((acc, key) => {
+ acc[key] = deepSortKeys(jsonObj[key]);
+ return acc;
+ }, {});
+ }
+
+ assert_equals(
+ JSON.stringify(deepSortKeys(actual)),
+ JSON.stringify(deepSortKeys(expected)), optMsg);
+}
diff --git a/webauthn/public-key-credential-creation-options-from-json.https.window.js b/webauthn/public-key-credential-creation-options-from-json.https.window.js
new file mode 100644
index 0000000..c0f1a08
--- /dev/null
+++ b/webauthn/public-key-credential-creation-options-from-json.https.window.js
@@ -0,0 +1,213 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/resources/utils.js
+// META: script=helpers.js
+
+// The string "test" as ASCII bytes and base64url-encoded.
+const test_bytes = new Uint8Array([0x74, 0x65, 0x73, 0x74]);
+const test_b64 = "dGVzdA";
+
+test(() => {
+ let actual = PublicKeyCredential.parseCreationOptionsFromJSON({
+ rp: {
+ id: "example.com",
+ name: "Example Inc",
+ },
+ user: {
+ id: test_b64,
+ name: "test@example.com",
+ displayName: "test user"
+ },
+ challenge: test_b64,
+ pubKeyCredParams: [
+ {
+ type: "public-key",
+ alg: -7,
+ },
+ ],
+ });
+ let expected = {
+ rp: {
+ id: "example.com",
+ name: "Example Inc",
+ },
+ user: {
+ id: test_bytes,
+ name: "test@example.com",
+ displayName: "test user"
+ },
+ challenge: test_bytes,
+ pubKeyCredParams: [
+ {
+ type: "public-key",
+ alg: -7,
+ },
+ ],
+ // The spec defaults the following fields:
+ attestation: "none",
+ hints: [],
+ };
+
+ assertJsonEquals(actual.rp, expected.rp);
+ assert_true(userEntityEquals(actual.user, expected.user));
+ assert_true(bytesEqual(actual.challenge, expected.challenge));
+ assertJsonEquals(actual.pubKeyCredParams, expected.pubKeyCredParams, "pk");
+ assert_equals(actual.attestation, expected.attestation);
+ assertJsonEquals(actual.hints, expected.hints);
+}, "parseCreationOptionsFromJSON()");
+
+test(() => {
+ assert_throws_dom("EncodingError", () => {
+ PublicKeyCredential.parseCreationOptionsFromJSON({
+ rp: {
+ id: "example.com",
+ name: "Example Inc",
+ },
+ user: {
+ id: "not valid base64url",
+ name: "test@example.com",
+ displayName: "test user"
+ },
+ challenge: "not valid base64url",
+ pubKeyCredParams: [
+ {
+ type: "public-key",
+ alg: -7,
+ },
+ ],
+ });
+ });
+}, "parseCreationOptionsFromJSON() throws EncodingError");
+
+test(() => {
+ let actual = PublicKeyCredential.parseCreationOptionsFromJSON({
+ rp: {
+ id: "example.com",
+ name: "Example Inc",
+ },
+ user: {
+ id: test_b64,
+ name: "test@example.com",
+ displayName: "test user"
+ },
+ challenge: test_b64,
+ pubKeyCredParams: [
+ {
+ type: "public-key",
+ alg: -7,
+ },
+ ],
+ extensions: {
+ appid: "app id",
+ appidExclude: "app id exclude",
+ hamcCreateSecret: true,
+ uvm: true,
+ credentialProtectionPolicy: "cred protect",
+ enforceCredentialProtectionPolicy: true,
+ minPinLength: true,
+ credProps: true,
+ largeBlob: {
+ support: "large blob support",
+ read: true,
+ write: test_b64,
+ },
+ credBlob: test_b64,
+ getCredBlob: true,
+ supplementalPubKeys: {
+ scopes: ["spk scope"],
+ attestation: "directest",
+ attestationFormats: ["asn2"],
+ },
+ prf: {
+ eval: {
+ first: test_b64,
+ second: test_b64,
+ },
+ evalByCredential: {
+ "test cred": {
+ first: test_b64,
+ second: test_b64,
+ },
+ },
+ },
+ },
+ });
+ let expected = {
+ rp: {
+ id: "example.com",
+ name: "Example Inc",
+ },
+ user: {
+ id: test_bytes,
+ name: "test@example.com",
+ displayName: "test user"
+ },
+ challenge: test_bytes,
+ pubKeyCredParams: [
+ {
+ type: "public-key",
+ alg: -7,
+ },
+ ],
+ extensions: {
+ appid: "app id",
+ appidExclude: "app id exclude",
+ hamcCreateSecret: true,
+ uvm: true,
+ credentialProtectionPolicy: "cred protect",
+ enforceCredentialProtectionPolicy: true,
+ minPinLength: true,
+ credProps: true,
+ largeBlob: {
+ support: "large blob support",
+ read: true,
+ write: test_bytes,
+ },
+ credBlob: test_bytes,
+ getCredBlob: true,
+ supplementalPubKeys: {
+ scopes: ["spk scope"],
+ attestation: "directest",
+ attestationFormats: ["asn2"],
+ },
+ prf: {
+ eval: {
+ first: test_bytes,
+ second: test_bytes,
+ },
+ evalByCredential: {
+ "test cred": {
+ first: test_bytes,
+ second: test_bytes,
+ },
+ },
+ },
+ // The spec defaults the following fields:
+ attestation: "none",
+ hints: [],
+ },
+ };
+
+ assert_equals(actual.extensions.appid, expected.extensions.appid);
+ assert_equals(actual.extensions.appidExclude, expected.extensions.appidExclude);
+ assert_equals(actual.extensions.hmacCreateSecret, expected.extensions.hmacCreateSecret);
+ assert_equals(actual.extensions.uvm, expected.extensions.uvm);
+ assert_equals(actual.extensions.credentialProtectionPolicy, expected.extensions.credentialProtectionPolicy);
+ assert_equals(actual.extensions.enforceCredentialProtectionPolicy, expected.extensions.enforceCredentialProtectionPolicy);
+ assert_equals(actual.extensions.minPinLength, expected.extensions.minPinLength);
+ assert_equals(actual.extensions.credProps, expected.extensions.credProps);
+ assert_equals(actual.extensions.largeBlob.support, expected.extensions.largeBlob.support, "X");
+ assert_equals(actual.extensions.largeBlob.read, expected.extensions.largeBlob.read);
+
+ assert_true(bytesEqual(actual.extensions.largeBlob.write, expected.extensions.largeBlob.write), "XX");
+
+ assert_true(bytesEqual(actual.extensions.credBlob, expected.extensions.credBlob), "XXX");
+
+ assert_equals(actual.extensions.getCredBlob, expected.extensions.getCredBlob);
+ assertJsonEquals(actual.extensions.supplementalPubKeys, expected.extensions.supplementalPubKeys);
+ let prfValuesEquals = (a, b) => {
+ return bytesEqual(a.first, b.first) && bytesEqual(a.second, b.second);
+ };
+ assert_true(prfValuesEquals(actual.extensions.prf.eval, expected.extensions.prf.eval), "prf eval");
+ assert_true(prfValuesEquals(actual.extensions.prf.evalByCredential["test cred"], expected.extensions.prf.evalByCredential["test cred"]), "prf ebc");
+}, "parseCreationOptionsFromJSON() with extensions");
diff --git a/webauthn/public-key-credential-to-json.https.window.js b/webauthn/public-key-credential-to-json.https.window.js
index 1fbd434..8ef2513 100644
--- a/webauthn/public-key-credential-to-json.https.window.js
+++ b/webauthn/public-key-credential-to-json.https.window.js
@@ -115,29 +115,6 @@
return convertObject(cred, keys);
}
-// Returns a copy of `jsonObj`, which must be a JSON type, with object keys
-// recursively sorted in lexicographic order; or simply `jsonObj` if it is not
-// an instance of Object.
-function deepSortKeys(jsonObj) {
- if (typeof jsonObj !== 'object' || jsonObj === null ||
- jsonObj.__proto__.constructor !== Object ||
- Object.keys(jsonObj).length === 0) {
- return jsonObj;
- }
- return Object.keys(jsonObj).sort().reduce((acc, key) => {
- acc[key] = deepSortKeys(jsonObj[key]);
- return acc;
- }, {});
-}
-
-// Asserts that `actual` and `expected`, which are both JSON types, are equal.
-// The object key order is ignored for comparison.
-function assertJsonEquals(actual, expected, optMsg) {
- assert_equals(
- JSON.stringify(deepSortKeys(actual)),
- JSON.stringify(deepSortKeys(expected)), optMsg);
-}
-
virtualAuthenticatorPromiseTest(
async t => {
let credential = await createCredential();