Bug 1329764 - Call IsRegistrableDomainSuffixOfOrEqualTo for WebAuthn r?keeler draft
authorJ.C. Jones <jjones@mozilla.com>
Fri, 07 Jul 2017 13:32:31 -0700
changeset 605500 3477552c6b98ecd70a59439f4b898422370f0cc1
parent 605499 8f77d007cd42eed3a43bd9308f60ced20cda9de3
child 636507 c68550f8e9c2c5489359a6e84f92cd52def55bdb
push id67424
push userbmo:jjones@mozilla.com
push dateFri, 07 Jul 2017 20:34:04 +0000
reviewerskeeler
bugs1329764
milestone56.0a1
Bug 1329764 - Call IsRegistrableDomainSuffixOfOrEqualTo for WebAuthn r?keeler nsHTMLDocument included IsRegistrableDomainSuffixOfOrEqualTo() to facilitate some use cases in Web Authentication, and this patch adds support to our implementation. The general idea is to permit relaxing some of the same-origin policy for single-sign-on type approaches, while restricting other uses. [1] [1] https://w3c.github.io/webauthn/#rp-id MozReview-Commit-ID: BP74OYvcwBJ
dom/webauthn/WebAuthnManager.cpp
dom/webauthn/WebAuthnManager.h
dom/webauthn/tests/test_webauthn_loopback.html
dom/webauthn/tests/test_webauthn_make_credential.html
dom/webauthn/tests/test_webauthn_no_token.html
dom/webauthn/tests/test_webauthn_sameorigin.html
--- a/dom/webauthn/WebAuthnManager.cpp
+++ b/dom/webauthn/WebAuthnManager.cpp
@@ -149,22 +149,40 @@ GetOrigin(nsPIDOMWindowInner* aParent,
 }
 
 nsresult
 RelaxSameOrigin(nsPIDOMWindowInner* aParent,
                 const nsAString& aInputRpId,
                 /* out */ nsACString& aRelaxedRpId)
 {
   MOZ_ASSERT(aParent);
+  nsCOMPtr<nsIDocument> doc = aParent->GetDoc();
+  MOZ_ASSERT(doc);
+  nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
+  nsCOMPtr<nsIURI> uri;
+  if (NS_FAILED(principal->GetURI(getter_AddRefs(uri)))) {
+    return NS_ERROR_FAILURE;
+  }
+  nsAutoCString originHost;
+  if (NS_FAILED(uri->GetAsciiHost(originHost))) {
+    return NS_ERROR_FAILURE;
+  }
   nsCOMPtr<nsIDocument> document = aParent->GetDoc();
   if (!document || !document->IsHTMLDocument()) {
     return NS_ERROR_FAILURE;
   }
+  nsHTMLDocument* html = document->AsHTMLDocument();
+  if (NS_WARN_IF(!html)) {
+    return NS_ERROR_FAILURE;
+  }
 
-  // TODO: Bug 1329764: Invoke the Relax Algorithm, once properly defined
+  if (!html->IsRegistrableDomainSuffixOfOrEqualTo(aInputRpId, originHost)) {
+    return NS_ERROR_DOM_SECURITY_ERR;
+  }
+
   aRelaxedRpId.Assign(NS_ConvertUTF16toUTF8(aInputRpId));
   return NS_OK;
 }
 
 /***********************************************************************
  * WebAuthnManager Implementation
  **********************************************************************/
 
--- a/dom/webauthn/WebAuthnManager.h
+++ b/dom/webauthn/WebAuthnManager.h
@@ -36,16 +36,23 @@
  * - On return of successful transaction information from parent process, turn
  *   information into DOM object format required by spec, and resolve promise
  *   (by running the Finish* functions of WebAuthnManager). On cancellation
  *   request from parent, reject promise with corresponding error code. Either
  *   outcome will also close the IPC channel.
  *
  */
 
