mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 06:43:32 +00:00
Bug 1890206 - rework webauthn attestation consent flow. r=bvandersloot
Differential Revision: https://phabricator.services.mozilla.com/D206860
This commit is contained in:
parent
3babf00d0b
commit
c95cff3481
@ -7134,8 +7134,8 @@ var WebAuthnPromptHelper = {
|
||||
|
||||
if (data.prompt.type == "presence") {
|
||||
this.presence_required(mgr, data);
|
||||
} else if (data.prompt.type == "register-direct") {
|
||||
this.registerDirect(mgr, data);
|
||||
} else if (data.prompt.type == "attestation-consent") {
|
||||
this.attestation_consent(mgr, data);
|
||||
} else if (data.prompt.type == "pin-required") {
|
||||
this.pin_required(mgr, false, data);
|
||||
} else if (data.prompt.type == "pin-invalid") {
|
||||
@ -7298,9 +7298,23 @@ var WebAuthnPromptHelper = {
|
||||
);
|
||||
},
|
||||
|
||||
registerDirect(mgr, { origin, tid }) {
|
||||
let mainAction = this.buildProceedAction(mgr, tid);
|
||||
let secondaryActions = [this.buildCancelAction(mgr, tid)];
|
||||
attestation_consent(mgr, { origin, tid }) {
|
||||
let mainAction = {
|
||||
label: gNavigatorBundle.getString("webauthn.allow"),
|
||||
accessKey: gNavigatorBundle.getString("webauthn.allow.accesskey"),
|
||||
callback(_state) {
|
||||
mgr.setHasAttestationConsent(tid, true);
|
||||
},
|
||||
};
|
||||
let secondaryActions = [
|
||||
{
|
||||
label: gNavigatorBundle.getString("webauthn.block"),
|
||||
accessKey: gNavigatorBundle.getString("webauthn.block.accesskey"),
|
||||
callback(_state) {
|
||||
mgr.setHasAttestationConsent(tid, false);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let learnMoreURL =
|
||||
Services.urlFormatter.formatURLPref("app.support.baseURL") +
|
||||
@ -7308,9 +7322,6 @@ var WebAuthnPromptHelper = {
|
||||
|
||||
let options = {
|
||||
learnMoreURL,
|
||||
checkbox: {
|
||||
label: gNavigatorBundle.getString("webauthn.anonymize"),
|
||||
},
|
||||
hintText: "webauthn.registerDirectPromptHint",
|
||||
};
|
||||
this.show(
|
||||
@ -7433,16 +7444,6 @@ var WebAuthnPromptHelper = {
|
||||
}
|
||||
},
|
||||
|
||||
buildProceedAction(mgr, tid) {
|
||||
return {
|
||||
label: gNavigatorBundle.getString("webauthn.proceed"),
|
||||
accessKey: gNavigatorBundle.getString("webauthn.proceed.accesskey"),
|
||||
callback(state) {
|
||||
mgr.resumeMakeCredential(tid, state.checkboxChecked);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
buildCancelAction(mgr, tid) {
|
||||
return {
|
||||
label: gNavigatorBundle.getString("webauthn.cancel"),
|
||||
|
@ -150,9 +150,10 @@ webauthn.uvBlockedPrompt=User verification failed on %S. There were too many fai
|
||||
webauthn.alreadyRegisteredPrompt=This device is already registered. Try a different device.
|
||||
webauthn.cancel=Cancel
|
||||
webauthn.cancel.accesskey=c
|
||||
webauthn.proceed=Proceed
|
||||
webauthn.proceed.accesskey=p
|
||||
webauthn.anonymize=Anonymize anyway
|
||||
webauthn.allow=Allow
|
||||
webauthn.allow.accesskey=A
|
||||
webauthn.block=Block
|
||||
webauthn.block.accesskey=B
|
||||
|
||||
# LOCALIZATION NOTE (identity.identified.verifier, identity.identified.state_and_country, identity.ev.contentOwner2):
|
||||
# %S is the hostname of the site that is being displayed.
|
||||
|
@ -186,12 +186,6 @@ AndroidWebAuthnService::MakeCredential(uint64_t aTransactionId,
|
||||
GetCurrentSerialEventTarget(), __func__,
|
||||
[aPromise, credPropsResponse = std::move(credPropsResponse)](
|
||||
RefPtr<WebAuthnRegisterResult>&& aValue) {
|
||||
// We don't have a way for the user to consent to attestation
|
||||
// on Android, so always anonymize the result.
|
||||
nsresult rv = aValue->Anonymize();
|
||||
if (NS_FAILED(rv)) {
|
||||
aPromise->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
}
|
||||
if (credPropsResponse.isSome()) {
|
||||
Unused << aValue->SetCredPropsRk(credPropsResponse.ref());
|
||||
}
|
||||
@ -357,8 +351,8 @@ AndroidWebAuthnService::PinCallback(uint64_t aTransactionId,
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
AndroidWebAuthnService::ResumeMakeCredential(uint64_t aTransactionId,
|
||||
bool aForceNoneAttestation) {
|
||||
AndroidWebAuthnService::SetHasAttestationConsent(uint64_t aTransactionId,
|
||||
bool aHasConsent) {
|
||||
return NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
|
@ -1121,8 +1121,8 @@ MacOSWebAuthnService::PinCallback(uint64_t aTransactionId,
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
MacOSWebAuthnService::ResumeMakeCredential(uint64_t aTransactionId,
|
||||
bool aForceNoneAttestation) {
|
||||
MacOSWebAuthnService::SetHasAttestationConsent(uint64_t aTransactionId,
|
||||
bool aHasConsent) {
|
||||
return NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
|
@ -155,7 +155,18 @@ WebAuthnRegisterArgs::GetTimeoutMS(uint32_t* aTimeoutMS) {
|
||||
NS_IMETHODIMP
|
||||
WebAuthnRegisterArgs::GetAttestationConveyancePreference(
|
||||
nsAString& aAttestationConveyancePreference) {
|
||||
aAttestationConveyancePreference = mInfo.attestationConveyancePreference();
|
||||
const nsString& attPref = mInfo.attestationConveyancePreference();
|
||||
if (attPref.EqualsLiteral(
|
||||
MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT) ||
|
||||
attPref.EqualsLiteral(
|
||||
MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT) ||
|
||||
attPref.EqualsLiteral(
|
||||
MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE)) {
|
||||
aAttestationConveyancePreference.Assign(attPref);
|
||||
} else {
|
||||
aAttestationConveyancePreference.AssignLiteral(
|
||||
MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE);
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,27 @@ WebAuthnRegisterResult::GetAuthenticatorAttachment(
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
nsresult WebAuthnRegisterResult::Anonymize() {
|
||||
NS_IMETHODIMP
|
||||
WebAuthnRegisterResult::HasIdentifyingAttestation(
|
||||
bool* aHasIdentifyingAttestation) {
|
||||
// Assume the attestation statement is identifying in case the constructor or
|
||||
// the getter below fail.
|
||||
bool isIdentifying = true;
|
||||
|
||||
nsCOMPtr<nsIWebAuthnAttObj> attObj;
|
||||
nsresult rv = authrs_webauthn_att_obj_constructor(mAttestationObject,
|
||||
/* anonymize */ false,
|
||||
getter_AddRefs(attObj));
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
Unused << attObj->IsIdentifying(&isIdentifying);
|
||||
}
|
||||
|
||||
*aHasIdentifyingAttestation = isIdentifying;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
WebAuthnRegisterResult::Anonymize() {
|
||||
// The anonymize flag in the nsIWebAuthnAttObj constructor causes the
|
||||
// attestation statement to be removed during deserialization. It also
|
||||
// causes the AAGUID to be zeroed out. If we can't deserialize the
|
||||
|
@ -134,8 +134,6 @@ class WebAuthnRegisterResult final : public nsIWebAuthnRegisterResult {
|
||||
}
|
||||
#endif
|
||||
|
||||
nsresult Anonymize();
|
||||
|
||||
private:
|
||||
~WebAuthnRegisterResult() = default;
|
||||
|
||||
|
@ -5,7 +5,9 @@
|
||||
#include "mozilla/Services.h"
|
||||
#include "mozilla/StaticPrefs_security.h"
|
||||
#include "nsIObserverService.h"
|
||||
#include "nsTextFormatter.h"
|
||||
#include "nsThreadUtils.h"
|
||||
#include "WebAuthnEnumStrings.h"
|
||||
#include "WebAuthnService.h"
|
||||
#include "WebAuthnTransportIdentifiers.h"
|
||||
|
||||
@ -18,32 +20,139 @@ already_AddRefed<nsIWebAuthnService> NewWebAuthnService() {
|
||||
|
||||
NS_IMPL_ISUPPORTS(WebAuthnService, nsIWebAuthnService)
|
||||
|
||||
void WebAuthnService::ShowAttestationConsentPrompt(
|
||||
const nsString& aOrigin, uint64_t aTransactionId,
|
||||
uint64_t aBrowsingContextId) {
|
||||
RefPtr<WebAuthnService> self = this;
|
||||
#ifdef MOZ_WIDGET_ANDROID
|
||||
// We don't have a way to prompt the user for consent on Android, so just
|
||||
// assume consent not granted.
|
||||
nsCOMPtr<nsIRunnable> runnable(
|
||||
NS_NewRunnableFunction(__func__, [self, aTransactionId]() {
|
||||
self->SetHasAttestationConsent(
|
||||
aTransactionId,
|
||||
StaticPrefs::
|
||||
security_webauth_webauthn_testing_allow_direct_attestation());
|
||||
}));
|
||||
#else
|
||||
nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction(
|
||||
__func__, [self, aOrigin, aTransactionId, aBrowsingContextId]() {
|
||||
if (StaticPrefs::
|
||||
security_webauth_webauthn_testing_allow_direct_attestation()) {
|
||||
self->SetHasAttestationConsent(aTransactionId, true);
|
||||
return;
|
||||
}
|
||||
nsCOMPtr<nsIObserverService> os = services::GetObserverService();
|
||||
if (!os) {
|
||||
return;
|
||||
}
|
||||
const nsLiteralString jsonFmt =
|
||||
u"{\"prompt\": {\"type\":\"attestation-consent\"},"_ns
|
||||
u"\"origin\": \"%S\","_ns
|
||||
u"\"tid\": %llu, \"browsingContextId\": %llu}"_ns;
|
||||
nsString json;
|
||||
nsTextFormatter::ssprintf(json, jsonFmt.get(), aOrigin.get(),
|
||||
aTransactionId, aBrowsingContextId);
|
||||
MOZ_ALWAYS_SUCCEEDS(
|
||||
os->NotifyObservers(nullptr, "webauthn-prompt", json.get()));
|
||||
}));
|
||||
#endif
|
||||
NS_DispatchToMainThread(runnable.forget());
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
WebAuthnService::MakeCredential(uint64_t aTransactionId,
|
||||
uint64_t browsingContextId,
|
||||
uint64_t aBrowsingContextId,
|
||||
nsIWebAuthnRegisterArgs* aArgs,
|
||||
nsIWebAuthnRegisterPromise* aPromise) {
|
||||
MOZ_ASSERT(aArgs);
|
||||
MOZ_ASSERT(aPromise);
|
||||
|
||||
auto guard = mTransactionState.Lock();
|
||||
if (guard->isSome()) {
|
||||
guard->ref().service->Reset();
|
||||
*guard = Nothing();
|
||||
ResetLocked(guard);
|
||||
*guard = Some(TransactionState{.service = DefaultService(),
|
||||
.transactionId = aTransactionId,
|
||||
.parentRegisterPromise = Some(aPromise)});
|
||||
|
||||
// We may need to show an attestation consent prompt before we return a
|
||||
// credential to WebAuthnTransactionParent, so we insert a new promise that
|
||||
// chains to `aPromise` here.
|
||||
|
||||
nsString attestation;
|
||||
Unused << aArgs->GetAttestationConveyancePreference(attestation);
|
||||
bool attestationRequested = !attestation.EqualsLiteral(
|
||||
MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE);
|
||||
|
||||
nsString origin;
|
||||
Unused << aArgs->GetOrigin(origin);
|
||||
|
||||
RefPtr<WebAuthnRegisterPromiseHolder> promiseHolder =
|
||||
new WebAuthnRegisterPromiseHolder(GetCurrentSerialEventTarget());
|
||||
|
||||
RefPtr<WebAuthnService> self = this;
|
||||
RefPtr<WebAuthnRegisterPromise> promise = promiseHolder->Ensure();
|
||||
promise
|
||||
->Then(
|
||||
GetCurrentSerialEventTarget(), __func__,
|
||||
[self, origin, aTransactionId, aBrowsingContextId,
|
||||
attestationRequested](
|
||||
const WebAuthnRegisterPromise::ResolveOrRejectValue& aValue) {
|
||||
auto guard = self->mTransactionState.Lock();
|
||||
if (guard->isNothing()) {
|
||||
return;
|
||||
}
|
||||
MOZ_ASSERT(guard->ref().parentRegisterPromise.isSome());
|
||||
MOZ_ASSERT(guard->ref().registerResult.isNothing());
|
||||
MOZ_ASSERT(guard->ref().childRegisterRequest.Exists());
|
||||
|
||||
guard->ref().childRegisterRequest.Complete();
|
||||
|
||||
if (aValue.IsReject()) {
|
||||
guard->ref().parentRegisterPromise.ref()->Reject(
|
||||
aValue.RejectValue());
|
||||
guard->reset();
|
||||
return;
|
||||
}
|
||||
|
||||
nsIWebAuthnRegisterResult* result = aValue.ResolveValue();
|
||||
// If the RP requested attestation, we need to show a consent prompt
|
||||
// before returning any identifying information. The platform may
|
||||
// have already done this for us, so we need to inspect the
|
||||
// attestation object at this point.
|
||||
bool resultIsIdentifying = true;
|
||||
Unused << result->HasIdentifyingAttestation(&resultIsIdentifying);
|
||||
if (attestationRequested && resultIsIdentifying) {
|
||||
guard->ref().registerResult = Some(result);
|
||||
self->ShowAttestationConsentPrompt(origin, aTransactionId,
|
||||
aBrowsingContextId);
|
||||
return;
|
||||
}
|
||||
result->Anonymize();
|
||||
guard->ref().parentRegisterPromise.ref()->Resolve(result);
|
||||
guard->reset();
|
||||
})
|
||||
->Track(guard->ref().childRegisterRequest);
|
||||
|
||||
nsresult rv = guard->ref().service->MakeCredential(
|
||||
aTransactionId, aBrowsingContextId, aArgs, promiseHolder);
|
||||
if (NS_FAILED(rv)) {
|
||||
promiseHolder->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
}
|
||||
*guard = Some(TransactionState{DefaultService()});
|
||||
return guard->ref().service->MakeCredential(aTransactionId, browsingContextId,
|
||||
aArgs, aPromise);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
WebAuthnService::GetAssertion(uint64_t aTransactionId,
|
||||
uint64_t browsingContextId,
|
||||
uint64_t aBrowsingContextId,
|
||||
nsIWebAuthnSignArgs* aArgs,
|
||||
nsIWebAuthnSignPromise* aPromise) {
|
||||
MOZ_ASSERT(aArgs);
|
||||
MOZ_ASSERT(aPromise);
|
||||
|
||||
auto guard = mTransactionState.Lock();
|
||||
if (guard->isSome()) {
|
||||
guard->ref().service->Reset();
|
||||
*guard = Nothing();
|
||||
}
|
||||
*guard = Some(TransactionState{DefaultService()});
|
||||
ResetLocked(guard);
|
||||
*guard = Some(TransactionState{.service = DefaultService(),
|
||||
.transactionId = aTransactionId});
|
||||
nsresult rv;
|
||||
|
||||
#if defined(XP_MACOSX)
|
||||
@ -71,7 +180,7 @@ WebAuthnService::GetAssertion(uint64_t aTransactionId,
|
||||
}
|
||||
#endif
|
||||
|
||||
rv = guard->ref().service->GetAssertion(aTransactionId, browsingContextId,
|
||||
rv = guard->ref().service->GetAssertion(aTransactionId, aBrowsingContextId,
|
||||
aArgs, aPromise);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
@ -125,13 +234,22 @@ WebAuthnService::ResumeConditionalGet(uint64_t aTransactionId) {
|
||||
return SelectedService()->ResumeConditionalGet(aTransactionId);
|
||||
}
|
||||
|
||||
void WebAuthnService::ResetLocked(
|
||||
const TransactionStateMutex::AutoLock& aGuard) {
|
||||
if (aGuard->isSome()) {
|
||||
aGuard->ref().childRegisterRequest.DisconnectIfExists();
|
||||
if (aGuard->ref().parentRegisterPromise.isSome()) {
|
||||
aGuard->ref().parentRegisterPromise.ref()->Reject(NS_ERROR_DOM_ABORT_ERR);
|
||||
}
|
||||
aGuard->ref().service->Reset();
|
||||
}
|
||||
aGuard->reset();
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
WebAuthnService::Reset() {
|
||||
auto guard = mTransactionState.Lock();
|
||||
if (guard->isSome()) {
|
||||
guard->ref().service->Reset();
|
||||
}
|
||||
*guard = Nothing();
|
||||
ResetLocked(guard);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
@ -146,10 +264,27 @@ WebAuthnService::PinCallback(uint64_t aTransactionId, const nsACString& aPin) {
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
WebAuthnService::ResumeMakeCredential(uint64_t aTransactionId,
|
||||
bool aForceNoneAttestation) {
|
||||
return SelectedService()->ResumeMakeCredential(aTransactionId,
|
||||
aForceNoneAttestation);
|
||||
WebAuthnService::SetHasAttestationConsent(uint64_t aTransactionId,
|
||||
bool aHasConsent) {
|
||||
auto guard = this->mTransactionState.Lock();
|
||||
if (guard->isNothing() || guard->ref().transactionId != aTransactionId) {
|
||||
// This could happen if the transaction was reset just when the prompt was
|
||||
// receiving user input.
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
MOZ_ASSERT(guard->ref().parentRegisterPromise.isSome());
|
||||
MOZ_ASSERT(guard->ref().registerResult.isSome());
|
||||
MOZ_ASSERT(!guard->ref().childRegisterRequest.Exists());
|
||||
|
||||
if (!aHasConsent) {
|
||||
guard->ref().registerResult.ref()->Anonymize();
|
||||
}
|
||||
guard->ref().parentRegisterPromise.ref()->Resolve(
|
||||
guard->ref().registerResult.ref());
|
||||
|
||||
guard->reset();
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
#include "nsIWebAuthnService.h"
|
||||
#include "AuthrsBridge_ffi.h"
|
||||
#include "mozilla/dom/WebAuthnPromiseHolder.h"
|
||||
|
||||
#ifdef MOZ_WIDGET_ANDROID
|
||||
# include "AndroidWebAuthnService.h"
|
||||
@ -55,6 +56,21 @@ class WebAuthnService final : public nsIWebAuthnService {
|
||||
private:
|
||||
~WebAuthnService() = default;
|
||||
|
||||
struct TransactionState {
|
||||
nsCOMPtr<nsIWebAuthnService> service;
|
||||
uint64_t transactionId;
|
||||
Maybe<nsCOMPtr<nsIWebAuthnRegisterPromise>> parentRegisterPromise;
|
||||
Maybe<nsCOMPtr<nsIWebAuthnRegisterResult>> registerResult;
|
||||
MozPromiseRequestHolder<WebAuthnRegisterPromise> childRegisterRequest;
|
||||
};
|
||||
using TransactionStateMutex = DataMutex<Maybe<TransactionState>>;
|
||||
TransactionStateMutex mTransactionState;
|
||||
|
||||
void ShowAttestationConsentPrompt(const nsString& aOrigin,
|
||||
uint64_t aTransactionId,
|
||||
uint64_t aBrowsingContextId);
|
||||
void ResetLocked(const TransactionStateMutex::AutoLock& aGuard);
|
||||
|
||||
nsIWebAuthnService* DefaultService() {
|
||||
if (StaticPrefs::security_webauth_webauthn_enable_softtoken()) {
|
||||
return mAuthrsService;
|
||||
@ -72,12 +88,6 @@ class WebAuthnService final : public nsIWebAuthnService {
|
||||
return DefaultService();
|
||||
}
|
||||
|
||||
struct TransactionState {
|
||||
nsCOMPtr<nsIWebAuthnService> service;
|
||||
};
|
||||
using TransactionStateMutex = DataMutex<Maybe<TransactionState>>;
|
||||
TransactionStateMutex mTransactionState;
|
||||
|
||||
nsCOMPtr<nsIWebAuthnService> mAuthrsService;
|
||||
nsCOMPtr<nsIWebAuthnService> mPlatformService;
|
||||
};
|
||||
|
@ -411,14 +411,12 @@ WinWebAuthnService::MakeCredential(uint64_t aTransactionId,
|
||||
// AttestationConveyance
|
||||
nsString attestation;
|
||||
Unused << aArgs->GetAttestationConveyancePreference(attestation);
|
||||
bool anonymize = false;
|
||||
// This mapping needs to be reviewed if values are added to the
|
||||
// AttestationConveyancePreference enum.
|
||||
static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3);
|
||||
if (attestation.EqualsLiteral(
|
||||
MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE)) {
|
||||
winAttestation = WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE;
|
||||
anonymize = true;
|
||||
} else if (
|
||||
attestation.EqualsLiteral(
|
||||
MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT)) {
|
||||
@ -579,13 +577,6 @@ WinWebAuthnService::MakeCredential(uint64_t aTransactionId,
|
||||
}
|
||||
gWinWebauthnFreeCredentialAttestation(pWebAuthNCredentialAttestation);
|
||||
|
||||
if (anonymize) {
|
||||
nsresult rv = result->Anonymize();
|
||||
if (NS_FAILED(rv)) {
|
||||
aPromise->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
return;
|
||||
}
|
||||
}
|
||||
aPromise->Resolve(result);
|
||||
} else {
|
||||
PCWSTR errorName = gWinWebauthnGetErrorName(hr);
|
||||
@ -977,8 +968,8 @@ WinWebAuthnService::PinCallback(uint64_t aTransactionId,
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
WinWebAuthnService::ResumeMakeCredential(uint64_t aTransactionId,
|
||||
bool aForceNoneAttestation) {
|
||||
WinWebAuthnService::SetHasAttestationConsent(uint64_t aTransactionId,
|
||||
bool aHasConsent) {
|
||||
return NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ extern crate xpcom;
|
||||
|
||||
use authenticator::{
|
||||
authenticatorservice::{RegisterArgs, SignArgs},
|
||||
ctap2::attestation::AttestationObject,
|
||||
ctap2::attestation::{AAGuid, AttestationObject, AttestationStatement},
|
||||
ctap2::commands::{get_info::AuthenticatorVersion, PinUvAuthResult},
|
||||
ctap2::server::{
|
||||
AuthenticationExtensionsClientInputs, AuthenticatorAttachment,
|
||||
@ -34,6 +34,7 @@ use nserror::{
|
||||
use nsstring::{nsACString, nsAString, nsCString, nsString};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Write;
|
||||
use std::sync::mpsc::{channel, Receiver, RecvError, Sender};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
@ -86,7 +87,6 @@ enum BrowserPromptType<'a> {
|
||||
},
|
||||
PinIsTooLong,
|
||||
PinIsTooShort,
|
||||
RegisterDirect,
|
||||
UvInvalid {
|
||||
retries: Option<u8>,
|
||||
},
|
||||
@ -166,7 +166,8 @@ fn cancel_prompts(tid: u64) -> Result<(), nsresult> {
|
||||
|
||||
#[xpcom(implement(nsIWebAuthnRegisterResult), atomic)]
|
||||
pub struct WebAuthnRegisterResult {
|
||||
result: RegisterResult,
|
||||
// result is only borrowed mutably in `Anonymize`.
|
||||
result: RefCell<RegisterResult>,
|
||||
}
|
||||
|
||||
impl WebAuthnRegisterResult {
|
||||
@ -178,13 +179,13 @@ impl WebAuthnRegisterResult {
|
||||
xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>);
|
||||
fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> {
|
||||
let mut out = ThinVec::new();
|
||||
serde_cbor::to_writer(&mut out, &self.result.att_obj).or(Err(NS_ERROR_FAILURE))?;
|
||||
serde_cbor::to_writer(&mut out, &self.result.borrow().att_obj).or(Err(NS_ERROR_FAILURE))?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>);
|
||||
fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> {
|
||||
let Some(credential_data) = &self.result.att_obj.auth_data.credential_data else {
|
||||
let Some(credential_data) = &self.result.borrow().att_obj.auth_data.credential_data else {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
};
|
||||
Ok(credential_data.credential_id.as_slice().into())
|
||||
@ -197,7 +198,7 @@ impl WebAuthnRegisterResult {
|
||||
// In tests, the result is not very important, but we can at least return "internal" if
|
||||
// we're simulating platform attachment.
|
||||
if static_prefs::pref!("security.webauth.webauthn_enable_softtoken")
|
||||
&& self.result.attachment == AuthenticatorAttachment::Platform
|
||||
&& self.result.borrow().attachment == AuthenticatorAttachment::Platform
|
||||
{
|
||||
Ok(thin_vec![nsString::from("internal")])
|
||||
} else {
|
||||
@ -207,7 +208,7 @@ impl WebAuthnRegisterResult {
|
||||
|
||||
xpcom_method!(get_hmac_create_secret => GetHmacCreateSecret() -> bool);
|
||||
fn get_hmac_create_secret(&self) -> Result<bool, nsresult> {
|
||||
let Some(hmac_create_secret) = self.result.extensions.hmac_create_secret else {
|
||||
let Some(hmac_create_secret) = self.result.borrow().extensions.hmac_create_secret else {
|
||||
return Err(NS_ERROR_NOT_AVAILABLE);
|
||||
};
|
||||
Ok(hmac_create_secret)
|
||||
@ -215,7 +216,7 @@ impl WebAuthnRegisterResult {
|
||||
|
||||
xpcom_method!(get_cred_props_rk => GetCredPropsRk() -> bool);
|
||||
fn get_cred_props_rk(&self) -> Result<bool, nsresult> {
|
||||
let Some(cred_props) = &self.result.extensions.cred_props else {
|
||||
let Some(cred_props) = &self.result.borrow().extensions.cred_props else {
|
||||
return Err(NS_ERROR_NOT_AVAILABLE);
|
||||
};
|
||||
Ok(cred_props.rk)
|
||||
@ -228,12 +229,29 @@ impl WebAuthnRegisterResult {
|
||||
|
||||
xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString);
|
||||
fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> {
|
||||
match self.result.attachment {
|
||||
match self.result.borrow().attachment {
|
||||
AuthenticatorAttachment::CrossPlatform => Ok(nsString::from("cross-platform")),
|
||||
AuthenticatorAttachment::Platform => Ok(nsString::from("platform")),
|
||||
AuthenticatorAttachment::Unknown => Err(NS_ERROR_NOT_AVAILABLE),
|
||||
}
|
||||
}
|
||||
|
||||
xpcom_method!(has_identifying_attestation => HasIdentifyingAttestation() -> bool);
|
||||
fn has_identifying_attestation(&self) -> Result<bool, nsresult> {
|
||||
if self.result.borrow().att_obj.att_stmt != AttestationStatement::None {
|
||||
return Ok(true);
|
||||
}
|
||||
if let Some(data) = &self.result.borrow().att_obj.auth_data.credential_data {
|
||||
return Ok(data.aaguid != AAGuid::default());
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
xpcom_method!(anonymize => Anonymize());
|
||||
fn anonymize(&self) -> Result<nsresult, nsresult> {
|
||||
self.result.borrow_mut().att_obj.anonymize();
|
||||
Ok(NS_OK)
|
||||
}
|
||||
}
|
||||
|
||||
#[xpcom(implement(nsIWebAuthnAttObj), atomic)]
|
||||
@ -275,6 +293,17 @@ impl WebAuthnAttObj {
|
||||
// safe to cast to i32 by inspection of defined values
|
||||
Ok(credential_data.credential_public_key.alg as i32)
|
||||
}
|
||||
|
||||
xpcom_method!(is_identifying => IsIdentifying() -> bool);
|
||||
fn is_identifying(&self) -> Result<bool, nsresult> {
|
||||
if self.att_obj.att_stmt != AttestationStatement::None {
|
||||
return Ok(true);
|
||||
}
|
||||
if let Some(data) = &self.att_obj.auth_data.credential_data {
|
||||
return Ok(data.aaguid != AAGuid::default());
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[xpcom(implement(nsIWebAuthnSignResult), atomic)]
|
||||
@ -490,10 +519,11 @@ impl RegisterPromise {
|
||||
fn resolve_or_reject(&self, result: Result<RegisterResult, nsresult>) -> Result<(), nsresult> {
|
||||
match result {
|
||||
Ok(result) => {
|
||||
let wrapped_result =
|
||||
WebAuthnRegisterResult::allocate(InitWebAuthnRegisterResult { result })
|
||||
.query_interface::<nsIWebAuthnRegisterResult>()
|
||||
.ok_or(NS_ERROR_FAILURE)?;
|
||||
let wrapped_result = WebAuthnRegisterResult::allocate(InitWebAuthnRegisterResult {
|
||||
result: RefCell::new(result),
|
||||
})
|
||||
.query_interface::<nsIWebAuthnRegisterResult>()
|
||||
.ok_or(NS_ERROR_FAILURE)?;
|
||||
unsafe { self.0.Resolve(wrapped_result.coerce()) };
|
||||
}
|
||||
Err(result) => {
|
||||
@ -543,7 +573,6 @@ impl TransactionPromise {
|
||||
}
|
||||
|
||||
enum TransactionArgs {
|
||||
Register(/* timeout */ u64, RegisterArgs),
|
||||
Sign(/* timeout */ u64, SignArgs),
|
||||
}
|
||||
|
||||
@ -713,13 +742,6 @@ impl AuthrsService {
|
||||
}
|
||||
}
|
||||
|
||||
let mut attestation_conveyance_preference = nsString::new();
|
||||
unsafe { args.GetAttestationConveyancePreference(&mut *attestation_conveyance_preference) }
|
||||
.to_result()?;
|
||||
let none_attestation = !(attestation_conveyance_preference.eq("indirect")
|
||||
|| attestation_conveyance_preference.eq("direct")
|
||||
|| attestation_conveyance_preference.eq("enterprise"));
|
||||
|
||||
let mut cred_props = false;
|
||||
unsafe { args.GetCredProps(&mut cred_props) }.to_result()?;
|
||||
|
||||
@ -760,51 +782,19 @@ impl AuthrsService {
|
||||
use_ctap1_fallback: !static_prefs::pref!("security.webauthn.ctap2"),
|
||||
};
|
||||
|
||||
*self.transaction.lock().unwrap() = Some(TransactionState {
|
||||
let mut guard = self.transaction.lock().unwrap();
|
||||
*guard = Some(TransactionState {
|
||||
tid,
|
||||
browsing_context_id,
|
||||
pending_args: Some(TransactionArgs::Register(timeout_ms as u64, info)),
|
||||
pending_args: None,
|
||||
promise: TransactionPromise::Register(promise),
|
||||
pin_receiver: None,
|
||||
selection_receiver: None,
|
||||
interactive_receiver: None,
|
||||
puat_cache: None,
|
||||
});
|
||||
|
||||
if none_attestation
|
||||
|| static_prefs::pref!("security.webauth.webauthn_testing_allow_direct_attestation")
|
||||
{
|
||||
self.resume_make_credential(tid, none_attestation)
|
||||
} else {
|
||||
send_prompt(
|
||||
BrowserPromptType::RegisterDirect,
|
||||
tid,
|
||||
Some(&origin),
|
||||
Some(browsing_context_id),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
xpcom_method!(resume_make_credential => ResumeMakeCredential(aTid: u64, aForceNoneAttestation: bool));
|
||||
fn resume_make_credential(
|
||||
&self,
|
||||
tid: u64,
|
||||
force_none_attestation: bool,
|
||||
) -> Result<(), nsresult> {
|
||||
let mut guard = self.transaction.lock().unwrap();
|
||||
let Some(state) = guard.as_mut() else {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
};
|
||||
if state.tid != tid {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
};
|
||||
let browsing_context_id = state.browsing_context_id;
|
||||
let Some(TransactionArgs::Register(timeout_ms, info)) = state.pending_args.take() else {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
};
|
||||
// We have to drop the guard here, as there _may_ still be another operation
|
||||
// ongoing and `register()` below will try to cancel it. This will call the state
|
||||
// callback of that operation, which in turn may try to access `transaction`, deadlocking.
|
||||
// drop the guard here to ensure we don't deadlock if the call to `register()` below
|
||||
// hairpins the state callback.
|
||||
drop(guard);
|
||||
|
||||
let (status_tx, status_rx) = channel::<StatusUpdate>();
|
||||
@ -825,7 +815,7 @@ impl AuthrsService {
|
||||
let callback_transaction = self.transaction.clone();
|
||||
let callback_origin = info.origin.clone();
|
||||
let state_callback = StateCallback::<Result<RegisterResult, AuthenticatorError>>::new(
|
||||
Box::new(move |mut result| {
|
||||
Box::new(move |result| {
|
||||
let mut guard = callback_transaction.lock().unwrap();
|
||||
let Some(state) = guard.as_mut() else {
|
||||
return;
|
||||
@ -836,13 +826,6 @@ impl AuthrsService {
|
||||
let TransactionPromise::Register(ref promise) = state.promise else {
|
||||
return;
|
||||
};
|
||||
if let Ok(inner) = result.as_mut() {
|
||||
// Tokens always provide attestation, but the user may have asked we not
|
||||
// include the attestation statement in the response.
|
||||
if force_none_attestation {
|
||||
inner.att_obj.anonymize();
|
||||
}
|
||||
}
|
||||
if let Err(AuthenticatorError::CredentialExcluded) = result {
|
||||
let _ = send_prompt(
|
||||
BrowserPromptType::AlreadyRegistered,
|
||||
@ -874,14 +857,14 @@ impl AuthrsService {
|
||||
Some(browsing_context_id),
|
||||
)?;
|
||||
self.usb_token_manager.lock().unwrap().register(
|
||||
timeout_ms,
|
||||
timeout_ms.into(),
|
||||
info,
|
||||
status_tx,
|
||||
state_callback,
|
||||
);
|
||||
} else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") {
|
||||
self.test_token_manager
|
||||
.register(timeout_ms, info, status_tx, state_callback);
|
||||
.register(timeout_ms.into(), info, status_tx, state_callback);
|
||||
} else {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
}
|
||||
@ -889,6 +872,11 @@ impl AuthrsService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
xpcom_method!(set_has_attestation_consent => SetHasAttestationConsent(aTid: u64, aHasConsent: bool));
|
||||
fn set_has_attestation_consent(&self, _tid: u64, _has_consent: bool) -> Result<(), nsresult> {
|
||||
Err(NS_ERROR_NOT_IMPLEMENTED)
|
||||
}
|
||||
|
||||
xpcom_method!(get_assertion => GetAssertion(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsIWebAuthnSignArgs, aPromise: *const nsIWebAuthnSignPromise));
|
||||
fn get_assertion(
|
||||
&self,
|
||||
|
@ -17,4 +17,6 @@ interface nsIWebAuthnAttObj : nsISupports {
|
||||
readonly attribute Array<octet> publicKey;
|
||||
|
||||
readonly attribute COSEAlgorithmIdentifier publicKeyAlgorithm;
|
||||
|
||||
boolean isIdentifying();
|
||||
};
|
||||
|
@ -28,6 +28,9 @@ interface nsIWebAuthnRegisterResult : nsISupports {
|
||||
[must_use] attribute boolean credPropsRk;
|
||||
|
||||
[must_use] readonly attribute AString authenticatorAttachment;
|
||||
|
||||
boolean hasIdentifyingAttestation();
|
||||
void anonymize();
|
||||
};
|
||||
|
||||
// The nsIWebAuthnSignResult interface is used to construct IPDL-defined
|
||||
|
@ -85,7 +85,7 @@ interface nsIWebAuthnService : nsISupports
|
||||
void resumeConditionalGet(in uint64_t aTransactionId);
|
||||
|
||||
void pinCallback(in uint64_t aTransactionId, in ACString aPin);
|
||||
void resumeMakeCredential(in uint64_t aTransactionId, in boolean aForceNoneAttestation);
|
||||
void setHasAttestationConsent(in uint64_t aTransactionId, in boolean aHasConsent);
|
||||
void selectionCallback(in uint64_t aTransactionId, in uint64_t aIndex);
|
||||
|
||||
// Adds a virtual (software) authenticator for use in tests (particularly
|
||||
|
@ -46,15 +46,13 @@ add_task(async function test_webauthn_modal_request_cancels_conditional_get() {
|
||||
ok(active, "conditional request should still be active");
|
||||
|
||||
let promptPromise = promiseNotification("webauthn-prompt-register-direct");
|
||||
let modalPromise = promiseWebAuthnMakeCredential(tab, "direct")
|
||||
.then(arrivingHereIsBad)
|
||||
.catch(gExpectNotAllowedError);
|
||||
let modalPromise = promiseWebAuthnMakeCredential(tab, "direct");
|
||||
|
||||
await condPromise;
|
||||
|
||||
ok(!active, "conditional request should not be active");
|
||||
|
||||
// Cancel the modal request with the button.
|
||||
// Proceed through the consent prompt
|
||||
await promptPromise;
|
||||
PopupNotifications.panel.firstElementChild.secondaryButton.click();
|
||||
await modalPromise;
|
||||
|
@ -43,34 +43,26 @@ add_task(async function test_setup_usbtoken() {
|
||||
});
|
||||
add_task(test_register);
|
||||
add_task(test_register_escape);
|
||||
add_task(test_register_direct_cancel);
|
||||
add_task(test_register_direct_presence);
|
||||
add_task(test_sign);
|
||||
add_task(test_sign_escape);
|
||||
add_task(test_tab_switching);
|
||||
add_task(test_window_switching);
|
||||
add_task(async function test_setup_fullscreen() {
|
||||
add_task(async function test_setup_softtoken() {
|
||||
gAuthenticatorId = add_virtual_authenticator();
|
||||
return SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.fullscreen.autohide", true],
|
||||
["full-screen-api.enabled", true],
|
||||
["full-screen-api.allow-trusted-requests-only", false],
|
||||
],
|
||||
});
|
||||
});
|
||||
add_task(test_fullscreen_show_nav_toolbar);
|
||||
add_task(test_no_fullscreen_dom);
|
||||
add_task(async function test_setup_softtoken() {
|
||||
gAuthenticatorId = add_virtual_authenticator();
|
||||
return SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["security.webauth.webauthn_enable_softtoken", true],
|
||||
["security.webauth.webauthn_enable_usbtoken", false],
|
||||
],
|
||||
});
|
||||
});
|
||||
add_task(test_register_direct_proceed);
|
||||
add_task(test_register_direct_proceed_anon);
|
||||
add_task(test_fullscreen_show_nav_toolbar);
|
||||
add_task(test_no_fullscreen_dom);
|
||||
add_task(test_register_direct_with_consent);
|
||||
add_task(test_register_direct_without_consent);
|
||||
add_task(test_select_sign_result);
|
||||
|
||||
function promiseNavToolboxStatus(aExpectedStatus) {
|
||||
@ -215,53 +207,6 @@ async function test_sign_escape() {
|
||||
await BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
|
||||
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 = promiseWebAuthnMakeCredential(tab, "direct")
|
||||
.then(arrivingHereIsBad)
|
||||
.catch(expectNotAllowedError)
|
||||
.then(() => (active = false));
|
||||
await promiseNotification("webauthn-prompt-register-direct");
|
||||
|
||||
// Cancel the request.
|
||||
ok(active, "request should still be active");
|
||||
PopupNotifications.panel.firstElementChild.secondaryButton.click();
|
||||
await promise;
|
||||
|
||||
// Close tab.
|
||||
await BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
|
||||
async function test_register_direct_presence() {
|
||||
// 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 = promiseWebAuthnMakeCredential(tab, "direct")
|
||||
.then(arrivingHereIsBad)
|
||||
.catch(expectNotAllowedError)
|
||||
.then(() => (active = false));
|
||||
await promiseNotification("webauthn-prompt-register-direct");
|
||||
|
||||
// Click "proceed" and wait for presence prompt
|
||||
let presence = promiseNotification("webauthn-prompt-presence");
|
||||
PopupNotifications.panel.firstElementChild.button.click();
|
||||
await presence;
|
||||
|
||||
// Cancel the request.
|
||||
ok(active, "request should still be active");
|
||||
PopupNotifications.panel.firstElementChild.button.click();
|
||||
await promise;
|
||||
|
||||
// Close tab.
|
||||
await BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
|
||||
// Add two tabs, open WebAuthn in the first, switch, assert the prompt is
|
||||
// not visible, switch back, assert the prompt is there and cancel it.
|
||||
async function test_tab_switching() {
|
||||
@ -359,7 +304,7 @@ async function test_window_switching() {
|
||||
await BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
|
||||
async function test_register_direct_proceed() {
|
||||
async function test_register_direct_with_consent() {
|
||||
// Open a new tab.
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
|
||||
|
||||
@ -367,7 +312,7 @@ async function test_register_direct_proceed() {
|
||||
let request = promiseWebAuthnMakeCredential(tab, "direct");
|
||||
await promiseNotification("webauthn-prompt-register-direct");
|
||||
|
||||
// Proceed.
|
||||
// Click "Allow".
|
||||
PopupNotifications.panel.firstElementChild.button.click();
|
||||
|
||||
// Ensure we got "direct" attestation.
|
||||
@ -377,7 +322,7 @@ async function test_register_direct_proceed() {
|
||||
await BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
|
||||
async function test_register_direct_proceed_anon() {
|
||||
async function test_register_direct_without_consent() {
|
||||
// Open a new tab.
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
|
||||
|
||||
@ -385,9 +330,8 @@ async function test_register_direct_proceed_anon() {
|
||||
let request = promiseWebAuthnMakeCredential(tab, "direct");
|
||||
await promiseNotification("webauthn-prompt-register-direct");
|
||||
|
||||
// Check "anonymize anyway" and proceed.
|
||||
PopupNotifications.panel.firstElementChild.checkbox.checked = true;
|
||||
PopupNotifications.panel.firstElementChild.button.click();
|
||||
// Click "Block".
|
||||
PopupNotifications.panel.firstElementChild.secondaryButton.click();
|
||||
|
||||
// Ensure we got "none" attestation.
|
||||
await request.then(verifyAnonymizedCertificate);
|
||||
@ -438,23 +382,22 @@ async function test_fullscreen_show_nav_toolbar() {
|
||||
|
||||
await navToolboxHiddenPromise;
|
||||
|
||||
// Request a new credential and wait for the direct attestation consent
|
||||
// prompt.
|
||||
// Request a new credential with direct attestation. The consent prompt will
|
||||
// keep the request active until we can verify that the nav toolbar is shown.
|
||||
let promptPromise = promiseNotification("webauthn-prompt-register-direct");
|
||||
let navToolboxShownPromise = promiseNavToolboxStatus("shown");
|
||||
|
||||
let active = true;
|
||||
let requestPromise = promiseWebAuthnMakeCredential(tab, "direct")
|
||||
.then(arrivingHereIsBad)
|
||||
.catch(expectNotAllowedError)
|
||||
.then(() => (active = false));
|
||||
let requestPromise = promiseWebAuthnMakeCredential(tab, "direct").then(
|
||||
() => (active = false)
|
||||
);
|
||||
|
||||
await Promise.all([promptPromise, navToolboxShownPromise]);
|
||||
|
||||
ok(active, "request is active");
|
||||
ok(window.fullScreen, "window is fullscreen");
|
||||
|
||||
// Cancel the request.
|
||||
// Proceed through the consent prompt.
|
||||
PopupNotifications.panel.firstElementChild.secondaryButton.click();
|
||||
await requestPromise;
|
||||
|
||||
@ -475,23 +418,22 @@ async function test_no_fullscreen_dom() {
|
||||
await fullScreenPaintPromise;
|
||||
ok(!!document.fullscreenElement, "a DOM element is fullscreen");
|
||||
|
||||
// Request a new credential and wait for the direct attestation consent
|
||||
// prompt.
|
||||
// Request a new credential with direct attestation. The consent prompt will
|
||||
// keep the request active until we can verify that we've left fullscreen.
|
||||
let promptPromise = promiseNotification("webauthn-prompt-register-direct");
|
||||
fullScreenPaintPromise = promiseFullScreenPaint();
|
||||
|
||||
let active = true;
|
||||
let requestPromise = promiseWebAuthnMakeCredential(tab, "direct")
|
||||
.then(arrivingHereIsBad)
|
||||
.catch(expectNotAllowedError)
|
||||
.then(() => (active = false));
|
||||
let requestPromise = promiseWebAuthnMakeCredential(tab, "direct").then(
|
||||
() => (active = false)
|
||||
);
|
||||
|
||||
await Promise.all([promptPromise, fullScreenPaintPromise]);
|
||||
|
||||
ok(active, "request is active");
|
||||
ok(!document.fullscreenElement, "no DOM element is fullscreen");
|
||||
|
||||
// Cancel the request.
|
||||
// Proceed through the consent prompt.
|
||||
await waitForPopupNotificationSecurityDelay();
|
||||
PopupNotifications.panel.firstElementChild.secondaryButton.click();
|
||||
await requestPromise;
|
||||
|
Loading…
Reference in New Issue
Block a user