Bug 1430150 - Implement WebAuthentication permission prompts r=jcj,johannh

Reviewers: jcj, johannh

Reviewed By: jcj, johannh

Bug #: 1430150

Differential Revision: https://phabricator.services.mozilla.com/D638
This commit is contained in:
Tim Taubert
2018-03-11 18:47:14 +01:00
parent e8b407b808
commit 28467af293
24 changed files with 688 additions and 101 deletions

View File

@@ -1367,6 +1367,10 @@ toolbarpaletteitem[place="palette"][hidden] {
box-shadow: 0 0 2px 2px rgba(255,0,0,0.4);
}
.popup-notification-description[popupid=webauthn-prompt-register-direct] {
white-space: pre-line;
}
.dragfeedback-tab {
-moz-appearance: none;
opacity: 0.65;

View File

@@ -1467,6 +1467,7 @@ var gBrowserInit = {
BrowserOffline.init();
IndexedDBPromptHelper.init();
CanvasPermissionPromptHelper.init();
WebAuthnPromptHelper.init();
// Initialize the full zoom setting.
// We do this before the session restore service gets initialized so we can
@@ -1937,6 +1938,7 @@ var gBrowserInit = {
BrowserOffline.uninit();
IndexedDBPromptHelper.uninit();
CanvasPermissionPromptHelper.uninit();
WebAuthnPromptHelper.uninit();
PanelUI.uninit();
AutoShowBookmarksToolbar.uninit();
}
@@ -6769,6 +6771,133 @@ var CanvasPermissionPromptHelper = {
}
};
var WebAuthnPromptHelper = {
_icon: "default-notification-icon",
_topic: "webauthn-prompt",
// The current notification, if any. The U2F manager is a singleton, we will
// never allow more than one active request. And thus we'll never have more
// than one notification either.
_current: null,
// The current transaction ID. Will be checked when we're notified of the
// cancellation of an ongoing WebAuthhn request.
_tid: 0,
init() {
Services.obs.addObserver(this, this._topic);
},
uninit() {
Services.obs.removeObserver(this, this._topic);
},
observe(aSubject, aTopic, aData) {
let mgr = aSubject.QueryInterface(Ci.nsIU2FTokenManager);
let data = JSON.parse(aData);
if (data.action == "register") {
this.register(mgr, data);
} else if (data.action == "register-direct") {
this.registerDirect(mgr, data);
} else if (data.action == "sign") {
this.sign(mgr, data);
} else if (data.action == "cancel") {
this.cancel(data);
}
},
register(mgr, {origin, tid}) {
let mainAction = this.buildCancelAction(mgr, tid);
this.show(tid, "register", "webauthn.registerPrompt", origin, mainAction);
},
registerDirect(mgr, {origin, tid}) {
let mainAction = this.buildProceedAction(mgr, tid);
let secondaryActions = [this.buildCancelAction(mgr, tid)];
let learnMoreURL =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"webauthn-direct-attestation";
let options = {
learnMoreURL,
checkbox: {
label: gNavigatorBundle.getString("webauthn.anonymize")
}
};
this.show(tid, "register-direct", "webauthn.registerDirectPrompt",
origin, mainAction, secondaryActions, options);
},
sign(mgr, {origin, tid}) {
let mainAction = this.buildCancelAction(mgr, tid);
this.show(tid, "sign", "webauthn.signPrompt", origin, mainAction);
},
show(tid, id, stringId, origin, mainAction, secondaryActions = [], options = {}) {
this.reset();
try {
origin = Services.io.newURI(origin).asciiHost;
} catch (e) {
/* Might fail for arbitrary U2F RP IDs. */
}
let brandShortName =
document.getElementById("bundle_brand").getString("brandShortName");
let message =
gNavigatorBundle.getFormattedString(stringId, ["<>", brandShortName], 1);
options.name = origin;
options.hideClose = true;
options.eventCallback = event => {
if (event == "removed") {
this._current = null;
this._tid = 0;
}
};
this._tid = tid;
this._current = PopupNotifications.show(
gBrowser.selectedBrowser, `webauthn-prompt-${id}`, message,
this._icon, mainAction, secondaryActions, options);
},
cancel({tid}) {
if (this._tid == tid) {
this.reset();
}
},
reset() {
if (this._current) {
this._current.remove();
}
},
buildProceedAction(mgr, tid) {
return {
label: gNavigatorBundle.getString("webauthn.proceed"),
accessKey: gNavigatorBundle.getString("webauthn.proceed.accesskey"),
callback(state) {
mgr.resumeRegister(tid, !state.checkboxChecked);
}
};
},
buildCancelAction(mgr, tid) {
return {
label: gNavigatorBundle.getString("webauthn.cancel"),
accessKey: gNavigatorBundle.getString("webauthn.cancel.accesskey"),
callback() {
mgr.cancel(tid);
}
};
},
};
function CanCloseWindow() {
// Avoid redundant calls to canClose from showing multiple
// PermitUnload dialogs.

View File

@@ -216,6 +216,7 @@
@RESPATH@/components/dom_security.xpt
@RESPATH@/components/dom_sidebar.xpt
@RESPATH@/components/dom_storage.xpt
@RESPATH@/components/dom_webauthn.xpt
#ifdef MOZ_WEBSPEECH
@RESPATH@/components/dom_webspeechrecognition.xpt
#endif

View File

@@ -504,6 +504,28 @@ canvas.allow=Allow Data Access
canvas.allow.accesskey=A
canvas.remember=Always remember my decision
# WebAuthn prompts
# LOCALIZATION NOTE (webauthn.registerPrompt): %S is hostname
webauthn.registerPrompt=%S wants to register an account with one of your security tokens. You can connect and authorize one now, or cancel.
# LOCALIZATION NOTE (webauthn.registerDirectPrompt):
# %1$S is hostname. %2$S is brandShortName.
# The website is asking for extended information about your
# hardware authenticator that shouldn't be generally necessary. Permitting
# this is safe if you only use one account at this website. If you have
# multiple accounts at this website, and you use the same hardware
# authenticator, then the website could link those accounts together.
# And this is true even if you use a different profile / browser (or even Tor
# Browser). To avoid this, you should use different hardware authenticators
# for different accounts on this website.
webauthn.registerDirectPrompt=%1$S is requesting extended information about your authenticator, which may affect your privacy.\n\n%2$S can anonymize this for you, but the website might decline this authenticator. If declined, you can try again.
# LOCALIZATION NOTE (webauthn.signPrompt): %S is hostname
webauthn.signPrompt=%S wants to authenticate you using a registered security token. You can connect and authorize one now, or cancel.
webauthn.cancel=Cancel
webauthn.cancel.accesskey=c
webauthn.proceed=Proceed
webauthn.proceed.accesskey=p
webauthn.anonymize=Anonymize anyway
# Spoof Accept-Language prompt
privacy.spoof_english=Changing your language setting to English will make you more difficult to identify and enhance your privacy. Do you want to request English language versions of web pages?

View File

@@ -342,12 +342,14 @@ U2F::Register(const nsAString& aAppId,
uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);
WebAuthnMakeCredentialInfo info(rpIdHash,
WebAuthnMakeCredentialInfo info(mOrigin,
rpIdHash,
clientDataHash,
adjustedTimeoutMillis,
excludeList,
extensions,
authSelection);
authSelection,
false /* RequestDirectAttestation */);
MOZ_ASSERT(mTransaction.isNothing());
mTransaction = Some(U2FTransaction(clientData, Move(AsVariant(callback))));
@@ -483,7 +485,8 @@ U2F::Sign(const nsAString& aAppId,
uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);
WebAuthnGetAssertionInfo info(rpIdHash,
WebAuthnGetAssertionInfo info(mOrigin,
rpIdHash,
clientDataHash,
adjustedTimeoutMillis,
permittedList,

View File

@@ -47,19 +47,23 @@ union WebAuthnExtensionResult {
};
struct WebAuthnMakeCredentialInfo {
nsString Origin;
uint8_t[] RpIdHash;
uint8_t[] ClientDataHash;
uint32_t TimeoutMS;
WebAuthnScopedCredential[] ExcludeList;
WebAuthnExtension[] Extensions;
WebAuthnAuthenticatorSelection AuthenticatorSelection;
bool RequestDirectAttestation;
};
struct WebAuthnMakeCredentialResult {
uint8_t[] RegBuffer;
bool DirectAttestationPermitted;
};
struct WebAuthnGetAssertionInfo {
nsString Origin;
uint8_t[] RpIdHash;
uint8_t[] ClientDataHash;
uint32_t TimeoutMS;

View File

@@ -224,7 +224,9 @@ U2FHIDTokenManager::HandleRegisterResult(UniquePtr<U2FResult>&& aResult)
return;
}
WebAuthnMakeCredentialResult result(registration);
// Will be set by the U2FTokenManager.
bool directAttestationPermitted = false;
WebAuthnMakeCredentialResult result(registration, directAttestationPermitted);
mRegisterPromise.Resolve(Move(result), __func__);
}

View File

@@ -688,7 +688,10 @@ U2FSoftTokenManager::Register(const WebAuthnMakeCredentialInfo& aInfo)
registrationBuf.AppendSECItem(attestCert.get()->derCert);
registrationBuf.AppendSECItem(signatureItem);
WebAuthnMakeCredentialResult result((nsTArray<uint8_t>(registrationBuf)));
// Will be set by the U2FTokenManager.
bool directAttestationPermitted = false;
WebAuthnMakeCredentialResult result((nsTArray<uint8_t>(registrationBuf)),
directAttestationPermitted);
return U2FRegisterPromise::CreateAndResolve(Move(result), __func__);
}

View File

@@ -11,10 +11,12 @@
#include "mozilla/dom/PWebAuthnTransactionParent.h"
#include "mozilla/MozPromise.h"
#include "mozilla/dom/WebAuthnUtil.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Unused.h"
#include "hasht.h"
#include "nsICryptoHash.h"
#include "nsTextFormatter.h"
#include "pkix/Input.h"
#include "pkixutil.h"
@@ -24,6 +26,7 @@
#define PREF_U2F_NSSTOKEN_COUNTER "security.webauth.softtoken_counter"
#define PREF_WEBAUTHN_SOFTTOKEN_ENABLED "security.webauth.webauthn_enable_softtoken"
#define PREF_WEBAUTHN_USBTOKEN_ENABLED "security.webauth.webauthn_enable_usbtoken"
#define PREF_WEBAUTHN_ALLOW_DIRECT_ATTESTATION "security.webauth.webauthn_testing_allow_direct_attestation"
namespace mozilla {
namespace dom {
@@ -38,8 +41,19 @@ namespace {
static mozilla::LazyLogModule gU2FTokenManagerLog("u2fkeymanager");
StaticRefPtr<U2FTokenManager> gU2FTokenManager;
StaticRefPtr<U2FPrefManager> gPrefManager;
static nsIThread* gBackgroundThread;
}
// Data for WebAuthn UI prompt notifications.
static const char16_t kRegisterPromptNotifcation[] =
u"{\"action\":\"register\",\"tid\":%llu,\"origin\":\"%s\"}";
static const char16_t kRegisterDirectPromptNotifcation[] =
u"{\"action\":\"register-direct\",\"tid\":%llu,\"origin\":\"%s\"}";
static const char16_t kSignPromptNotifcation[] =
u"{\"action\":\"sign\",\"tid\":%llu,\"origin\":\"%s\"}";
static const char16_t kCancelPromptNotifcation[] =
u"{\"action\":\"cancel\",\"tid\":%llu}";
class U2FPrefManager final : public nsIObserver
{
private:
@@ -61,6 +75,7 @@ public:
Preferences::AddStrongObserver(gPrefManager, PREF_WEBAUTHN_SOFTTOKEN_ENABLED);
Preferences::AddStrongObserver(gPrefManager, PREF_U2F_NSSTOKEN_COUNTER);
Preferences::AddStrongObserver(gPrefManager, PREF_WEBAUTHN_USBTOKEN_ENABLED);
Preferences::AddStrongObserver(gPrefManager, PREF_WEBAUTHN_ALLOW_DIRECT_ATTESTATION);
ClearOnShutdown(&gPrefManager, ShutdownPhase::ShutdownThreads);
}
return gPrefManager;
@@ -89,6 +104,12 @@ public:
return mUsbTokenEnabled;
}
bool GetAllowDirectAttestationForTesting()
{
MutexAutoLock lock(mPrefMutex);
return mAllowDirectAttestation;
}
NS_IMETHODIMP
Observe(nsISupports* aSubject,
const char* aTopic,
@@ -104,12 +125,14 @@ private:
mSoftTokenEnabled = Preferences::GetBool(PREF_WEBAUTHN_SOFTTOKEN_ENABLED);
mSoftTokenCounter = Preferences::GetUint(PREF_U2F_NSSTOKEN_COUNTER);
mUsbTokenEnabled = Preferences::GetBool(PREF_WEBAUTHN_USBTOKEN_ENABLED);
mAllowDirectAttestation = Preferences::GetBool(PREF_WEBAUTHN_ALLOW_DIRECT_ATTESTATION);
}
Mutex mPrefMutex;
bool mSoftTokenEnabled;
int mSoftTokenCounter;
bool mUsbTokenEnabled;
bool mAllowDirectAttestation;
};
NS_IMPL_ISUPPORTS(U2FPrefManager, nsIObserver);
@@ -118,6 +141,8 @@ NS_IMPL_ISUPPORTS(U2FPrefManager, nsIObserver);
* U2FManager Implementation
**********************************************************************/
NS_IMPL_ISUPPORTS(U2FTokenManager, nsIU2FTokenManager);
U2FTokenManager::U2FTokenManager()
: mTransactionParent(nullptr)
, mLastTransactionId(0)
@@ -129,11 +154,6 @@ U2FTokenManager::U2FTokenManager()
U2FPrefManager::GetOrCreate();
}
U2FTokenManager::~U2FTokenManager()
{
MOZ_ASSERT(NS_IsMainThread());
}
//static
void
U2FTokenManager::Initialize()
@@ -178,28 +198,75 @@ U2FTokenManager::MaybeClearTransaction(PWebAuthnTransactionParent* aParent)
void
U2FTokenManager::ClearTransaction()
{
if (mLastTransactionId > 0) {
// Remove any prompts we might be showing for the current transaction.
SendPromptNotification(kCancelPromptNotifcation, mLastTransactionId);
}
mTransactionParent = nullptr;
// Drop managers at the end of all transactions
if (mTokenManagerImpl) {
mTokenManagerImpl->Drop();
mTokenManagerImpl = nullptr;
}
// Forget promises, if necessary.
mRegisterPromise.DisconnectIfExists();
mSignPromise.DisconnectIfExists();
// Clear transaction id.
mLastTransactionId = 0;
// Forget any pending registration.
mPendingRegisterInfo.reset();
}
template<typename ...T> void
U2FTokenManager::SendPromptNotification(const char16_t* aFormat, T... aArgs)
{
mozilla::ipc::AssertIsOnBackgroundThread();
nsAutoString json;
nsTextFormatter::ssprintf(json, aFormat, aArgs...);
nsCOMPtr<nsIRunnable> r(NewRunnableMethod<nsString>(
"U2FTokenManager::RunSendPromptNotification", this,
&U2FTokenManager::RunSendPromptNotification, json));
MOZ_ALWAYS_SUCCEEDS(
GetMainThreadEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL));
}
void
U2FTokenManager::RunSendPromptNotification(nsString aJSON)
{
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIObserverService> os = services::GetObserverService();
if (NS_WARN_IF(!os)) {
return;
}
nsCOMPtr<nsIU2FTokenManager> self = do_QueryInterface(this);
MOZ_ALWAYS_SUCCEEDS(os->NotifyObservers(self, "webauthn-prompt", aJSON.get()));
}
RefPtr<U2FTokenTransport>
U2FTokenManager::GetTokenManagerImpl()
{
MOZ_ASSERT(U2FPrefManager::Get());
mozilla::ipc::AssertIsOnBackgroundThread();
if (mTokenManagerImpl) {
return mTokenManagerImpl;
}
if (!gBackgroundThread) {
gBackgroundThread = NS_GetCurrentThread();
MOZ_ASSERT(gBackgroundThread, "This should never be null!");
}
auto pm = U2FPrefManager::Get();
// Prefer the HW token, even if the softtoken is enabled too.
@@ -246,13 +313,50 @@ U2FTokenManager::Register(PWebAuthnTransactionParent* aTransactionParent,
return;
}
uint64_t tid = mLastTransactionId = aTransactionId;
mLastTransactionId = aTransactionId;
// If the RP request direct attestation, ask the user for permission and
// store the transaction info until the user proceeds or cancels.
// Might be overriden by a pref for testing purposes.
if (aTransactionInfo.RequestDirectAttestation() &&
!U2FPrefManager::Get()->GetAllowDirectAttestationForTesting()) {
NS_ConvertUTF16toUTF8 origin(aTransactionInfo.Origin());
SendPromptNotification(kRegisterDirectPromptNotifcation,
aTransactionId,
origin.get());
MOZ_ASSERT(mPendingRegisterInfo.isNothing());
mPendingRegisterInfo = Some(aTransactionInfo);
} else {
DoRegister(aTransactionInfo);
}
}
void
U2FTokenManager::DoRegister(const WebAuthnMakeCredentialInfo& aInfo)
{
mozilla::ipc::AssertIsOnBackgroundThread();
MOZ_ASSERT(mLastTransactionId > 0);
// Show a prompt that lets the user cancel the ongoing transaction.
NS_ConvertUTF16toUTF8 origin(aInfo.Origin());
SendPromptNotification(kRegisterPromptNotifcation,
mLastTransactionId,
origin.get());
uint64_t tid = mLastTransactionId;
mozilla::TimeStamp startTime = mozilla::TimeStamp::Now();
bool requestDirectAttestation = aInfo.RequestDirectAttestation();
mTokenManagerImpl
->Register(aTransactionInfo)
->Register(aInfo)
->Then(GetCurrentThreadSerialEventTarget(), __func__,
[tid, startTime](WebAuthnMakeCredentialResult&& aResult) {
[tid, startTime, requestDirectAttestation](WebAuthnMakeCredentialResult&& aResult) {
U2FTokenManager* mgr = U2FTokenManager::Get();
// The token manager implementations set DirectAttestationPermitted
// to false by default. Override this here with information from
// the JS prompt.
aResult.DirectAttestationPermitted() = requestDirectAttestation;
mgr->MaybeConfirmRegister(tid, aResult);
Telemetry::ScalarAdd(
Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
@@ -314,8 +418,15 @@ U2FTokenManager::Sign(PWebAuthnTransactionParent* aTransactionParent,
return;
}
// Show a prompt that lets the user cancel the ongoing transaction.
NS_ConvertUTF16toUTF8 origin(aTransactionInfo.Origin());
SendPromptNotification(kSignPromptNotifcation,
aTransactionId,
origin.get());
uint64_t tid = mLastTransactionId = aTransactionId;
mozilla::TimeStamp startTime = mozilla::TimeStamp::Now();
mTokenManagerImpl
->Sign(aTransactionInfo)
->Then(GetCurrentThreadSerialEventTarget(), __func__,
@@ -372,5 +483,82 @@ U2FTokenManager::Cancel(PWebAuthnTransactionParent* aParent,
ClearTransaction();
}
// nsIU2FTokenManager
NS_IMETHODIMP
U2FTokenManager::ResumeRegister(uint64_t aTransactionId,
bool aPermitDirectAttestation)
{
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_ASSERT(NS_IsMainThread());
if (!gBackgroundThread) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t, bool>(
"U2FTokenManager::RunResumeRegister", this,
&U2FTokenManager::RunResumeRegister, aTransactionId,
aPermitDirectAttestation));
return gBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
}
void
U2FTokenManager::RunResumeRegister(uint64_t aTransactionId,
bool aPermitDirectAttestation)
{
mozilla::ipc::AssertIsOnBackgroundThread();
if (NS_WARN_IF(mPendingRegisterInfo.isNothing())) {
return;
}
if (mLastTransactionId != aTransactionId) {
return;
}
// Forward whether the user opted into direct attestation.
mPendingRegisterInfo.ref().RequestDirectAttestation() =
aPermitDirectAttestation;
// Resume registration and cleanup.
DoRegister(mPendingRegisterInfo.ref());
mPendingRegisterInfo.reset();
}
NS_IMETHODIMP
U2FTokenManager::Cancel(uint64_t aTransactionId)
{
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_ASSERT(NS_IsMainThread());
if (!gBackgroundThread) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t>(
"U2FTokenManager::RunCancel", this,
&U2FTokenManager::RunCancel, aTransactionId));
return gBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
}
void
U2FTokenManager::RunCancel(uint64_t aTransactionId)
{
mozilla::ipc::AssertIsOnBackgroundThread();
if (mLastTransactionId != aTransactionId) {
return;
}
// Cancel the request.
mTokenManagerImpl->Cancel();
// Reject the promise.
AbortTransaction(aTransactionId, NS_ERROR_DOM_ABORT_ERR);
}
}
}

View File

@@ -7,6 +7,7 @@
#ifndef mozilla_dom_U2FTokenManager_h
#define mozilla_dom_U2FTokenManager_h
#include "nsIU2FTokenManager.h"
#include "mozilla/dom/U2FTokenTransport.h"
#include "mozilla/dom/PWebAuthnTransaction.h"
@@ -26,10 +27,12 @@ namespace dom {
class U2FSoftTokenManager;
class WebAuthnTransactionParent;
class U2FTokenManager final
class U2FTokenManager final : public nsIU2FTokenManager
{
public:
NS_INLINE_DECL_REFCOUNTING(U2FTokenManager)
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIU2FTOKENMANAGER
static U2FTokenManager* Get();
void Register(PWebAuthnTransactionParent* aTransactionParent,
const uint64_t& aTransactionId,
@@ -43,16 +46,27 @@ public:
static void Initialize();
private:
U2FTokenManager();
~U2FTokenManager();
~U2FTokenManager() { }
RefPtr<U2FTokenTransport> GetTokenManagerImpl();
void AbortTransaction(const uint64_t& aTransactionId, const nsresult& aError);
void ClearTransaction();
// Step two of "Register", kicking off the actual transaction.
void DoRegister(const WebAuthnMakeCredentialInfo& aInfo);
void MaybeConfirmRegister(const uint64_t& aTransactionId,
const WebAuthnMakeCredentialResult& aResult);
void MaybeAbortRegister(const uint64_t& aTransactionId, const nsresult& aError);
void MaybeConfirmSign(const uint64_t& aTransactionId,
const WebAuthnGetAssertionResult& aResult);
void MaybeAbortSign(const uint64_t& aTransactionId, const nsresult& aError);
// The main thread runnable function for "nsIU2FTokenManager.ResumeRegister".
void RunResumeRegister(uint64_t aTransactionId, bool aPermitDirectAttestation);
// The main thread runnable function for "nsIU2FTokenManager.Cancel".
void RunCancel(uint64_t aTransactionId);
// Sends a "webauthn-prompt" observer notification with the given data.
template<typename ...T>
void SendPromptNotification(const char16_t* aFormat, T... aArgs);
// The main thread runnable function for "SendPromptNotification".
void RunSendPromptNotification(nsString aJSON);
// Using a raw pointer here, as the lifetime of the IPC object is managed by
// the PBackground protocol code. This means we cannot be left holding an
// invalid IPC protocol object after the transaction is finished.
@@ -64,6 +78,8 @@ private:
// guards any cancel messages to ensure we don't cancel newer transactions
// due to a stale message.
uint64_t mLastTransactionId;
// Pending registration info while we wait for user input.
Maybe<WebAuthnMakeCredentialInfo> mPendingRegisterInfo;
};
} // namespace dom

View File

@@ -408,36 +408,19 @@ WebAuthnManager::MakeCredential(const PublicKeyCredentialCreationOptions& aOptio
bool requestDirectAttestation =
attestation == AttestationConveyancePreference::Direct;
// XXX Bug 1430150. Need something that allows direct attestation
// for tests until we implement a permission dialog we can click.
if (requestDirectAttestation) {
nsresult rv;
nsCOMPtr<nsIPrefService> prefService = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
if (NS_SUCCEEDED(rv)) {
nsCOMPtr<nsIPrefBranch> branch;
rv = prefService->GetBranch("security.webauth.", getter_AddRefs(branch));
if (NS_SUCCEEDED(rv)) {
rv = branch->GetBoolPref("webauthn_testing_allow_direct_attestation",
&requestDirectAttestation);
}
}
requestDirectAttestation &= NS_SUCCEEDED(rv);
}
// Create and forward authenticator selection criteria.
WebAuthnAuthenticatorSelection authSelection(selection.mRequireResidentKey,
requireUserVerification,
requirePlatformAttachment);
WebAuthnMakeCredentialInfo info(rpIdHash,
WebAuthnMakeCredentialInfo info(origin,
rpIdHash,
clientDataHash,
adjustedTimeout,
excludeList,
extensions,
authSelection);
authSelection,
requestDirectAttestation);
ListenForVisibilityEvents();
@@ -455,6 +438,7 @@ WebAuthnManager::MakeCredential(const PublicKeyCredentialCreationOptions& aOptio
signal));
mChild->SendRequestRegister(mTransaction.ref().mId, info);
return promise.forget();
}
@@ -638,7 +622,8 @@ WebAuthnManager::GetAssertion(const PublicKeyCredentialRequestOptions& aOptions,
extensions.AppendElement(WebAuthnExtensionAppId(appIdHash));
}
WebAuthnGetAssertionInfo info(rpIdHash,
WebAuthnGetAssertionInfo info(origin,
rpIdHash,
clientDataHash,
adjustedTimeout,
allowList,
@@ -785,10 +770,10 @@ WebAuthnManager::FinishMakeCredential(const uint64_t& aTransactionId,
return;
}
// Direct attestation might have been requested by the RP. mDirectAttestation
// will be true only if the user consented via the permission UI.
// Direct attestation might have been requested by the RP. This will
// be true only if the user consented via the permission UI.
CryptoBuffer attObj;
if (mTransaction.ref().mDirectAttestation) {
if (aResult.DirectAttestationPermitted()) {
rv = CBOREncodeFidoU2FAttestationObj(authDataBuf, attestationCertBuf,
signatureBuf, attObj);
} else {

View File

@@ -11,6 +11,12 @@ IPDL_SOURCES += [
'PWebAuthnTransaction.ipdl'
]
XPIDL_SOURCES += [
'nsIU2FTokenManager.idl'
]
XPIDL_MODULE = 'dom_webauthn'
EXPORTS.mozilla.dom += [
'AuthenticatorAssertionResponse.h',
'AuthenticatorAttestationResponse.h',

View File

@@ -0,0 +1,38 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
/**
* nsIU2FTokenManager
*
* An interface to the U2FTokenManager singleton.
*
* This should be used only by the WebAuthn browser UI prompts.
*/
[scriptable, uuid(745e1eac-e449-4342-bca1-ee0e6ead09fc)]
interface nsIU2FTokenManager : nsISupports
{
/**
* Resumes the current WebAuthn/U2F transaction if that matches the given
* transaction ID. This is used only when direct attestation was requested
* and we have to wait for user input to proceed.
*
* @param aTransactionID : The ID of the transaction to resume.
* @param aPermitDirectAttestation : Whether direct attestation was
* permitted by the user.
*/
void resumeRegister(in uint64_t aTransactionID,
in bool aPermitDirectAttestation);
/**
* Cancels the current WebAuthn/U2F transaction if that matches the given
* transaction ID.
*
* @param aTransactionID : The ID of the transaction to cancel.
*/
void cancel(in uint64_t aTransactionID);
};

View File

@@ -3,11 +3,12 @@ support-files =
head.js
tab_webauthn_result.html
tab_webauthn_success.html
../cbor/*
../pkijs/*
../cbor.js
../u2futil.js
skip-if = !e10s
[browser_abort_visibility.js]
[browser_fido_appid_extension.js]
[browser_webauthn_prompts.js]
[browser_webauthn_telemetry.js]

View File

@@ -0,0 +1,215 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const TEST_URL = "https://example.com/";
function promiseNotification(id) {
return new Promise(resolve => {
PopupNotifications.panel.addEventListener("popupshown", function shown() {
let notification = PopupNotifications.getNotification(id);
if (notification) {
ok(true, `${id} prompt visible`);
PopupNotifications.panel.removeEventListener("popupshown", shown);
resolve();
}
});
});
}
function arrivingHereIsBad(aResult) {
ok(false, "Bad result! Received a: " + aResult);
}
function expectAbortError(aResult) {
let expected = "AbortError";
is(aResult.slice(0, expected.length), expected, `Expecting a ${expected}`);
}
function verifyAnonymizedCertificate(attestationObject) {
return webAuthnDecodeCBORAttestation(attestationObject)
.then(({fmt, attStmt}) => {
is("none", fmt, "Is a None Attestation");
is("object", typeof(attStmt), "attStmt is a map");
is(0, Object.keys(attStmt).length, "attStmt is empty");
});
}
function verifyDirectCertificate(attestationObject) {
return webAuthnDecodeCBORAttestation(attestationObject)
.then(({fmt, attStmt}) => {
is("fido-u2f", fmt, "Is a FIDO U2F Attestation");
is("object", typeof(attStmt), "attStmt is a map");
ok(attStmt.hasOwnProperty("x5c"), "attStmt.x5c exists");
ok(attStmt.hasOwnProperty("sig"), "attStmt.sig exists");
});
}
function promiseWebAuthnRegister(tab, attestation = "indirect") {
return ContentTask.spawn(tab.linkedBrowser, [attestation],
([attestation]) => {
const cose_alg_ECDSA_w_SHA256 = -7;
let challenge = content.crypto.getRandomValues(new Uint8Array(16));
let pubKeyCredParams = [{
type: "public-key",
alg: cose_alg_ECDSA_w_SHA256
}];
let publicKey = {
rp: {id: content.document.domain, name: "none", icon: "none"},
user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
pubKeyCredParams,
attestation,
challenge
};
return content.navigator.credentials.create({publicKey})
.then(cred => cred.response.attestationObject);
});
}
function promiseWebAuthnSign(tab) {
return ContentTask.spawn(tab.linkedBrowser, [], () => {
let challenge = content.crypto.getRandomValues(new Uint8Array(16));
let key_handle = content.crypto.getRandomValues(new Uint8Array(16));
let credential = {
id: key_handle,
type: "public-key",
transports: ["usb"]
};
let publicKey = {
challenge,
rpId: content.document.domain,
allowCredentials: [credential],
};
return content.navigator.credentials.get({publicKey});
});
}
add_task(async function test_setup_usbtoken() {
await SpecialPowers.pushPrefEnv({
"set": [
["security.webauth.u2f", false],
["security.webauth.webauthn", true],
["security.webauth.webauthn_enable_softtoken", false],
["security.webauth.webauthn_enable_usbtoken", true]
]
});
});
add_task(async function test_register() {
// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Request a new credential and wait for the prompt.
let active = true;
let request = promiseWebAuthnRegister(tab)
.then(arrivingHereIsBad)
.catch(expectAbortError)
.then(() => active = false);
await promiseNotification("webauthn-prompt-register");
// Cancel the request.
ok(active, "request should still be active");
PopupNotifications.panel.firstChild.button.click();
await request;
// Close tab.
await BrowserTestUtils.removeTab(tab);
});
add_task(async function test_sign() {
// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Request a new assertion and wait for the prompt.
let active = true;
let request = promiseWebAuthnSign(tab)
.then(arrivingHereIsBad)
.catch(expectAbortError)
.then(() => active = false);
await promiseNotification("webauthn-prompt-sign");
// Cancel the request.
ok(active, "request should still be active");
PopupNotifications.panel.firstChild.button.click();
await request;
// Close tab.
await BrowserTestUtils.removeTab(tab);
});
add_task(async function test_register_direct_cancel() {
// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Request a new credential with direct attestation and wait for the prompt.
let active = true;
let promise = promiseWebAuthnRegister(tab, "direct")
.then(arrivingHereIsBad).catch(expectAbortError)
.then(() => active = false);
await promiseNotification("webauthn-prompt-register-direct");
// Cancel the request.
ok(active, "request should still be active");
PopupNotifications.panel.firstChild.secondaryButton.click();
await promise;
// Close tab.
await BrowserTestUtils.removeTab(tab);
});
add_task(async function test_setup_softtoken() {
await SpecialPowers.pushPrefEnv({
"set": [
["security.webauth.u2f", false],
["security.webauth.webauthn", true],
["security.webauth.webauthn_enable_softtoken", true],
["security.webauth.webauthn_enable_usbtoken", false]
]
})
});
add_task(async function test_register_direct_proceed() {
// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Request a new credential with direct attestation and wait for the prompt.
let request = promiseWebAuthnRegister(tab, "direct");
await promiseNotification("webauthn-prompt-register-direct");
// Proceed.
PopupNotifications.panel.firstChild.button.click();
// Ensure we got "direct" attestation.
await request.then(verifyDirectCertificate);
// Close tab.
await BrowserTestUtils.removeTab(tab);
});
add_task(async function test_register_direct_proceed_anon() {
// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Request a new credential with direct attestation and wait for the prompt.
let request = promiseWebAuthnRegister(tab, "direct");
await promiseNotification("webauthn-prompt-register-direct");
// Check "anonymize anyway" and proceed.
PopupNotifications.panel.firstChild.checkbox.checked = true;
PopupNotifications.panel.firstChild.button.click();
// Ensure we got "none" attestation.
await request.then(verifyAnonymizedCertificate);
// Close tab.
await BrowserTestUtils.removeTab(tab);
});

View File

@@ -57,6 +57,17 @@ async function executeTestPage(aUri) {
}
}
add_task(async function test_setup() {
await SpecialPowers.pushPrefEnv({
"set": [
["security.webauth.webauthn", true],
["security.webauth.webauthn_enable_softtoken", true],
["security.webauth.webauthn_enable_usbtoken", false],
["security.webauth.webauthn_testing_allow_direct_attestation", true]
]
});
});
add_task(async function test_loopback() {
// These tests can't run simultaneously as the preference changes will race.
// So let's run them sequentially here, but in an async function so we can
@@ -64,12 +75,6 @@ add_task(async function test_loopback() {
const testPage = "https://example.com/browser/dom/webauthn/tests/browser/tab_webauthn_success.html";
{
cleanupTelemetry();
// Enable the soft token, and execute a simple end-to-end test
Services.prefs.setBoolPref("security.webauth.webauthn", true);
Services.prefs.setBoolPref("security.webauth.webauthn_enable_softtoken", true);
Services.prefs.setBoolPref("security.webauth.webauthn_enable_usbtoken", false);
Services.prefs.setBoolPref("security.webauth.webauthn_testing_allow_direct_attestation", true);
await executeTestPage(testPage);
let webauthn_used = getTelemetryForScalar("security.webauthn_used");

View File

@@ -4,48 +4,12 @@
"use strict";
function bytesToBase64(u8a){
let CHUNK_SZ = 0x8000;
let c = [];
for (let i = 0; i < u8a.length; i += CHUNK_SZ) {
c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ)));
}
return window.btoa(c.join(""));
}
function bytesToBase64UrlSafe(buf) {
return bytesToBase64(buf)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
function base64ToBytes(b64encoded) {
return new Uint8Array(window.atob(b64encoded).split("").map(function(c) {
return c.charCodeAt(0);
}));
}
function base64ToBytesUrlSafe(str) {
if (!str || str.length % 4 == 1) {
throw "Improper b64 string";
}
var b64 = str.replace(/\-/g, "+").replace(/\_/g, "/");
while (b64.length % 4 != 0) {
b64 += "=";
}
return base64ToBytes(b64);
}
function buffer2string(buf) {
let str = "";
if (!(buf.constructor === Uint8Array)) {
buf = new Uint8Array(buf);
}
buf.map(function(x){ return str += String.fromCharCode(x) });
return str;
}
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/dom/webauthn/tests/browser/cbor.js",
this);
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/dom/webauthn/tests/browser/u2futil.js",
this);
function memcmp(x, y) {
let xb = new Uint8Array(x);

View File

@@ -7,7 +7,7 @@
<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>
<script type="text/javascript" src="../cbor/cbor.js"></script>
<script type="text/javascript" src="cbor.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>

View File

@@ -1,8 +1,8 @@
[DEFAULT]
support-files =
cbor/*
pkijs/*
cbor.js
u2futil.js
pkijs/*
skip-if = !e10s
scheme = https

View File

@@ -9,7 +9,7 @@
<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>
<script type="text/javascript" src="cbor/cbor.js"></script>
<script type="text/javascript" src="cbor.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>

View File

@@ -8,7 +8,7 @@
<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>
<script type="text/javascript" src="cbor/cbor.js"></script>
<script type="text/javascript" src="cbor.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>

View File

@@ -202,7 +202,7 @@ function webAuthnDecodeAuthDataArray(aAuthData) {
console.log(":: Authenticator Data ::");
console.log("AAGUID: " + hexEncode(attData.aaguid));
cborPubKey = aAuthData.slice(55 + attData.credIdLen);
let cborPubKey = aAuthData.slice(55 + attData.credIdLen);
var pubkeyObj = CBOR.decode(cborPubKey.buffer);
if (!(cose_kty in pubkeyObj && cose_alg in pubkeyObj && cose_crv in pubkeyObj
&& cose_crv_x in pubkeyObj && cose_crv_y in pubkeyObj)) {

View File

@@ -512,11 +512,12 @@
<xul:label class="popup-notification-origin header"
xbl:inherits="value=origin,tooltiptext=origin"
crop="center"/>
<xul:description class="popup-notification-description"
xbl:inherits="popupid">
<!-- These need to be on the same line to avoid creating whitespace between them (whitespace is added in the localization file, if necessary). -->
<html:span xbl:inherits="xbl:text=label,popupid"/><html:b xbl:inherits="xbl:text=name,popupid"/><html:span xbl:inherits="xbl:text=endlabel,popupid"/>
</xul:description>
<!-- These need to be on the same line to avoid creating
whitespace between them (whitespace is added in the
localization file, if necessary). -->
<xul:description class="popup-notification-description" xbl:inherits="popupid"><html:span
xbl:inherits="xbl:text=label,popupid"/><html:b xbl:inherits="xbl:text=name,popupid"/><html:span
xbl:inherits="xbl:text=endlabel,popupid"/></xul:description>
</xul:vbox>
<xul:toolbarbutton anonid="closebutton"
class="messageCloseButton close-icon popup-notification-closebutton tabbable"