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();