Bug 1329802 - WebAuthn Unit Tests: Add Unit Tests r=keeler draft
authorJ.C. Jones <jjones@mozilla.com>
Mon, 09 Jan 2017 13:28:02 -0700
changeset 460899 72c24bdc028e440705598c694f3c4119d5304d83
parent 460898 671a2f8542cadcf9aad318ab76250a730a03b240
child 542164 c19e862325d7302c87e00358608245a1ff9249fc
push id41519
push userjjones@mozilla.com
push dateSat, 14 Jan 2017 00:34:52 +0000
reviewerskeeler
bugs1329802, 1286312
milestone53.0a1
Bug 1329802 - WebAuthn Unit Tests: Add Unit Tests r=keeler This uses the new mochitest "scheme" option from Bug 1286312. This cannot land until after Bug 1286312 does. For now, you can test locally by adding --setpref dom.securecontext.whitelist=mochi.test to your command line, such as: ~/hg/mozilla-central/mach mochitest \ --setpref dom.securecontext.whitelist=mochi.test ./dom/u2f/tests/ Updated: Review fixes (thanks keeler!) MozReview-Commit-ID: 7jTxF3Mrtcg
dom/u2f/tests/mochitest.ini
dom/u2f/tests/test_webauthn_get_assertion.html
dom/u2f/tests/test_webauthn_loopback.html
dom/u2f/tests/test_webauthn_make_credential.html
dom/u2f/tests/test_webauthn_no_token.html
dom/u2f/tests/test_webauthn_sameorigin.html
dom/u2f/tests/u2futil.js
--- a/dom/u2f/tests/mochitest.ini
+++ b/dom/u2f/tests/mochitest.ini
@@ -22,8 +22,23 @@ skip-if = !e10s
 [test_register_sign.html]
 skip-if = !e10s
 [test_appid_facet.html]
 skip-if = !e10s
 [test_appid_facet_insecure.html]
 skip-if = !e10s
 [test_appid_facet_subdomain.html]
 skip-if = !e10s
