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
--- 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) {