Backed out changeset 9ef1475ad0a2 (bug 1890206) for causing build bustages in nsIWebAuthnAttObj.idl CLOSED TREE

This commit is contained in:
Cristian Tuns
2024-04-09 17:13:15 -04:00
parent d7df973069
commit a62348feef
16 changed files with 228 additions and 322 deletions

View File

@@ -7134,8 +7134,8 @@ var WebAuthnPromptHelper = {
if (data.prompt.type == "presence") { if (data.prompt.type == "presence") {
this.presence_required(mgr, data); this.presence_required(mgr, data);
} else if (data.prompt.type == "attestation-consent") { } else if (data.prompt.type == "register-direct") {
this.attestation_consent(mgr, data); this.registerDirect(mgr, data);
} else if (data.prompt.type == "pin-required") { } else if (data.prompt.type == "pin-required") {
this.pin_required(mgr, false, data); this.pin_required(mgr, false, data);
} else if (data.prompt.type == "pin-invalid") { } else if (data.prompt.type == "pin-invalid") {
@@ -7298,23 +7298,9 @@ var WebAuthnPromptHelper = {
); );
}, },
attestation_consent(mgr, { origin, tid }) { registerDirect(mgr, { origin, tid }) {
let mainAction = { let mainAction = this.buildProceedAction(mgr, tid);
label: gNavigatorBundle.getString("webauthn.allow"), let secondaryActions = [this.buildCancelAction(mgr, tid)];
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 = let learnMoreURL =
Services.urlFormatter.formatURLPref("app.support.baseURL") + Services.urlFormatter.formatURLPref("app.support.baseURL") +
@@ -7322,6 +7308,9 @@ var WebAuthnPromptHelper = {
let options = { let options = {
learnMoreURL, learnMoreURL,
checkbox: {
label: gNavigatorBundle.getString("webauthn.anonymize"),
},
hintText: "webauthn.registerDirectPromptHint", hintText: "webauthn.registerDirectPromptHint",
}; };
this.show( this.show(
@@ -7444,6 +7433,16 @@ 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) { buildCancelAction(mgr, tid) {
return { return {
label: gNavigatorBundle.getString("webauthn.cancel"), label: gNavigatorBundle.getString("webauthn.cancel"),

View File

@@ -150,10 +150,9 @@ webauthn.uvBlockedPrompt=User verification failed on %S. There were too many fai
webauthn.alreadyRegisteredPrompt=This device is already registered. Try a different device. webauthn.alreadyRegisteredPrompt=This device is already registered. Try a different device.
webauthn.cancel=Cancel webauthn.cancel=Cancel
webauthn.cancel.accesskey=c webauthn.cancel.accesskey=c
webauthn.allow=Allow webauthn.proceed=Proceed
webauthn.allow.accesskey=A webauthn.proceed.accesskey=p
webauthn.block=Block webauthn.anonymize=Anonymize anyway
webauthn.block.accesskey=B
# LOCALIZATION NOTE (identity.identified.verifier, identity.identified.state_and_country, identity.ev.contentOwner2): # LOCALIZATION NOTE (identity.identified.verifier, identity.identified.state_and_country, identity.ev.contentOwner2):
# %S is the hostname of the site that is being displayed. # %S is the hostname of the site that is being displayed.

View File

@@ -186,6 +186,12 @@ AndroidWebAuthnService::MakeCredential(uint64_t aTransactionId,
GetCurrentSerialEventTarget(), __func__, GetCurrentSerialEventTarget(), __func__,
[aPromise, credPropsResponse = std::move(credPropsResponse)]( [aPromise, credPropsResponse = std::move(credPropsResponse)](
RefPtr<WebAuthnRegisterResult>&& aValue) { 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()) { if (credPropsResponse.isSome()) {
Unused << aValue->SetCredPropsRk(credPropsResponse.ref()); Unused << aValue->SetCredPropsRk(credPropsResponse.ref());
} }
@@ -351,8 +357,8 @@ AndroidWebAuthnService::PinCallback(uint64_t aTransactionId,
} }
NS_IMETHODIMP NS_IMETHODIMP
AndroidWebAuthnService::SetHasAttestationConsent(uint64_t aTransactionId, AndroidWebAuthnService::ResumeMakeCredential(uint64_t aTransactionId,
bool aHasConsent) { bool aForceNoneAttestation) {
return NS_ERROR_NOT_IMPLEMENTED; return NS_ERROR_NOT_IMPLEMENTED;
} }

View File

@@ -1121,8 +1121,8 @@ MacOSWebAuthnService::PinCallback(uint64_t aTransactionId,
} }
NS_IMETHODIMP NS_IMETHODIMP
MacOSWebAuthnService::SetHasAttestationConsent(uint64_t aTransactionId, MacOSWebAuthnService::ResumeMakeCredential(uint64_t aTransactionId,
bool aHasConsent) { bool aForceNoneAttestation) {
return NS_ERROR_NOT_IMPLEMENTED; return NS_ERROR_NOT_IMPLEMENTED;
} }

View File

@@ -155,18 +155,7 @@ WebAuthnRegisterArgs::GetTimeoutMS(uint32_t* aTimeoutMS) {
NS_IMETHODIMP NS_IMETHODIMP
WebAuthnRegisterArgs::GetAttestationConveyancePreference( WebAuthnRegisterArgs::GetAttestationConveyancePreference(
nsAString& aAttestationConveyancePreference) { nsAString& aAttestationConveyancePreference) {
const nsString& attPref = mInfo.attestationConveyancePreference(); aAttestationConveyancePreference = 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; return NS_OK;
} }

View File

@@ -102,27 +102,7 @@ WebAuthnRegisterResult::GetAuthenticatorAttachment(
return NS_ERROR_NOT_AVAILABLE; return NS_ERROR_NOT_AVAILABLE;
} }
NS_IMETHODIMP nsresult WebAuthnRegisterResult::Anonymize() {
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 // The anonymize flag in the nsIWebAuthnAttObj constructor causes the
// attestation statement to be removed during deserialization. It also // attestation statement to be removed during deserialization. It also
// causes the AAGUID to be zeroed out. If we can't deserialize the // causes the AAGUID to be zeroed out. If we can't deserialize the

View File

@@ -134,6 +134,8 @@ class WebAuthnRegisterResult final : public nsIWebAuthnRegisterResult {
} }
#endif #endif
nsresult Anonymize();
private: private:
~WebAuthnRegisterResult() = default; ~WebAuthnRegisterResult() = default;

View File

@@ -5,9 +5,7 @@
#include "mozilla/Services.h" #include "mozilla/Services.h"
#include "mozilla/StaticPrefs_security.h" #include "mozilla/StaticPrefs_security.h"
#include "nsIObserverService.h" #include "nsIObserverService.h"
#include "nsTextFormatter.h"
#include "nsThreadUtils.h" #include "nsThreadUtils.h"
#include "WebAuthnEnumStrings.h"
#include "WebAuthnService.h" #include "WebAuthnService.h"
#include "WebAuthnTransportIdentifiers.h" #include "WebAuthnTransportIdentifiers.h"
@@ -20,139 +18,32 @@ already_AddRefed<nsIWebAuthnService> NewWebAuthnService() {
NS_IMPL_ISUPPORTS(WebAuthnService, nsIWebAuthnService) 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, aOrigin, aTransactionId, aBrowsingContextId]() {
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 NS_IMETHODIMP
WebAuthnService::MakeCredential(uint64_t aTransactionId, WebAuthnService::MakeCredential(uint64_t aTransactionId,
uint64_t aBrowsingContextId, uint64_t browsingContextId,
nsIWebAuthnRegisterArgs* aArgs, nsIWebAuthnRegisterArgs* aArgs,
nsIWebAuthnRegisterPromise* aPromise) { nsIWebAuthnRegisterPromise* aPromise) {
MOZ_ASSERT(aArgs);
MOZ_ASSERT(aPromise);
auto guard = mTransactionState.Lock(); auto guard = mTransactionState.Lock();
ResetLocked(guard); if (guard->isSome()) {
*guard = Some(TransactionState{.service = DefaultService(), guard->ref().service->Reset();
.transactionId = aTransactionId, *guard = Nothing();
.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()); *guard = Some(TransactionState{DefaultService()});
MOZ_ASSERT(guard->ref().registerResult.isNothing()); return guard->ref().service->MakeCredential(aTransactionId, browsingContextId,
MOZ_ASSERT(guard->ref().childRegisterRequest.Exists()); aArgs, aPromise);
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);
}
return NS_OK;
} }
NS_IMETHODIMP NS_IMETHODIMP
WebAuthnService::GetAssertion(uint64_t aTransactionId, WebAuthnService::GetAssertion(uint64_t aTransactionId,
uint64_t aBrowsingContextId, uint64_t browsingContextId,
nsIWebAuthnSignArgs* aArgs, nsIWebAuthnSignArgs* aArgs,
nsIWebAuthnSignPromise* aPromise) { nsIWebAuthnSignPromise* aPromise) {
MOZ_ASSERT(aArgs);
MOZ_ASSERT(aPromise);
auto guard = mTransactionState.Lock(); auto guard = mTransactionState.Lock();
ResetLocked(guard); if (guard->isSome()) {
*guard = Some(TransactionState{.service = DefaultService(), guard->ref().service->Reset();
.transactionId = aTransactionId}); *guard = Nothing();
}
*guard = Some(TransactionState{DefaultService()});
nsresult rv; nsresult rv;
#if defined(XP_MACOSX) #if defined(XP_MACOSX)
@@ -180,7 +71,7 @@ WebAuthnService::GetAssertion(uint64_t aTransactionId,
} }
#endif #endif
rv = guard->ref().service->GetAssertion(aTransactionId, aBrowsingContextId, rv = guard->ref().service->GetAssertion(aTransactionId, browsingContextId,
aArgs, aPromise); aArgs, aPromise);
if (NS_FAILED(rv)) { if (NS_FAILED(rv)) {
return rv; return rv;
@@ -234,22 +125,13 @@ WebAuthnService::ResumeConditionalGet(uint64_t aTransactionId) {
return SelectedService()->ResumeConditionalGet(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 NS_IMETHODIMP
WebAuthnService::Reset() { WebAuthnService::Reset() {
auto guard = mTransactionState.Lock(); auto guard = mTransactionState.Lock();
ResetLocked(guard); if (guard->isSome()) {
guard->ref().service->Reset();
}
*guard = Nothing();
return NS_OK; return NS_OK;
} }
@@ -264,27 +146,10 @@ WebAuthnService::PinCallback(uint64_t aTransactionId, const nsACString& aPin) {
} }
NS_IMETHODIMP NS_IMETHODIMP
WebAuthnService::SetHasAttestationConsent(uint64_t aTransactionId, WebAuthnService::ResumeMakeCredential(uint64_t aTransactionId,
bool aHasConsent) { bool aForceNoneAttestation) {
auto guard = this->mTransactionState.Lock(); return SelectedService()->ResumeMakeCredential(aTransactionId,
if (guard->isNothing() || guard->ref().transactionId != aTransactionId) { aForceNoneAttestation);
// 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 NS_IMETHODIMP

View File

@@ -7,7 +7,6 @@
#include "nsIWebAuthnService.h" #include "nsIWebAuthnService.h"
#include "AuthrsBridge_ffi.h" #include "AuthrsBridge_ffi.h"
#include "mozilla/dom/WebAuthnPromiseHolder.h"
#ifdef MOZ_WIDGET_ANDROID #ifdef MOZ_WIDGET_ANDROID
# include "AndroidWebAuthnService.h" # include "AndroidWebAuthnService.h"
@@ -56,21 +55,6 @@ class WebAuthnService final : public nsIWebAuthnService {
private: private:
~WebAuthnService() = default; ~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() { nsIWebAuthnService* DefaultService() {
if (StaticPrefs::security_webauth_webauthn_enable_softtoken()) { if (StaticPrefs::security_webauth_webauthn_enable_softtoken()) {
return mAuthrsService; return mAuthrsService;
@@ -88,6 +72,12 @@ class WebAuthnService final : public nsIWebAuthnService {
return DefaultService(); return DefaultService();
} }
struct TransactionState {
nsCOMPtr<nsIWebAuthnService> service;
};
using TransactionStateMutex = DataMutex<Maybe<TransactionState>>;
TransactionStateMutex mTransactionState;
nsCOMPtr<nsIWebAuthnService> mAuthrsService; nsCOMPtr<nsIWebAuthnService> mAuthrsService;
nsCOMPtr<nsIWebAuthnService> mPlatformService; nsCOMPtr<nsIWebAuthnService> mPlatformService;
}; };

View File

@@ -411,12 +411,14 @@ WinWebAuthnService::MakeCredential(uint64_t aTransactionId,
// AttestationConveyance // AttestationConveyance
nsString attestation; nsString attestation;
Unused << aArgs->GetAttestationConveyancePreference(attestation); Unused << aArgs->GetAttestationConveyancePreference(attestation);
bool anonymize = false;
// This mapping needs to be reviewed if values are added to the // This mapping needs to be reviewed if values are added to the
// AttestationConveyancePreference enum. // AttestationConveyancePreference enum.
static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3);
if (attestation.EqualsLiteral( if (attestation.EqualsLiteral(
MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE)) { MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE)) {
winAttestation = WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE; winAttestation = WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE;
anonymize = true;
} else if ( } else if (
attestation.EqualsLiteral( attestation.EqualsLiteral(
MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT)) { MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT)) {
@@ -577,6 +579,13 @@ WinWebAuthnService::MakeCredential(uint64_t aTransactionId,
} }
gWinWebauthnFreeCredentialAttestation(pWebAuthNCredentialAttestation); gWinWebauthnFreeCredentialAttestation(pWebAuthNCredentialAttestation);
if (anonymize) {
nsresult rv = result->Anonymize();
if (NS_FAILED(rv)) {
aPromise->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
return;
}
}
aPromise->Resolve(result); aPromise->Resolve(result);
} else { } else {
PCWSTR errorName = gWinWebauthnGetErrorName(hr); PCWSTR errorName = gWinWebauthnGetErrorName(hr);
@@ -968,8 +977,8 @@ WinWebAuthnService::PinCallback(uint64_t aTransactionId,
} }
NS_IMETHODIMP NS_IMETHODIMP
WinWebAuthnService::SetHasAttestationConsent(uint64_t aTransactionId, WinWebAuthnService::ResumeMakeCredential(uint64_t aTransactionId,
bool aHasConsent) { bool aForceNoneAttestation) {
return NS_ERROR_NOT_IMPLEMENTED; return NS_ERROR_NOT_IMPLEMENTED;
} }

View File

@@ -10,7 +10,7 @@ extern crate xpcom;
use authenticator::{ use authenticator::{
authenticatorservice::{RegisterArgs, SignArgs}, authenticatorservice::{RegisterArgs, SignArgs},
ctap2::attestation::{AAGuid, AttestationObject, AttestationStatement}, ctap2::attestation::AttestationObject,
ctap2::commands::{get_info::AuthenticatorVersion, PinUvAuthResult}, ctap2::commands::{get_info::AuthenticatorVersion, PinUvAuthResult},
ctap2::server::{ ctap2::server::{
AuthenticationExtensionsClientInputs, AuthenticatorAttachment, AuthenticationExtensionsClientInputs, AuthenticatorAttachment,
@@ -34,7 +34,6 @@ use nserror::{
use nsstring::{nsACString, nsAString, nsCString, nsString}; use nsstring::{nsACString, nsAString, nsCString, nsString};
use serde::Serialize; use serde::Serialize;
use serde_json::json; use serde_json::json;
use std::cell::RefCell;
use std::fmt::Write; use std::fmt::Write;
use std::sync::mpsc::{channel, Receiver, RecvError, Sender}; use std::sync::mpsc::{channel, Receiver, RecvError, Sender};
use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::{Arc, Mutex, MutexGuard};
@@ -87,6 +86,7 @@ enum BrowserPromptType<'a> {
}, },
PinIsTooLong, PinIsTooLong,
PinIsTooShort, PinIsTooShort,
RegisterDirect,
UvInvalid { UvInvalid {
retries: Option<u8>, retries: Option<u8>,
}, },
@@ -166,8 +166,7 @@ fn cancel_prompts(tid: u64) -> Result<(), nsresult> {
#[xpcom(implement(nsIWebAuthnRegisterResult), atomic)] #[xpcom(implement(nsIWebAuthnRegisterResult), atomic)]
pub struct WebAuthnRegisterResult { pub struct WebAuthnRegisterResult {
// result is only borrowed mutably in `Anonymize`. result: RegisterResult,
result: RefCell<RegisterResult>,
} }
impl WebAuthnRegisterResult { impl WebAuthnRegisterResult {
@@ -179,13 +178,13 @@ impl WebAuthnRegisterResult {
xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>); xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>);
fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> { fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> {
let mut out = ThinVec::new(); let mut out = ThinVec::new();
serde_cbor::to_writer(&mut out, &self.result.borrow().att_obj).or(Err(NS_ERROR_FAILURE))?; serde_cbor::to_writer(&mut out, &self.result.att_obj).or(Err(NS_ERROR_FAILURE))?;
Ok(out) Ok(out)
} }
xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>); xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>);
fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> { fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> {
let Some(credential_data) = &self.result.borrow().att_obj.auth_data.credential_data else { let Some(credential_data) = &self.result.att_obj.auth_data.credential_data else {
return Err(NS_ERROR_FAILURE); return Err(NS_ERROR_FAILURE);
}; };
Ok(credential_data.credential_id.as_slice().into()) Ok(credential_data.credential_id.as_slice().into())
@@ -198,7 +197,7 @@ impl WebAuthnRegisterResult {
// In tests, the result is not very important, but we can at least return "internal" if // In tests, the result is not very important, but we can at least return "internal" if
// we're simulating platform attachment. // we're simulating platform attachment.
if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") if static_prefs::pref!("security.webauth.webauthn_enable_softtoken")
&& self.result.borrow().attachment == AuthenticatorAttachment::Platform && self.result.attachment == AuthenticatorAttachment::Platform
{ {
Ok(thin_vec![nsString::from("internal")]) Ok(thin_vec![nsString::from("internal")])
} else { } else {
@@ -208,7 +207,7 @@ impl WebAuthnRegisterResult {
xpcom_method!(get_hmac_create_secret => GetHmacCreateSecret() -> bool); xpcom_method!(get_hmac_create_secret => GetHmacCreateSecret() -> bool);
fn get_hmac_create_secret(&self) -> Result<bool, nsresult> { fn get_hmac_create_secret(&self) -> Result<bool, nsresult> {
let Some(hmac_create_secret) = self.result.borrow().extensions.hmac_create_secret else { let Some(hmac_create_secret) = self.result.extensions.hmac_create_secret else {
return Err(NS_ERROR_NOT_AVAILABLE); return Err(NS_ERROR_NOT_AVAILABLE);
}; };
Ok(hmac_create_secret) Ok(hmac_create_secret)
@@ -216,7 +215,7 @@ impl WebAuthnRegisterResult {
xpcom_method!(get_cred_props_rk => GetCredPropsRk() -> bool); xpcom_method!(get_cred_props_rk => GetCredPropsRk() -> bool);
fn get_cred_props_rk(&self) -> Result<bool, nsresult> { fn get_cred_props_rk(&self) -> Result<bool, nsresult> {
let Some(cred_props) = &self.result.borrow().extensions.cred_props else { let Some(cred_props) = &self.result.extensions.cred_props else {
return Err(NS_ERROR_NOT_AVAILABLE); return Err(NS_ERROR_NOT_AVAILABLE);
}; };
Ok(cred_props.rk) Ok(cred_props.rk)
@@ -229,29 +228,12 @@ impl WebAuthnRegisterResult {
xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString); xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString);
fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> { fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> {
match self.result.borrow().attachment { match self.result.attachment {
AuthenticatorAttachment::CrossPlatform => Ok(nsString::from("cross-platform")), AuthenticatorAttachment::CrossPlatform => Ok(nsString::from("cross-platform")),
AuthenticatorAttachment::Platform => Ok(nsString::from("platform")), AuthenticatorAttachment::Platform => Ok(nsString::from("platform")),
AuthenticatorAttachment::Unknown => Err(NS_ERROR_NOT_AVAILABLE), 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)] #[xpcom(implement(nsIWebAuthnAttObj), atomic)]
@@ -293,17 +275,6 @@ impl WebAuthnAttObj {
// safe to cast to i32 by inspection of defined values // safe to cast to i32 by inspection of defined values
Ok(credential_data.credential_public_key.alg as i32) 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)] #[xpcom(implement(nsIWebAuthnSignResult), atomic)]
@@ -519,9 +490,8 @@ impl RegisterPromise {
fn resolve_or_reject(&self, result: Result<RegisterResult, nsresult>) -> Result<(), nsresult> { fn resolve_or_reject(&self, result: Result<RegisterResult, nsresult>) -> Result<(), nsresult> {
match result { match result {
Ok(result) => { Ok(result) => {
let wrapped_result = WebAuthnRegisterResult::allocate(InitWebAuthnRegisterResult { let wrapped_result =
result: RefCell::new(result), WebAuthnRegisterResult::allocate(InitWebAuthnRegisterResult { result })
})
.query_interface::<nsIWebAuthnRegisterResult>() .query_interface::<nsIWebAuthnRegisterResult>()
.ok_or(NS_ERROR_FAILURE)?; .ok_or(NS_ERROR_FAILURE)?;
unsafe { self.0.Resolve(wrapped_result.coerce()) }; unsafe { self.0.Resolve(wrapped_result.coerce()) };
@@ -573,6 +543,7 @@ impl TransactionPromise {
} }
enum TransactionArgs { enum TransactionArgs {
Register(/* timeout */ u64, RegisterArgs),
Sign(/* timeout */ u64, SignArgs), Sign(/* timeout */ u64, SignArgs),
} }
@@ -742,6 +713,13 @@ 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; let mut cred_props = false;
unsafe { args.GetCredProps(&mut cred_props) }.to_result()?; unsafe { args.GetCredProps(&mut cred_props) }.to_result()?;
@@ -782,19 +760,51 @@ impl AuthrsService {
use_ctap1_fallback: !static_prefs::pref!("security.webauthn.ctap2"), use_ctap1_fallback: !static_prefs::pref!("security.webauthn.ctap2"),
}; };
let mut guard = self.transaction.lock().unwrap(); *self.transaction.lock().unwrap() = Some(TransactionState {
*guard = Some(TransactionState {
tid, tid,
browsing_context_id, browsing_context_id,
pending_args: None, pending_args: Some(TransactionArgs::Register(timeout_ms as u64, info)),
promise: TransactionPromise::Register(promise), promise: TransactionPromise::Register(promise),
pin_receiver: None, pin_receiver: None,
selection_receiver: None, selection_receiver: None,
interactive_receiver: None, interactive_receiver: None,
puat_cache: None, puat_cache: None,
}); });
// drop the guard here to ensure we don't deadlock if the call to `register()` below
// hairpins the state callback. 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(guard); drop(guard);
let (status_tx, status_rx) = channel::<StatusUpdate>(); let (status_tx, status_rx) = channel::<StatusUpdate>();
@@ -815,7 +825,7 @@ impl AuthrsService {
let callback_transaction = self.transaction.clone(); let callback_transaction = self.transaction.clone();
let callback_origin = info.origin.clone(); let callback_origin = info.origin.clone();
let state_callback = StateCallback::<Result<RegisterResult, AuthenticatorError>>::new( let state_callback = StateCallback::<Result<RegisterResult, AuthenticatorError>>::new(
Box::new(move |result| { Box::new(move |mut result| {
let mut guard = callback_transaction.lock().unwrap(); let mut guard = callback_transaction.lock().unwrap();
let Some(state) = guard.as_mut() else { let Some(state) = guard.as_mut() else {
return; return;
@@ -826,6 +836,13 @@ impl AuthrsService {
let TransactionPromise::Register(ref promise) = state.promise else { let TransactionPromise::Register(ref promise) = state.promise else {
return; 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 { if let Err(AuthenticatorError::CredentialExcluded) = result {
let _ = send_prompt( let _ = send_prompt(
BrowserPromptType::AlreadyRegistered, BrowserPromptType::AlreadyRegistered,
@@ -857,14 +874,14 @@ impl AuthrsService {
Some(browsing_context_id), Some(browsing_context_id),
)?; )?;
self.usb_token_manager.lock().unwrap().register( self.usb_token_manager.lock().unwrap().register(
timeout_ms.into(), timeout_ms,
info, info,
status_tx, status_tx,
state_callback, state_callback,
); );
} else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") {
self.test_token_manager self.test_token_manager
.register(timeout_ms.into(), info, status_tx, state_callback); .register(timeout_ms, info, status_tx, state_callback);
} else { } else {
return Err(NS_ERROR_FAILURE); return Err(NS_ERROR_FAILURE);
} }
@@ -872,11 +889,6 @@ impl AuthrsService {
Ok(()) 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)); xpcom_method!(get_assertion => GetAssertion(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsIWebAuthnSignArgs, aPromise: *const nsIWebAuthnSignPromise));
fn get_assertion( fn get_assertion(
&self, &self,

View File

@@ -17,6 +17,4 @@ interface nsIWebAuthnAttObj : nsISupports {
readonly attribute Array<octet> publicKey; readonly attribute Array<octet> publicKey;
readonly attribute COSEAlgorithmIdentifier publicKeyAlgorithm; readonly attribute COSEAlgorithmIdentifier publicKeyAlgorithm;
bool isIdentifying();
}; };

View File

@@ -28,9 +28,6 @@ interface nsIWebAuthnRegisterResult : nsISupports {
[must_use] attribute boolean credPropsRk; [must_use] attribute boolean credPropsRk;
[must_use] readonly attribute AString authenticatorAttachment; [must_use] readonly attribute AString authenticatorAttachment;
bool hasIdentifyingAttestation();
void anonymize();
}; };
// The nsIWebAuthnSignResult interface is used to construct IPDL-defined // The nsIWebAuthnSignResult interface is used to construct IPDL-defined

View File

@@ -85,7 +85,7 @@ interface nsIWebAuthnService : nsISupports
void resumeConditionalGet(in uint64_t aTransactionId); void resumeConditionalGet(in uint64_t aTransactionId);
void pinCallback(in uint64_t aTransactionId, in ACString aPin); void pinCallback(in uint64_t aTransactionId, in ACString aPin);
void setHasAttestationConsent(in uint64_t aTransactionId, in boolean aHasConsent); void resumeMakeCredential(in uint64_t aTransactionId, in boolean aForceNoneAttestation);
void selectionCallback(in uint64_t aTransactionId, in uint64_t aIndex); void selectionCallback(in uint64_t aTransactionId, in uint64_t aIndex);
// Adds a virtual (software) authenticator for use in tests (particularly // Adds a virtual (software) authenticator for use in tests (particularly

View File

@@ -46,13 +46,15 @@ add_task(async function test_webauthn_modal_request_cancels_conditional_get() {
ok(active, "conditional request should still be active"); ok(active, "conditional request should still be active");
let promptPromise = promiseNotification("webauthn-prompt-register-direct"); let promptPromise = promiseNotification("webauthn-prompt-register-direct");
let modalPromise = promiseWebAuthnMakeCredential(tab, "direct"); let modalPromise = promiseWebAuthnMakeCredential(tab, "direct")
.then(arrivingHereIsBad)
.catch(gExpectNotAllowedError);
await condPromise; await condPromise;
ok(!active, "conditional request should not be active"); ok(!active, "conditional request should not be active");
// Proceed through the consent prompt // Cancel the modal request with the button.
await promptPromise; await promptPromise;
PopupNotifications.panel.firstElementChild.secondaryButton.click(); PopupNotifications.panel.firstElementChild.secondaryButton.click();
await modalPromise; await modalPromise;

View File

@@ -43,26 +43,34 @@ add_task(async function test_setup_usbtoken() {
}); });
add_task(test_register); add_task(test_register);
add_task(test_register_escape); 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);
add_task(test_sign_escape); add_task(test_sign_escape);
add_task(test_tab_switching); add_task(test_tab_switching);
add_task(test_window_switching); add_task(test_window_switching);
add_task(async function test_setup_softtoken() { add_task(async function test_setup_fullscreen() {
gAuthenticatorId = add_virtual_authenticator();
return SpecialPowers.pushPrefEnv({ return SpecialPowers.pushPrefEnv({
set: [ set: [
["browser.fullscreen.autohide", true], ["browser.fullscreen.autohide", true],
["full-screen-api.enabled", true], ["full-screen-api.enabled", true],
["full-screen-api.allow-trusted-requests-only", false], ["full-screen-api.allow-trusted-requests-only", false],
["security.webauth.webauthn_enable_softtoken", true],
["security.webauth.webauthn_enable_usbtoken", false],
], ],
}); });
}); });
add_task(test_fullscreen_show_nav_toolbar); add_task(test_fullscreen_show_nav_toolbar);
add_task(test_no_fullscreen_dom); add_task(test_no_fullscreen_dom);
add_task(test_register_direct_with_consent); add_task(async function test_setup_softtoken() {
add_task(test_register_direct_without_consent); 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_select_sign_result); add_task(test_select_sign_result);
function promiseNavToolboxStatus(aExpectedStatus) { function promiseNavToolboxStatus(aExpectedStatus) {
@@ -207,6 +215,53 @@ async function test_sign_escape() {
await BrowserTestUtils.removeTab(tab); 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 // 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. // not visible, switch back, assert the prompt is there and cancel it.
async function test_tab_switching() { async function test_tab_switching() {
@@ -304,7 +359,7 @@ async function test_window_switching() {
await BrowserTestUtils.removeTab(tab); await BrowserTestUtils.removeTab(tab);
} }
async function test_register_direct_with_consent() { async function test_register_direct_proceed() {
// Open a new tab. // Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
@@ -312,7 +367,7 @@ async function test_register_direct_with_consent() {
let request = promiseWebAuthnMakeCredential(tab, "direct"); let request = promiseWebAuthnMakeCredential(tab, "direct");
await promiseNotification("webauthn-prompt-register-direct"); await promiseNotification("webauthn-prompt-register-direct");
// Click "Allow". // Proceed.
PopupNotifications.panel.firstElementChild.button.click(); PopupNotifications.panel.firstElementChild.button.click();
// Ensure we got "direct" attestation. // Ensure we got "direct" attestation.
@@ -322,7 +377,7 @@ async function test_register_direct_with_consent() {
await BrowserTestUtils.removeTab(tab); await BrowserTestUtils.removeTab(tab);
} }
async function test_register_direct_without_consent() { async function test_register_direct_proceed_anon() {
// Open a new tab. // Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
@@ -330,8 +385,9 @@ async function test_register_direct_without_consent() {
let request = promiseWebAuthnMakeCredential(tab, "direct"); let request = promiseWebAuthnMakeCredential(tab, "direct");
await promiseNotification("webauthn-prompt-register-direct"); await promiseNotification("webauthn-prompt-register-direct");
// Click "Block". // Check "anonymize anyway" and proceed.
PopupNotifications.panel.firstElementChild.secondaryButton.click(); PopupNotifications.panel.firstElementChild.checkbox.checked = true;
PopupNotifications.panel.firstElementChild.button.click();
// Ensure we got "none" attestation. // Ensure we got "none" attestation.
await request.then(verifyAnonymizedCertificate); await request.then(verifyAnonymizedCertificate);
@@ -382,22 +438,23 @@ async function test_fullscreen_show_nav_toolbar() {
await navToolboxHiddenPromise; await navToolboxHiddenPromise;
// Request a new credential with direct attestation. The consent prompt will // Request a new credential and wait for the direct attestation consent
// keep the request active until we can verify that the nav toolbar is shown. // prompt.
let promptPromise = promiseNotification("webauthn-prompt-register-direct"); let promptPromise = promiseNotification("webauthn-prompt-register-direct");
let navToolboxShownPromise = promiseNavToolboxStatus("shown"); let navToolboxShownPromise = promiseNavToolboxStatus("shown");
let active = true; let active = true;
let requestPromise = promiseWebAuthnMakeCredential(tab, "direct").then( let requestPromise = promiseWebAuthnMakeCredential(tab, "direct")
() => (active = false) .then(arrivingHereIsBad)
); .catch(expectNotAllowedError)
.then(() => (active = false));
await Promise.all([promptPromise, navToolboxShownPromise]); await Promise.all([promptPromise, navToolboxShownPromise]);
ok(active, "request is active"); ok(active, "request is active");
ok(window.fullScreen, "window is fullscreen"); ok(window.fullScreen, "window is fullscreen");
// Proceed through the consent prompt. // Cancel the request.
PopupNotifications.panel.firstElementChild.secondaryButton.click(); PopupNotifications.panel.firstElementChild.secondaryButton.click();
await requestPromise; await requestPromise;
@@ -418,22 +475,23 @@ async function test_no_fullscreen_dom() {
await fullScreenPaintPromise; await fullScreenPaintPromise;
ok(!!document.fullscreenElement, "a DOM element is fullscreen"); ok(!!document.fullscreenElement, "a DOM element is fullscreen");
// Request a new credential with direct attestation. The consent prompt will // Request a new credential and wait for the direct attestation consent
// keep the request active until we can verify that we've left fullscreen. // prompt.
let promptPromise = promiseNotification("webauthn-prompt-register-direct"); let promptPromise = promiseNotification("webauthn-prompt-register-direct");
fullScreenPaintPromise = promiseFullScreenPaint(); fullScreenPaintPromise = promiseFullScreenPaint();
let active = true; let active = true;
let requestPromise = promiseWebAuthnMakeCredential(tab, "direct").then( let requestPromise = promiseWebAuthnMakeCredential(tab, "direct")
() => (active = false) .then(arrivingHereIsBad)
); .catch(expectNotAllowedError)
.then(() => (active = false));
await Promise.all([promptPromise, fullScreenPaintPromise]); await Promise.all([promptPromise, fullScreenPaintPromise]);
ok(active, "request is active"); ok(active, "request is active");
ok(!document.fullscreenElement, "no DOM element is fullscreen"); ok(!document.fullscreenElement, "no DOM element is fullscreen");
// Proceed through the consent prompt. // Cancel the request.
await waitForPopupNotificationSecurityDelay(); await waitForPopupNotificationSecurityDelay();
PopupNotifications.panel.firstElementChild.secondaryButton.click(); PopupNotifications.panel.firstElementChild.secondaryButton.click();
await requestPromise; await requestPromise;