+// Forward decl because of nsHTMLDocument.h's complex dependency on /layout/style
+class nsHTMLDocument {
+public:
+  bool IsRegistrableDomainSuffixOfOrEqualTo(const nsAString& aHostSuffixString,
+                                            const nsACString& aOrigHost);
+};
+
 namespace mozilla {
 namespace dom {
 
 struct Account;
 class ArrayBufferViewOrArrayBuffer;
 struct AssertionOptions;
 class OwningArrayBufferViewOrArrayBuffer;
 struct ScopedCredentialOptions;
--- a/dom/webauthn/tests/test_webauthn_loopback.html
+++ b/dom/webauthn/tests/test_webauthn_loopback.html
@@ -99,17 +99,17 @@ function() {
     })
     .then(function(aSignedData) {
       console.log(aPublicKey, aSignedData, signatureValue);
       return verifySignature(aPublicKey, aSignedData, signatureValue);
     })
   }
 
   function testMakeCredential() {
-    let rp = {id: document.origin, name: "none", icon: "none"};
+    let rp = {id: document.domain, name: "none", icon: "none"};
     let user = {id: "none", name: "none", icon: "none", displayName: "none"};
     let param = {type: "public-key", algorithm: "P-256"};
     let makeCredentialOptions = {
       rp: rp,
       user: user,
       challenge: gCredentialChallenge,
       parameters: [param]
     };
@@ -118,17 +118,17 @@ function() {
     .then(testMakeDuplicate)
     .catch(function(aReason) {
       ok(false, aReason);
       SimpleTest.finish();
     });
   }
 
   function testMakeDuplicate(aCredInfo) {
-    let rp = {id: document.origin, name: "none", icon: "none"};
+    let rp = {id: document.domain, name: "none", icon: "none"};
     let user = {id: "none", name: "none", icon: "none", displayName: "none"};
     let param = {type: "public-key", algorithm: "P-256"};
     let makeCredentialOptions = {
       rp: rp,
       user: user,
       challenge: gCredentialChallenge,
       parameters: [param],
       excludeList: [{type: "public-key", id: Uint8Array.from(aCredInfo.rawId),
@@ -151,17 +151,17 @@ function() {
       type: "public-key",
       id: Uint8Array.from(aCredInfo.rawId),
       transports: ["usb"],
     }
 
     let publicKeyCredentialRequestOptions = {
       challenge: gAssertionChallenge,
       timeout: 5000, // the minimum timeout is actually 15 seconds
-      rpId: document.origin,
+      rpId: document.domain,
       allowList: [newCredential]
     };
     credm.get({publicKey: publicKeyCredentialRequestOptions})
     .then(function(aAssertion) {
       /* Pass along the pubKey. */
       return checkAssertionAndSigValid(aCredInfo.u2fReg.publicKey, aAssertion);
     })
     .then(function(aSigVerifyResult) {
--- a/dom/webauthn/tests/test_webauthn_make_credential.html
+++ b/dom/webauthn/tests/test_webauthn_make_credential.html
@@ -55,17 +55,17 @@
       isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist");
       isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist");
 
       let credm = navigator.credentials;
 
       let gCredentialChallenge = new Uint8Array(16);
       window.crypto.getRandomValues(gCredentialChallenge);
 
-      let rp = {id: "none", name: "none", icon: "none"};
+      let rp = {id: document.domain, name: "none", icon: "none"};
       let user = {id: "none", name: "none", icon: "none", displayName: "none"};
       let param = {type: "public-key", algorithm: "p-256"};
       let unsupportedParam = {type: "public-key", algorithm: "3DES"};
       let badParam = {type: "SimplePassword", algorithm: "MaxLength=2"};
 
       var testFuncs = [
         // Test basic good call
         function() {
@@ -210,17 +210,17 @@
           };
           return credm.create({publicKey: makeCredentialOptions})
                       .then(arrivingHereIsBad)
                       .catch(expectTypeError);
         },
 
         // Test a complete account
         function() {
-          let completeRP = {id: "Foxxy RP", name: "Foxxy Name",
+          let completeRP = {id: document.domain, name: "Foxxy Name",
                             icon: "https://example.com/fox.svg"};
           let completeUser = {id: "foxes_are_the_best@example.com",
                               name: "Fox F. Foxington",
                               icon: "https://example.com/fox.svg",
                               displayName: "Foxxy V"};
           let makeCredentialOptions = {
             rp: completeRP, user: completeUser, challenge: gCredentialChallenge,
             parameters: [param]
--- a/dom/webauthn/tests/test_webauthn_no_token.html
+++ b/dom/webauthn/tests/test_webauthn_no_token.html
@@ -38,17 +38,17 @@ function() {
   let assertionChallenge = new Uint8Array(16);
   window.crypto.getRandomValues(assertionChallenge);
   let credentialId = new Uint8Array(128);
   window.crypto.getRandomValues(credentialId);
 
   testMakeCredential();
 
   function testMakeCredential() {
-    let rp = {id: "none", name: "none", icon: "none"};
+    let rp = {id: document.domain, name: "none", icon: "none"};
     let user = {id: "none", name: "none", icon: "none", displayName: "none"};
     let param = {type: "public-key", algorithm: "p-256"};
     let makeCredentialOptions = {
       rp: rp, user: user, challenge: credentialChallenge, parameters: [param]
     };
     credm.create({publicKey: makeCredentialOptions})
     .then(function(aResult) {
       ok(false, "Should have failed.");
@@ -64,17 +64,17 @@ function() {
     let newCredential = {
       type: "public-key",
       id: credentialId,
       transports: ["usb"],
     }
     let publicKeyCredentialRequestOptions = {
       challenge: assertionChallenge,
       timeout: 5000, // the minimum timeout is actually 15 seconds
-      rpId: document.origin,
+      rpId: document.domain,
       allowList: [newCredential]
     };
     credm.get({publicKey: publicKeyCredentialRequestOptions})
     .then(function(aResult) {
       ok(false, "Should have failed.");
       SimpleTest.finish();
     })
     .catch(function(aReason) {
--- a/dom/webauthn/tests/test_webauthn_sameorigin.html
+++ b/dom/webauthn/tests/test_webauthn_sameorigin.html
@@ -24,24 +24,22 @@
     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);
+      ok(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");
+      ok(aResult.toString().startsWith("SecurityError"), "Expecting a SecurityError");
       return Promise.resolve();
     }
 
     function keepThisPublicKeyCredential(aPublicKeyCredential) {
       gTrackedCredential = {
         type: "public-key",
         id: Uint8Array.from(aPublicKeyCredential.rawId),
         transports: [ "usb" ],
@@ -61,17 +59,17 @@
       window.crypto.getRandomValues(chall);
 
       let user = {id: "none", name: "none", icon: "none", displayName: "none"};
       let param = {type: "public-key", algorithm: "p-256"};
 
       var testFuncs = [
         function() {
           // Test basic good call
-          let rp = {id: document.origin};
+          let rp = {id: document.domain};
           let makeCredentialOptions = {
             rp: rp, user: user, challenge: chall, parameters: [param]
           };
           return credm.create({publicKey: makeCredentialOptions})
                       .then(keepThisPublicKeyCredential)
                       .then(arrivingHereIsGood)
                       .catch(arrivingHereIsBad);
         },
@@ -82,17 +80,17 @@
             rp: {}, user: user, challenge: chall, parameters: [param]
           };
           return credm.create({publicKey: makeCredentialOptions})
                       .then(arrivingHereIsGood)
                       .catch(arrivingHereIsBad);
         },
         function() {
           // Test this origin with optional fields
-          let rp = {id: "user:pass@" + document.origin + ":8888"};
+          let rp = {id: "user:pass@" + document.domain + ":8888"};
           let makeCredentialOptions = {
             rp: rp, user: user, challenge: chall, parameters: [param]
           };
           return credm.create({publicKey: makeCredentialOptions})
                       .then(arrivingHereIsBad)
                       .catch(expectSecurityError);
         },
         function() {
@@ -102,31 +100,41 @@
             rp: rp, user: user, challenge: chall, parameters: [param]
           };
           return credm.create({publicKey: makeCredentialOptions})
                       .then(arrivingHereIsBad)
                       .catch(expectSecurityError);
         },
         function() {
           // Test subdomain of this origin
-          let rp = {id: "subdomain." + document.origin};
+          let rp = {id: "subdomain." + document.domain};
           let makeCredentialOptions = {
             rp: rp, user: user, challenge: chall, parameters: [param]
           };
           return credm.create({publicKey: makeCredentialOptions})
                       .then(arrivingHereIsBad)
                       .catch(expectSecurityError);
         },
         function() {
-          // Test another origin
+          // Test the same origin
           let rp = {id: "example.com"};
           let makeCredentialOptions = {
             rp: rp, user: user, challenge: chall, parameters: [param]
           };
           return credm.create({publicKey: makeCredentialOptions})
+                      .then(arrivingHereIsGood)
+                      .catch(arrivingHereIsBad);
+        },
+        function() {
+          // Test the eTLD
+          let rp = {id: "com"};
+          let makeCredentialOptions = {
+            rp: rp, user: user, challenge: chall, parameters: [param]
+          };
+          return credm.create({publicKey: makeCredentialOptions})
                       .then(arrivingHereIsBad)
                       .catch(expectSecurityError);
         },
         function () {
           // Test a different domain within the same TLD
           let rp = {id: "alt.test"};
           let makeCredentialOptions = {
             rp: rp, user: user, challenge: chall, parameters: [param]
@@ -134,17 +142,17 @@
           return credm.create({publicKey: makeCredentialOptions})
                       .then(arrivingHereIsBad)
                       .catch(expectSecurityError);
         },
         function () {
           // Test basic good call
           let publicKeyCredentialRequestOptions = {
             challenge: chall,
-            rpId: document.origin,
+            rpId: document.domain,
             allowList: [gTrackedCredential]
           };
           return credm.get({publicKey: publicKeyCredentialRequestOptions})
                       .then(arrivingHereIsGood)
                       .catch(arrivingHereIsBad);
         },
         function () {
           // Test rpId being unset
@@ -177,44 +185,55 @@
           return credm.get({publicKey: publicKeyCredentialRequestOptions})
                       .then(arrivingHereIsBad)
                       .catch(expectSecurityError);
         },
         function () {
           // Test subdomain of this origin
           let publicKeyCredentialRequestOptions = {
             challenge: chall,
-            rpId: "subdomain." + document.origin,
+            rpId: "subdomain." + document.domain,
             allowList: [gTrackedCredential]
           };
           return credm.get({publicKey: publicKeyCredentialRequestOptions})
                       .then(arrivingHereIsBad)
                       .catch(expectSecurityError);
         },
         function () {
-          // Test another origin
+          // Test the same origin
           let publicKeyCredentialRequestOptions = {
             challenge: chall,
             rpId: "example.com",
             allowList: [gTrackedCredential]
           };
           return credm.get({publicKey: publicKeyCredentialRequestOptions})
+                      .then(arrivingHereIsGood)
+                      .catch(arrivingHereIsBad);
+        },
+        function() {
+          // Test the eTLD
+          let publicKeyCredentialRequestOptions = {
+            challenge: chall,
+            rpId: "com",
+            allowList: [gTrackedCredential]
+          };
+          return credm.get({publicKey: publicKeyCredentialRequestOptions})
                       .then(arrivingHereIsBad)
                       .catch(expectSecurityError);
         },
         function () {
           // Test a different domain within the same TLD
           let publicKeyCredentialRequestOptions = {
             challenge: chall,
             rpId: "alt.test",
             allowList: [gTrackedCredential]
           };
           return credm.get({publicKey: publicKeyCredentialRequestOptions})
                       .then(arrivingHereIsBad)
-                      .catch(expectSecurityError)
+                      .catch(expectSecurityError);
         },
         function () {
           SimpleTest.finish();
         }
       ];
       var i = 0;
       var runNextTest = () => {
         if (i == testFuncs.length) {