+[test_webauthn_loopback.html]
+skip-if = !e10s
+scheme = https
+[test_webauthn_no_token.html]
+skip-if = !e10s
+scheme = https
+[test_webauthn_make_credential.html]
+skip-if = !e10s
+scheme = https
+[test_webauthn_get_assertion.html]
+skip-if = !e10s
+scheme = https
+[test_webauthn_sameorigin.html]
+skip-if = !e10s
+scheme = https
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/u2f/tests/test_webauthn_get_assertion.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+  <title>Tests for GetAssertion for W3C Web Authentication</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="u2futil.js"></script>
+  <script type="text/javascript" src="pkijs/common.js"></script>
+  <script type="text/javascript" src="pkijs/asn1.js"></script>
+  <script type="text/javascript" src="pkijs/x509_schema.js"></script>
+  <script type="text/javascript" src="pkijs/x509_simpl.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<h1>Tests for GetAssertion for W3C Web Authentication</h1>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+// Execute the full-scope test
+SimpleTest.waitForExplicitFinish();
+
+function arrivingHereIsBad(aResult) {
+  ok(false, "Bad result! Received a: " + aResult);
+  return Promise.resolve();
+}
+
+function expectNotAllowedError(aResult) {
+  ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError");
+  return Promise.resolve();
+}
+
+function expectTypeError(aResult) {
+  ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError");
+  return Promise.resolve();
+}
+
+SpecialPowers.pushPrefEnv({"set": [["security.webauth.w3c", true],
+                                   ["security.webauth.u2f_enable_softtoken", true],
+                                   ["security.webauth.u2f_enable_usbtoken", false]]},
+function() {
+  isnot(navigator.authentication, undefined, "WebAuthn API endpoint must exist");
+  isnot(navigator.authentication.makeCredential, undefined, "WebAuthn makeCredential API endpoint must exist");
+  isnot(navigator.authentication.getAssertion, undefined, "WebAuthn getAssertion API endpoint must exist");
+
+  let authn = navigator.authentication;
+
+  let gAssertionChallenge = new Uint8Array(16);
+  window.crypto.getRandomValues(gAssertionChallenge);
+
+  let invalidCred = { type: "Magic", id: base64ToBytes("AAA=") };
+  let unknownCred = { type: "ScopedCred", id: base64ToBytes("AAA=") };
+
+  Promise.all([
+    // Test basic good call, but without giving a credential so expect failures
+    // this is OK by the standard, but not supported by U2F-backed authenticators
+    // like the soft token in use here.
+    authn.getAssertion(gAssertionChallenge)
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError),
+
+    // Test with an unexpected option
+    authn.getAssertion(gAssertionChallenge, { unknownValue: "hi" })
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError),
+
+    // Test with an invalid credential
+    authn.getAssertion(gAssertionChallenge, { allowList: [invalidCred] })
+    .then(arrivingHereIsBad)
+    .catch(expectTypeError),
+
+    // Test with an unknown credential
+    authn.getAssertion(gAssertionChallenge, { allowList: [unknownCred] })
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError),
+
+    // Test with an unexpected option and an invalid credential
+    authn.getAssertion(gAssertionChallenge, { unknownValue: "hi" })
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError)
+  ])
+  .then(function(){
+    SimpleTest.finish();
+  });
+});
+
+</script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/u2f/tests/test_webauthn_loopback.html
@@ -0,0 +1,170 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+  <title>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="u2futil.js"></script>
+  <script type="text/javascript" src="pkijs/common.js"></script>
+  <script type="text/javascript" src="pkijs/asn1.js"></script>
+  <script type="text/javascript" src="pkijs/x509_schema.js"></script>
+  <script type="text/javascript" src="pkijs/x509_simpl.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<h1>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</h1>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+// Execute the full-scope test
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({"set": [["security.webauth.w3c", true],
+                                   ["security.webauth.u2f_enable_softtoken", true],
+                                   ["security.webauth.u2f_enable_usbtoken", false]]},
+function() {
+  isnot(navigator.authentication, undefined, "WebAuthn API endpoint must exist");
+  isnot(navigator.authentication.makeCredential, undefined, "WebAuthn makeCredential API endpoint must exist");
+  isnot(navigator.authentication.getAssertion, undefined, "WebAuthn getAssertion API endpoint must exist");
+
+  let authn = navigator.authentication;
+
+  let gCredentialChallenge = new Uint8Array(16);
+  window.crypto.getRandomValues(gCredentialChallenge);
+  let gAssertionChallenge = new Uint8Array(16);
+  window.crypto.getRandomValues(gAssertionChallenge);
+
+  testMakeCredential();
+
+  function checkCredentialValid(aCredInfo) {
+    /* ScopedCredentialInfo
+    - Credential
+    -- ID: Key Handle buffer pulled from U2F Register() Response
+    -- Type: "ScopedCred"
+    - WebAuthnAttestation
+    -- Format: "u2f"
+    -- ClientData: serialized JSON
+    -- AuthenticatorData: RP ID Hash || U2F Sign() Response
+    -- Attestation: U2F Register() Response */
+
+    is(aCredInfo.credential.type, "ScopedCred", "Type is correct");
+    ok(aCredInfo.credential.id.length > 0, "Key ID exists");
+
+    is(aCredInfo.attestation.format, "u2f", "Format is correct");
+    is(aCredInfo.attestation.attestation[0], 0x05, "Reserved byte is correct");
+    ok(aCredInfo.attestation.authenticatorData.length > 0, "Authenticator data exists");
+    let clientData = JSON.parse(buffer2string(aCredInfo.attestation.clientData));
+    is(clientData.challenge, bytesToBase64UrlSafe(gCredentialChallenge), "Challenge is correct");
+    is(clientData.origin, window.location.origin, "Origin is correct");
+    is(clientData.hashAlg, "S256", "Hash algorithm is correct");
+
+    return decodeU2FRegistration(aCredInfo.attestation.attestation)
+    .then(function(u2fObj) {
+      aCredInfo.u2fReg = u2fObj;
+      return aCredInfo;
+    });
+  }
+
+  function checkAssertionAndSigValid(aPublicKey, aAssertion) {
+    /* WebAuthnAssertion
+    - Credential
+    -- ID: ID of Credential from AllowList that succeeded
+    -- Type: "ScopedCred"
+    - ClientData: serialized JSON
+    - AuthenticatorData: RP ID Hash || U2F Sign() Response
+    - Signature: U2F Sign() Response */
+
+    is(aAssertion.credential.type, "ScopedCred", "Type is correct");
+    ok(aAssertion.credential.id.length > 0, "Key ID exists");
+
+    ok(aAssertion.authenticatorData.length > 0, "Authenticator data exists");
+    let clientData = JSON.parse(buffer2string(aAssertion.clientData));
+    is(clientData.challenge, bytesToBase64UrlSafe(gAssertionChallenge), "Challenge is correct");
+    is(clientData.origin, window.location.origin, "Origin is correct");
+    is(clientData.hashAlg, "S256", "Hash algorithm is correct");
+
+    // Parse the signature data
+    if (aAssertion.signature[0] != 0x01) {
+      throw "User presence byte not set";
+    }
+    let presenceAndCounter = aAssertion.signature.slice(0,5);
+    let signatureValue = aAssertion.signature.slice(5);
+
+    let rpIdHash = aAssertion.authenticatorData.slice(0,32);
+
+    // Assemble the signed data and verify the signature
+    return deriveAppAndChallengeParam(clientData.origin, aAssertion.clientData)
+    .then(function(aParams) {
+      console.log(aParams.appParam, rpIdHash, presenceAndCounter, aParams.challengeParam);
+      console.log("ClientData buffer: ", hexEncode(aAssertion.clientData));
+      console.log("ClientDataHash: ", hexEncode(aParams.challengeParam));
+      return assembleSignedData(aParams.appParam, presenceAndCounter, aParams.challengeParam);
+    })
+    .then(function(aSignedData) {
+      console.log(aPublicKey, aSignedData, signatureValue);
+      return verifySignature(aPublicKey, aSignedData, signatureValue);
+    })
+  }
+
+  function testMakeCredential() {
+    let acct = {rpDisplayName: "none", displayName: "none", id: "none"};
+    let param = {type: "ScopedCred", algorithm: "p-256"};
+
+    authn.makeCredential(acct, [param], gCredentialChallenge)
+    .then(checkCredentialValid)
+    .then(testMakeDuplicate)
+    .catch(function(aReason) {
+      ok(false, aReason);
+      SimpleTest.finish();
+    });
+  }
+
+  function testMakeDuplicate(aCredInfo) {
+    let acct = {rpDisplayName: "none", displayName: "none", id: "none"};
+    let param = {type: "ScopedCred", algorithm: "p-256"};
+    let options = {rpId: document.origin,
+                   excludeList: [aCredInfo.credential]};
+
+    authn.makeCredential(acct, [param], gCredentialChallenge, options)
+    .then(function() {
+      // We should have errored here!
+      ok(false, "The excludeList didn't stop a duplicate being created!");
+      SimpleTest.finish();
+    })
+    .catch(function(aReason) {
+      ok(aReason.toString().startsWith("NotAllowedError"), "Expect NotAllowedError, got" + aReason);
+      testAssertion(aCredInfo);
+    });
+  }
+
+  function testAssertion(aCredInfo) {
+    let newCredential = {
+      type: aCredInfo.credential.type,
+      id: Uint8Array.from(aCredInfo.credential.id),
+      transports: [ "usb" ],
+    }
+
+    let assertOptions = {rpId: document.origin, timeoutSeconds: 5,
+                         allowList: [ newCredential ]};
+    authn.getAssertion(gAssertionChallenge, assertOptions)
+    .then(function(aAssertion) {
+      /* Pass along the pubKey. */
+      return checkAssertionAndSigValid(aCredInfo.u2fReg.publicKey, aAssertion);
+    })
+    .then(function(aSigVerifyResult) {
+      ok(aSigVerifyResult, "Signing signature verified");
+      SimpleTest.finish();
+    })
+    .catch(function(reason) {
+      ok(false, "Signing signature invalid: " + reason);
+      SimpleTest.finish();
+    });
+  }
+});
+
+</script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/u2f/tests/test_webauthn_make_credential.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+  <title>Test for MakeCredential for W3C Web Authentication</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="u2futil.js"></script>
+  <script type="text/javascript" src="pkijs/common.js"></script>
+  <script type="text/javascript" src="pkijs/asn1.js"></script>
+  <script type="text/javascript" src="pkijs/x509_schema.js"></script>
+  <script type="text/javascript" src="pkijs/x509_simpl.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<h1>Test for MakeCredential for W3C Web Authentication</h1>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+// Execute the full-scope test
+SimpleTest.waitForExplicitFinish();
+
+function arrivingHereIsGood(aResult) {
+  ok(true, "Good result! Received a: " + aResult);
+  return Promise.resolve();
+}
+
+function arrivingHereIsBad(aResult) {
+  ok(false, "Bad result! Received a: " + aResult);
+  return Promise.resolve();
+}
+
+function expectNotAllowedError(aResult) {
+  ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError");
+  return Promise.resolve();
+}
+
+function expectTypeError(aResult) {
+  ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError");
+  return Promise.resolve();
+}
+
+function expectNotSupportedError(aResult) {
+  ok(aResult.toString().startsWith("NotSupportedError"), "Expecting a NotSupportedError");
+  return Promise.resolve();
+}
+
+SpecialPowers.pushPrefEnv({"set": [["security.webauth.w3c", true],
+                                   ["security.webauth.u2f_enable_softtoken", true],
+                                   ["security.webauth.u2f_enable_usbtoken", false]]},
+function() {
+  isnot(navigator.authentication, undefined, "WebAuthn API endpoint must exist");
+  isnot(navigator.authentication.makeCredential, undefined, "WebAuthn makeCredential API endpoint must exist");
+  isnot(navigator.authentication.getAssertion, undefined, "WebAuthn getAssertion API endpoint must exist");
+
+  let authn = navigator.authentication;
+
+  let gCredentialChallenge = new Uint8Array(16);
+  window.crypto.getRandomValues(gCredentialChallenge);
+
+  let acct = {rpDisplayName: "none", displayName: "none", id: "none"};
+  let param = {type: "ScopedCred", algorithm: "p-256"};
+  let unsupportedParam = {type: "ScopedCred", algorithm: "3DES"};
+  let badParam = {type: "SimplePassword", algorithm: "MaxLength=2"};
+
+  Promise.all([
+    // Test basic good call
+    authn.makeCredential(acct, [param], gCredentialChallenge)
+    .then(arrivingHereIsGood)
+    .catch(arrivingHereIsBad),
+
+    // Test empty account
+    authn.makeCredential({}, [param], gCredentialChallenge)
+    .then(arrivingHereIsBad)
+    .catch(expectTypeError),
+
+    // Test without a parameter
+    authn.makeCredential(acct, [], gCredentialChallenge)
+    .then(arrivingHereIsBad)
+    .catch(expectNotSupportedError),
+
+    // Test without a parameter array at all
+    authn.makeCredential(acct, null, gCredentialChallenge)
+    .then(arrivingHereIsBad)
+    .catch(expectTypeError),
+
+    // Test with an unsupported parameter
+    authn.makeCredential(acct, [unsupportedParam], gCredentialChallenge)
+    .then(arrivingHereIsBad)
+    .catch(expectNotSupportedError),
+
+    // Test with an unsupported parameter and a good one
+    authn.makeCredential(acct, [unsupportedParam, param], gCredentialChallenge)
+    .then(arrivingHereIsGood)
+    .catch(arrivingHereIsBad),
+
+    // Test with a bad parameter
+    authn.makeCredential(acct, [badParam], gCredentialChallenge)
+    .then(arrivingHereIsBad)
+    .catch(expectTypeError),
+
+    // Test with an unsupported parameter, and a bad one
+    authn.makeCredential(acct, [unsupportedParam, badParam],
+                         gCredentialChallenge)
+    .then(arrivingHereIsBad)
+    .catch(expectTypeError),
+
+    // Test with an unsupported parameter, a bad one, and a good one. This
+    // should still fail, as anything with a badParam should fail.
+    authn.makeCredential(acct, [unsupportedParam, badParam, param],
+                         gCredentialChallenge)
+    .then(arrivingHereIsBad)
+    .catch(expectTypeError),
+
+    // Test without a challenge
+    authn.makeCredential(acct, [param], null)
+    .then(arrivingHereIsBad)
+    .catch(expectTypeError),
+
+    // Test with an invalid challenge
+    authn.makeCredential(acct, [param], "begone, thou ill-fitting moist glove!")
+    .then(arrivingHereIsBad)
+    .catch(expectTypeError),
+
+    // Test with duplicate parameters
+    authn.makeCredential(acct, [param, param, param], gCredentialChallenge)
+    .then(arrivingHereIsGood)
+    .catch(arrivingHereIsBad),
+
+    // Test an incomplete account
+    authn.makeCredential({id: "none"}, [param], gCredentialChallenge)
+    .then(arrivingHereIsBad)
+    .catch(expectTypeError),
+
+    authn.makeCredential({name: "none", imageURL: "http://example.com/404"},
+                         [param], gCredentialChallenge)
+    .then(arrivingHereIsBad)
+    .catch(expectTypeError),
+
+    // Test a complete account
+    authn.makeCredential({rpDisplayName: "Foxxy", displayName: "Foxxy V",
+                          id: "foxes_are_the_best@example.com",
+                          name: "Fox F. Foxington",
+                          imageURL: "https://example.com/fox.svg"},
+                         [param], gCredentialChallenge)
+    .then(arrivingHereIsGood)
+    .catch(arrivingHereIsBad)
+  ])
+  .then(function() {
+    SimpleTest.finish();
+  });
+});
+
+</script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/u2f/tests/test_webauthn_no_token.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+  <title>Test for W3C Web Authentication with no token</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="u2futil.js"></script>
+  <script type="text/javascript" src="pkijs/common.js"></script>
+  <script type="text/javascript" src="pkijs/asn1.js"></script>
+  <script type="text/javascript" src="pkijs/x509_schema.js"></script>
+  <script type="text/javascript" src="pkijs/x509_simpl.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<h1>Test for W3C Web Authentication with no token</h1>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+// Execute the full-scope test
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({"set": [["security.webauth.w3c", true],
+                                   ["security.webauth.u2f_enable_softtoken", false],
+                                   ["security.webauth.u2f_enable_usbtoken", false]]},
+function() {
+  isnot(navigator.authentication, undefined, "WebAuthn API endpoint must exist");
+  isnot(navigator.authentication.makeCredential, undefined, "WebAuthn makeCredential API endpoint must exist");
+  isnot(navigator.authentication.getAssertion, undefined, "WebAuthn getAssertion API endpoint must exist");
+
+  let authn = navigator.authentication;
+
+  let credentialChallenge = new Uint8Array(16);
+  window.crypto.getRandomValues(credentialChallenge);
+  let assertionChallenge = new Uint8Array(16);
+  window.crypto.getRandomValues(assertionChallenge);
+  let credentialId = new Uint8Array(128);
+  window.crypto.getRandomValues(credentialId);
+
+  testMakeCredential();
+
+  function testMakeCredential() {
+    let acct = {rpDisplayName: "none", displayName: "none", id: "none"};
+    let param = {type: "ScopedCred", algorithm: "p-256"};
+    authn.makeCredential(acct, [param], credentialChallenge)
+    .then(function(aResult) {
+      ok(false, "Should have failed.");
+      testAssertion();
+    })
+    .catch(function(aReason) {
+      ok(aReason.toString().startsWith("NotAllowedError"), aReason);
+      testAssertion();
+    });
+  }
+
+  function testAssertion() {
+    let newCredential = {
+      type: "ScopedCred",
+      id: credentialId,
+      transports: [ "usb" ],
+    }
+    let assertOptions = {rpId: document.origin, timeoutSeconds: 5,
+                         allowList: [ newCredential ]};
+    authn.getAssertion(assertionChallenge, assertOptions)
+    .then(function(aResult) {
+      ok(false, "Should have failed.");
+      SimpleTest.finish();
+    })
+    .catch(function(aReason) {
+      ok(aReason.toString().startsWith("NotAllowedError"), aReason);
+      SimpleTest.finish();
+    })
+  }
+});
+
+</script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/u2f/tests/test_webauthn_sameorigin.html
@@ -0,0 +1,162 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+  <title>Test for MakeCredential for W3C Web Authentication</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="u2futil.js"></script>
+  <script type="text/javascript" src="pkijs/common.js"></script>
+  <script type="text/javascript" src="pkijs/asn1.js"></script>
+  <script type="text/javascript" src="pkijs/x509_schema.js"></script>
+  <script type="text/javascript" src="pkijs/x509_simpl.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<h1>Test Same Origin Policy for W3C Web Authentication</h1>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+// Execute the full-scope test
+SimpleTest.waitForExplicitFinish();
+
+var gTrackedCredential = {};
+
+function arrivingHereIsGood(aResult) {
+  ok(true, "Good result! Received a: " + aResult);
+  return Promise.resolve();
+}
+
+function arrivingHereIsBad(aResult) {
+  // TODO: Change to `ok` when Bug 1329764 lands
+  todo(false, "Bad result! Received a: " + aResult);
+  return Promise.resolve();
+}
+
+function expectSecurityError(aResult) {
+  // TODO: Change to `ok` when Bug 1329764 lands
+  todo(aResult.toString().startsWith("SecurityError"), "Expecting a SecurityError");
+  return Promise.resolve();
+}
+
+function keepThisScopedCredential(aScopedCredInfo) {
+  gTrackedCredential = {
+    type: aScopedCredInfo.credential.type,
+    id: Uint8Array.from(aScopedCredInfo.credential.id),
+    transports: [ "usb" ],
+  }
+  return Promise.resolve(aScopedCredInfo);
+}
+
+SpecialPowers.pushPrefEnv({"set": [["security.webauth.w3c", true],
+                                   ["security.webauth.u2f_enable_softtoken", true],
+                                   ["security.webauth.u2f_enable_usbtoken", false]]},
+function() {
+  isnot(navigator.authentication, undefined, "WebAuthn API endpoint must exist");
+  isnot(navigator.authentication.makeCredential, undefined,
+        "WebAuthn makeCredential API endpoint must exist");
+  isnot(navigator.authentication.getAssertion, undefined,
+        "WebAuthn getAssertion API endpoint must exist");
+
+  let authn = navigator.authentication;
+
+  let chall = new Uint8Array(16);
+  window.crypto.getRandomValues(chall);
+
+  let acct = {rpDisplayName: "none", displayName: "none", id: "none"};
+  let param = {type: "ScopedCred", algorithm: "p-256"};
+
+  Promise.all([
+    // Test basic good call
+    authn.makeCredential(acct, [param], chall, {rpId: document.origin})
+    .then(keepThisScopedCredential)
+    .then(arrivingHereIsGood)
+    .catch(arrivingHereIsBad),
+
+    // Test rpId being unset
+    authn.makeCredential(acct, [param], chall, {})
+    .then(arrivingHereIsGood)
+    .catch(arrivingHereIsBad),
+
+    // Test this origin with optional fields
+    authn.makeCredential(acct, [param], chall,
+                         {rpId: "user:pass@" + document.origin + ":8888"})
+    .then(arrivingHereIsBad)
+    .catch(expectSecurityError),
+
+    // Test blank rpId
+    authn.makeCredential(acct, [param], chall, {rpId: ""})
+    .then(arrivingHereIsBad)
+    .catch(expectSecurityError),
+
+    // Test subdomain of this origin
+    authn.makeCredential(acct, [param], chall,
+                         {rpId: "subdomain." + document.origin})
+    .then(arrivingHereIsBad)
+    .catch(expectSecurityError),
+
+    // Test another origin
+    authn.makeCredential(acct, [param], chall, {rpId: "example.com"})
+    .then(arrivingHereIsBad)
+    .catch(expectSecurityError),
+
+    // est a different domain within the same TLD
+    authn.makeCredential(acct, [param], chall, {rpId: "alt.test"})
+    .then(arrivingHereIsBad)
+    .catch(expectSecurityError)
+
+  ])
+  .then(function(){
+    return Promise.all([
+      // Test basic good call
+      authn.getAssertion(chall, {allowList: [ gTrackedCredential ],
+                                 rpId: document.origin})
+      .then(arrivingHereIsGood)
+      .catch(arrivingHereIsBad),
+
+      // Test rpId being unset
+      authn.getAssertion(chall, {allowList: [ gTrackedCredential ]})
+      .then(arrivingHereIsGood)
+      .catch(arrivingHereIsBad),
+
+      // Test this origin with optional fields
+      authn.getAssertion(chall, {allowList: [ gTrackedCredential ],
+                                 rpId: "user:pass@" + document.origin + ":8888"})
+      .then(arrivingHereIsBad)
+      .catch(expectSecurityError),
+
+      // Test blank rpId
+      authn.getAssertion(chall, {allowList: [ gTrackedCredential ],
+                                 rpId: ""})
+      .then(arrivingHereIsBad)
+      .catch(expectSecurityError),
+
+      // Test subdomain of this origin
+      authn.getAssertion(chall, {allowList: [ gTrackedCredential ],
+                                 rpId: "subdomain." + document.origin})
+      .then(arrivingHereIsBad)
+      .catch(expectSecurityError),
+
+      // Test another origin
+      authn.getAssertion(chall, {allowList: [ gTrackedCredential ],
+                                 rpId: "example.com"})
+      .then(arrivingHereIsBad)
+      .catch(expectSecurityError),
+
+      // Test a different domain within the same TLD
+      authn.getAssertion(chall, {allowList: [ gTrackedCredential ],
+                                 rpId: "alt.test"})
+      .then(arrivingHereIsBad)
+      .catch(expectSecurityError)
+    ]);
+  })
+  .then(function(){
+    SimpleTest.finish();
+  });
+});
+
+</script>
+
+</body>
+</html>
--- a/dom/u2f/tests/u2futil.js
+++ b/dom/u2f/tests/u2futil.js
@@ -122,16 +122,37 @@ function hexEncode(buf) {
               .map(x => ("0"+x.toString(16)).substr(-2))
               .join("");
 }
 
 function hexDecode(str) {
   return new Uint8Array(str.match(/../g).map(x => parseInt(x, 16)));
 }
 
+function decodeU2FRegistration(aRegData) {
+  if (aRegData[0] != 0x05) {
+    return Promise.reject("Sentinal byte != 0x05");
+  }
+
+  let keyHandleLength = aRegData[66];
+  let u2fRegObj = {
+    publicKeyBytes: aRegData.slice(1, 66),
+    keyHandleBytes: aRegData.slice(67, 67 + keyHandleLength),
+    attestationBytes: aRegData.slice(67 + keyHandleLength)
+  }
+
+  u2fRegObj.keyHandle = bytesToBase64UrlSafe(u2fRegObj.keyHandleBytes);
+
+  return importPublicKey(u2fRegObj.publicKeyBytes)
+  .then(function(keyObj) {
+    u2fRegObj.publicKey = keyObj;
+    return u2fRegObj;
+  });
+}
+
 function importPublicKey(keyBytes) {
   if (keyBytes[0] != 0x04 || keyBytes.byteLength != 65) {
     throw "Bad public key octet string";
   }
   var jwk = {
     kty: "EC",
     crv: "P-256",
     x: bytesToBase64UrlSafe(keyBytes.slice(1, 33)),