Bug 1855048 - move webauthn prompt logic from WebAuthnController to authrs_bridge. r=keeler,fluent-reviewers,flod

Differential Revision: https://phabricator.services.mozilla.com/D189168
This commit is contained in:
John Schanck 2023-09-26 22:41:53 +00:00
parent 58364b15c1
commit 58260a36c4
11 changed files with 511 additions and 629 deletions

2
Cargo.lock generated
View File

@ -327,11 +327,13 @@ version = "0.1.0"
dependencies = [
"authenticator",
"base64 0.21.3",
"cstr",
"log",
"moz_task",
"nserror",
"nsstring",
"rand",
"serde",
"serde_cbor",
"serde_json",
"static_prefs",

View File

@ -7527,7 +7527,7 @@ var WebAuthnPromptHelper = {
// If we receive a cancel, it might be a WebAuthn prompt starting in another
// window, and the other window's browsing context will send out the
// cancellations, so any cancel action we get should prompt us to cancel.
if (data.action == "cancel") {
if (data.prompt.type == "cancel") {
this.cancel(data);
return;
}
@ -7539,17 +7539,21 @@ var WebAuthnPromptHelper = {
return;
}
let mgr = aSubject.QueryInterface(Ci.nsIWebAuthnController);
let mgr = Cc["@mozilla.org/webauthn/transport;1"].getService(
Ci.nsIWebAuthnTransport
);
if (data.action == "presence") {
if (data.prompt.type == "presence") {
this.presence_required(mgr, data);
} else if (data.action == "register-direct") {
} else if (data.prompt.type == "register-direct") {
this.registerDirect(mgr, data);
} else if (data.action == "pin-required") {
this.pin_required(mgr, data);
} else if (data.action == "select-sign-result") {
} else if (data.prompt.type == "pin-required") {
this.pin_required(mgr, false, data);
} else if (data.prompt.type == "pin-invalid") {
this.pin_required(mgr, true, data);
} else if (data.prompt.type == "select-sign-result") {
this.select_sign_result(mgr, data);
} else if (data.action == "already-registered") {
} else if (data.prompt.type == "already-registered") {
this.show_info(
mgr,
data.origin,
@ -7557,7 +7561,7 @@ var WebAuthnPromptHelper = {
"alreadyRegistered",
"webauthn.alreadyRegisteredPrompt"
);
} else if (data.action == "select-device") {
} else if (data.prompt.type == "select-device") {
this.show_info(
mgr,
data.origin,
@ -7565,7 +7569,7 @@ var WebAuthnPromptHelper = {
"selectDevice",
"webauthn.selectDevicePrompt"
);
} else if (data.action == "pin-auth-blocked") {
} else if (data.prompt.type == "pin-auth-blocked") {
this.show_info(
mgr,
data.origin,
@ -7573,7 +7577,7 @@ var WebAuthnPromptHelper = {
"pinAuthBlocked",
"webauthn.pinAuthBlockedPrompt"
);
} else if (data.action == "uv-blocked") {
} else if (data.prompt.type == "uv-blocked") {
this.show_info(
mgr,
data.origin,
@ -7581,8 +7585,8 @@ var WebAuthnPromptHelper = {
"uvBlocked",
"webauthn.uvBlockedPrompt"
);
} else if (data.action == "uv-invalid") {
let retriesLeft = data.retriesLeft;
} else if (data.prompt.type == "uv-invalid") {
let retriesLeft = data.prompt.retries;
let dialogText;
if (retriesLeft == 0) {
// We can skip that because it will either be replaced
@ -7600,7 +7604,7 @@ var WebAuthnPromptHelper = {
}
let mainAction = this.buildCancelAction(mgr, data.tid);
this.show_formatted_msg(data.tid, "uvInvalid", dialogText, mainAction);
} else if (data.action == "device-blocked") {
} else if (data.prompt.type == "device-blocked") {
this.show_info(
mgr,
data.origin,
@ -7608,7 +7612,7 @@ var WebAuthnPromptHelper = {
"deviceBlocked",
"webauthn.deviceBlockedPrompt"
);
} else if (data.action == "pin-not-set") {
} else if (data.prompt.type == "pin-not-set") {
this.show_info(
mgr,
data.origin,
@ -7651,14 +7655,18 @@ var WebAuthnPromptHelper = {
return res;
},
select_sign_result(mgr, { origin, tid, usernames }) {
select_sign_result(mgr, { origin, tid, prompt: { entities } }) {
let unknownAccount = this._l10n.formatValueSync(
"webauthn-select-sign-result-unknown-account"
);
let secondaryActions = [];
for (let i = 0; i < usernames.length; i++) {
for (let i = 0; i < entities.length; i++) {
let label = entities[i].name ?? unknownAccount;
secondaryActions.push({
label: usernames[i],
label,
accessKey: i.toString(),
callback(aState) {
mgr.signatureSelectionCallback(tid, i);
mgr.selectionCallback(tid, i);
},
});
}
@ -7675,14 +7683,9 @@ var WebAuthnPromptHelper = {
);
},
pin_required(mgr, { origin, tid, wasInvalid, retriesLeft }) {
pin_required(mgr, wasInvalid, { origin, tid, prompt: { retries } }) {
let aPassword = Object.create(null); // create a "null" object
let res = this.prompt_for_password(
origin,
wasInvalid,
retriesLeft,
aPassword
);
let res = this.prompt_for_password(origin, wasInvalid, retries, aPassword);
if (res) {
mgr.pinCallback(tid, aPassword.value);
} else {
@ -7846,7 +7849,7 @@ var WebAuthnPromptHelper = {
label: gNavigatorBundle.getString("webauthn.proceed"),
accessKey: gNavigatorBundle.getString("webauthn.proceed.accesskey"),
callback(state) {
mgr.resumeRegister(tid, state.checkboxChecked);
mgr.resumeMakeCredential(tid, state.checkboxChecked);
},
};
},

View File

@ -12,6 +12,8 @@ webauthn-pin-invalid-long-prompt =
webauthn-pin-invalid-short-prompt = Incorrect PIN. Try again.
webauthn-pin-required-prompt = Please enter the PIN for your device.
webauthn-select-sign-result-unknown-account = Unknown account
# Variables:
# $retriesLeft (Number): number of tries left
webauthn-uv-invalid-long-prompt =

View File

@ -153,13 +153,7 @@ NS_IMETHODIMP
CtapRegisterArgs::GetAttestationConveyancePreference(
nsAString& aAttestationConveyancePreference) {
mozilla::ipc::AssertIsOnBackgroundThread();
if (mForceNoneAttestation) {
aAttestationConveyancePreference = NS_ConvertUTF8toUTF16(
MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE);
} else {
aAttestationConveyancePreference = mInfo.attestationConveyancePreference();
}
aAttestationConveyancePreference = mInfo.attestationConveyancePreference();
return NS_OK;
}

View File

@ -25,10 +25,8 @@ class CtapRegisterArgs final : public nsICtapRegisterArgs {
NS_DECL_ISUPPORTS
NS_DECL_NSICTAPREGISTERARGS
explicit CtapRegisterArgs(const WebAuthnMakeCredentialInfo& aInfo,
bool aForceNoneAttestation)
explicit CtapRegisterArgs(const WebAuthnMakeCredentialInfo& aInfo)
: mInfo(aInfo),
mForceNoneAttestation(aForceNoneAttestation),
mCredProps(false),
mHmacCreateSecret(false),
mMinPinLength(false) {
@ -58,7 +56,6 @@ class CtapRegisterArgs final : public nsICtapRegisterArgs {
~CtapRegisterArgs() = default;
const WebAuthnMakeCredentialInfo& mInfo;
const bool mForceNoneAttestation;
// Flags to indicate whether an extension is being requested.
bool mCredProps;

View File

@ -42,18 +42,8 @@ StaticRefPtr<WebAuthnController> gWebAuthnController;
static nsIThread* gWebAuthnBackgroundThread;
} // namespace
// Data for WebAuthn UI prompt notifications.
static const char16_t kPresencePromptNotification[] =
u"{\"action\":\"presence\",\"tid\":%llu,"
u"\"origin\":\"%s\",\"browsingContextId\":%llu}";
static const char16_t kRegisterDirectPromptNotification[] =
u"{\"action\":\"register-direct\",\"tid\":%llu,"
u"\"origin\":\"%s\",\"browsingContextId\":%llu}";
static const char16_t kCancelPromptNotification[] =
u"{\"action\":\"cancel\",\"tid\":%llu}";
/***********************************************************************
* U2FManager Implementation
* WebAuthnController Implementation
**********************************************************************/
NS_IMPL_ISUPPORTS(WebAuthnController, nsIWebAuthnController);
@ -82,91 +72,43 @@ WebAuthnController* WebAuthnController::Get() {
return gWebAuthnController;
}
void WebAuthnController::AbortTransaction(const uint64_t& aTransactionId,
const nsresult& aError,
bool shouldCancelActiveDialog) {
if (mTransactionParent && mTransaction.isSome() && aTransactionId > 0 &&
aTransactionId == mTransaction.ref().mTransactionId) {
Unused << mTransactionParent->SendAbort(aTransactionId, aError);
ClearTransaction(shouldCancelActiveDialog);
}
}
void WebAuthnController::AbortOngoingTransaction() {
if (mTransaction.isSome()) {
AbortTransaction(mTransaction.ref().mTransactionId, NS_ERROR_DOM_ABORT_ERR,
true);
void WebAuthnController::AbortTransaction(
const nsresult& aError = NS_ERROR_DOM_NOT_ALLOWED_ERR) {
MOZ_ASSERT(XRE_IsParentProcess());
mozilla::ipc::AssertIsOnBackgroundThread();
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
("WebAuthnController::AbortTransaction"));
if (mTransactionParent && mTransactionId.isSome()) {
Unused << mTransactionParent->SendAbort(mTransactionId.ref(), aError);
}
ClearTransaction();
}
void WebAuthnController::MaybeClearTransaction(
PWebAuthnTransactionParent* aParent) {
MOZ_ASSERT(XRE_IsParentProcess());
mozilla::ipc::AssertIsOnBackgroundThread();
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
("WebAuthnController::MaybeClearTransaction"));
// Only clear if we've been requested to do so by our current transaction
// parent.
if (mTransactionParent == aParent) {
ClearTransaction(true);
ClearTransaction();
}
}
void WebAuthnController::ClearTransaction(bool cancel_prompt) {
if (cancel_prompt && mTransaction.isSome() &&
mTransaction.ref().mTransactionId > 0) {
// Remove any prompts we might be showing for the current transaction.
SendPromptNotification(kCancelPromptNotification,
mTransaction.ref().mTransactionId);
}
mTransactionParent = nullptr;
// Forget any pending registration.
mPendingRegisterInfo.reset();
mPendingSignInfo.reset();
mTransaction.reset();
}
template <typename... T>
void WebAuthnController::SendPromptNotification(const char16_t* aFormat,
T... aArgs) {
MOZ_ASSERT(!NS_IsMainThread());
nsAutoString json;
nsTextFormatter::ssprintf(json, aFormat, aArgs...);
nsCOMPtr<nsIRunnable> r(NewRunnableMethod<nsString>(
"WebAuthnController::RunSendPromptNotification", this,
&WebAuthnController::RunSendPromptNotification, json));
MOZ_ALWAYS_SUCCEEDS(GetMainThreadSerialEventTarget()->Dispatch(
r.forget(), NS_DISPATCH_NORMAL));
}
NS_IMETHODIMP
WebAuthnController::SendPromptNotificationPreformatted(
uint64_t aTransactionId, const nsACString& aJson) {
void WebAuthnController::ClearTransaction() {
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_ASSERT(!NS_IsMainThread());
nsCOMPtr<nsIRunnable> r(NewRunnableMethod<nsString>(
"WebAuthnController::RunSendPromptNotification", this,
&WebAuthnController::RunSendPromptNotification,
NS_ConvertUTF8toUTF16(aJson)));
MOZ_ALWAYS_SUCCEEDS(GetMainThreadSerialEventTarget()->Dispatch(
r.forget(), NS_DISPATCH_NORMAL));
return NS_OK;
}
void WebAuthnController::RunSendPromptNotification(const nsString& aJSON) {
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIObserverService> os = services::GetObserverService();
if (NS_WARN_IF(!os)) {
return;
}
nsCOMPtr<nsIWebAuthnController> self = this;
MOZ_ALWAYS_SUCCEEDS(
os->NotifyObservers(self, "webauthn-prompt", aJSON.get()));
mozilla::ipc::AssertIsOnBackgroundThread();
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
("WebAuthnController::ClearTransaction"));
mTransactionParent = nullptr;
mPendingClientData.reset();
mTransactionId.reset();
}
nsCOMPtr<nsIWebAuthnTransport> WebAuthnController::GetTransportImpl() {
MOZ_ASSERT(XRE_IsParentProcess());
mozilla::ipc::AssertIsOnBackgroundThread();
if (mTransportImpl) {
@ -179,159 +121,54 @@ nsCOMPtr<nsIWebAuthnTransport> WebAuthnController::GetTransportImpl() {
return transport;
}
void WebAuthnController::Cancel(PWebAuthnTransactionParent* aTransactionParent,
const Tainted<uint64_t>& aTransactionId) {
// The last transaction ID also suffers from the issue described in Bug
// 1696159. A content process could cancel another content processes
// transaction by guessing the last transaction ID.
if (mTransactionParent != aTransactionParent || mTransaction.isNothing() ||
!MOZ_IS_VALID(aTransactionId,
mTransaction.ref().mTransactionId == aTransactionId)) {
return;
}
if (mTransportImpl) {
mTransportImpl->Cancel();
}
ClearTransaction(true);
}
void WebAuthnController::Register(
PWebAuthnTransactionParent* aTransactionParent,
const uint64_t& aTransactionId, const WebAuthnMakeCredentialInfo& aInfo) {
MOZ_ASSERT(XRE_IsParentProcess());
mozilla::ipc::AssertIsOnBackgroundThread();
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
("WebAuthnController::Register"));
MOZ_ASSERT(aTransactionId > 0);
if (!gWebAuthnBackgroundThread) {
gWebAuthnBackgroundThread = NS_GetCurrentThread();
MOZ_ASSERT(gWebAuthnBackgroundThread, "This should never be null!");
}
AbortOngoingTransaction();
mTransactionParent = aTransactionParent;
// Hold on to any state that we need to finish the transaction.
mTransaction = Some(Transaction(aTransactionId, aInfo.ClientDataJSON()));
MOZ_ASSERT(mPendingRegisterInfo.isNothing());
mPendingRegisterInfo = Some(aInfo);
// Determine whether direct attestation was requested.
bool noneAttestationRequested = true;
// On Android, let's always reject direct attestations until we have a
// mechanism to solicit user consent, from Bug 1550164
#ifndef MOZ_WIDGET_ANDROID
// The default attestation type is "none", so set
// noneAttestationRequested=false only if the RP's preference matches one of
// the other known types. This needs to be reviewed if values are added to
// the AttestationConveyancePreference enum.
const nsString& attestation = aInfo.attestationConveyancePreference();
static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 2);
if (attestation.EqualsLiteral(
MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT) ||
attestation.EqualsLiteral(
MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT) ||
attestation.EqualsLiteral(
MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE)) {
noneAttestationRequested = false;
}
#endif // not MOZ_WIDGET_ANDROID
// Start a register request immediately if direct attestation
// wasn't requested or the test pref is set.
if (noneAttestationRequested ||
StaticPrefs::
security_webauth_webauthn_testing_allow_direct_attestation()) {
DoRegister(aInfo, noneAttestationRequested);
return;
}
// If the RP request direct attestation, ask the user for permission and
// store the transaction info until the user proceeds or cancels.
NS_ConvertUTF16toUTF8 origin(aInfo.Origin());
SendPromptNotification(kRegisterDirectPromptNotification, aTransactionId,
origin.get(), aInfo.BrowsingContextId());
}
void WebAuthnController::DoRegister(const WebAuthnMakeCredentialInfo& aInfo,
bool aForceNoneAttestation) {
mozilla::ipc::AssertIsOnBackgroundThread();
MOZ_ASSERT(mTransaction.isSome());
if (NS_WARN_IF(mTransaction.isNothing())) {
// Clear prompt?
return;
}
// Show a prompt that lets the user cancel the ongoing transaction.
NS_ConvertUTF16toUTF8 origin(aInfo.Origin());
SendPromptNotification(kPresencePromptNotification,
mTransaction.ref().mTransactionId, origin.get(),
aInfo.BrowsingContextId(), "false");
RefPtr<CtapRegisterArgs> args(
new CtapRegisterArgs(aInfo, aForceNoneAttestation));
// Abort ongoing transaction, if any.
AbortTransaction(NS_ERROR_DOM_ABORT_ERR);
mTransportImpl = GetTransportImpl();
if (!mTransportImpl) {
AbortTransaction(mTransaction.ref().mTransactionId,
NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
return;
}
nsresult rv = mTransportImpl->MakeCredential(
mTransaction.ref().mTransactionId, aInfo.BrowsingContextId(), args);
nsresult rv = mTransportImpl->Reset();
if (NS_FAILED(rv)) {
AbortTransaction(mTransaction.ref().mTransactionId,
NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
return;
}
}
NS_IMETHODIMP
WebAuthnController::ResumeRegister(uint64_t aTransactionId,
bool aForceNoneAttestation) {
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_ASSERT(NS_IsMainThread());
if (!gWebAuthnBackgroundThread) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t, bool>(
"WebAuthnController::RunResumeRegister", this,
&WebAuthnController::RunResumeRegister, aTransactionId,
aForceNoneAttestation));
if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) {
return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
}
return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
}
void WebAuthnController::RunResumeRegister(uint64_t aTransactionId,
bool aForceNoneAttestation) {
mozilla::ipc::AssertIsOnBackgroundThread();
if (NS_WARN_IF(mPendingRegisterInfo.isNothing())) {
AbortTransaction();
return;
}
if (mTransaction.isNothing() ||
mTransaction.ref().mTransactionId != aTransactionId) {
MOZ_ASSERT(aTransactionId > 0);
mTransactionParent = aTransactionParent;
mTransactionId = Some(aTransactionId);
mPendingClientData = Some(aInfo.ClientDataJSON());
RefPtr<CtapRegisterArgs> args(new CtapRegisterArgs(aInfo));
rv = mTransportImpl->MakeCredential(mTransactionId.ref(),
aInfo.BrowsingContextId(), args);
if (NS_FAILED(rv)) {
AbortTransaction();
return;
}
// Resume registration and cleanup.
DoRegister(mPendingRegisterInfo.ref(), aForceNoneAttestation);
}
NS_IMETHODIMP
WebAuthnController::FinishRegister(uint64_t aTransactionId,
nsICtapRegisterResult* aResult) {
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
("WebAuthnController::FinishRegister"));
nsCOMPtr<nsIRunnable> r(
NewRunnableMethod<uint64_t, RefPtr<nsICtapRegisterResult>>(
"WebAuthnController::RunFinishRegister", this,
@ -348,9 +185,12 @@ WebAuthnController::FinishRegister(uint64_t aTransactionId,
void WebAuthnController::RunFinishRegister(
uint64_t aTransactionId, const RefPtr<nsICtapRegisterResult>& aResult) {
MOZ_ASSERT(XRE_IsParentProcess());
mozilla::ipc::AssertIsOnBackgroundThread();
if (mTransaction.isNothing() ||
aTransactionId != mTransaction.ref().mTransactionId) {
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
("WebAuthnController::RunFinishRegister"));
if (mTransactionId.isNothing() || mPendingClientData.isNothing() ||
aTransactionId != mTransactionId.ref()) {
// The previous transaction was likely cancelled from the prompt.
return;
}
@ -358,43 +198,34 @@ void WebAuthnController::RunFinishRegister(
nsresult status;
nsresult rv = aResult->GetStatus(&status);
if (NS_WARN_IF(NS_FAILED(rv))) {
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
return;
}
if (NS_FAILED(status)) {
bool shouldCancelActiveDialog = true;
if (status == NS_ERROR_DOM_INVALID_STATE_ERR) {
// PIN-related errors. Let the dialog show to inform the user
shouldCancelActiveDialog = false;
} else {
status = NS_ERROR_DOM_NOT_ALLOWED_ERR;
}
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
u"CTAPRegisterAbort"_ns, 1);
AbortTransaction(aTransactionId, status, shouldCancelActiveDialog);
AbortTransaction(status);
return;
}
nsCString clientDataJson = mPendingRegisterInfo.ref().ClientDataJSON();
nsTArray<uint8_t> attObj;
rv = aResult->GetAttestationObject(attObj);
if (NS_WARN_IF(NS_FAILED(rv))) {
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
return;
}
nsTArray<uint8_t> credentialId;
rv = aResult->GetCredentialId(credentialId);
if (NS_WARN_IF(NS_FAILED(rv))) {
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
return;
}
nsTArray<nsString> transports;
rv = aResult->GetTransports(transports);
if (NS_WARN_IF(NS_FAILED(rv))) {
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
return;
}
@ -403,26 +234,28 @@ void WebAuthnController::RunFinishRegister(
rv = aResult->GetCredPropsRk(&credPropsRk);
if (rv != NS_ERROR_NOT_AVAILABLE) {
if (NS_WARN_IF(NS_FAILED(rv))) {
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
return;
}
extensions.AppendElement(WebAuthnExtensionResultCredProps(credPropsRk));
}
WebAuthnMakeCredentialResult result(clientDataJson, attObj, credentialId,
transports, extensions);
WebAuthnMakeCredentialResult result(mPendingClientData.extract(), attObj,
credentialId, transports, extensions);
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
u"CTAPRegisterFinish"_ns, 1);
Unused << mTransactionParent->SendConfirmRegister(aTransactionId, result);
ClearTransaction(true);
ClearTransaction();
}
void WebAuthnController::Sign(PWebAuthnTransactionParent* aTransactionParent,
const uint64_t& aTransactionId,
const WebAuthnGetAssertionInfo& aInfo) {
MOZ_ASSERT(XRE_IsParentProcess());
mozilla::ipc::AssertIsOnBackgroundThread();
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug, ("WebAuthnSign"));
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
("WebAuthnController::Sign"));
MOZ_ASSERT(aTransactionId > 0);
if (!gWebAuthnBackgroundThread) {
@ -430,34 +263,30 @@ void WebAuthnController::Sign(PWebAuthnTransactionParent* aTransactionParent,
MOZ_ASSERT(gWebAuthnBackgroundThread, "This should never be null!");
}
AbortOngoingTransaction();
mTransactionParent = aTransactionParent;
// Hold on to any state that we need to finish the transaction.
mTransaction = Some(Transaction(aTransactionId, aInfo.ClientDataJSON()));
mPendingSignInfo = Some(aInfo);
// Show a prompt that lets the user cancel the ongoing transaction.
NS_ConvertUTF16toUTF8 origin(aInfo.Origin());
SendPromptNotification(kPresencePromptNotification,
mTransaction.ref().mTransactionId, origin.get(),
aInfo.BrowsingContextId(), "false");
RefPtr<CtapSignArgs> args(new CtapSignArgs(aInfo));
// Abort ongoing transaction, if any.
AbortTransaction(NS_ERROR_DOM_ABORT_ERR);
mTransportImpl = GetTransportImpl();
if (!mTransportImpl) {
AbortTransaction(mTransaction.ref().mTransactionId,
NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
return;
}
nsresult rv = mTransportImpl->GetAssertion(
mTransaction.ref().mTransactionId, aInfo.BrowsingContextId(), args.get());
nsresult rv = mTransportImpl->Reset();
if (NS_FAILED(rv)) {
AbortTransaction(mTransaction.ref().mTransactionId,
NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
return;
}
mTransactionParent = aTransactionParent;
mTransactionId = Some(aTransactionId);
mPendingClientData = Some(aInfo.ClientDataJSON());
RefPtr<CtapSignArgs> args(new CtapSignArgs(aInfo));
rv = mTransportImpl->GetAssertion(mTransactionId.ref(),
aInfo.BrowsingContextId(), args.get());
if (NS_FAILED(rv)) {
AbortTransaction();
return;
}
}
@ -466,6 +295,8 @@ NS_IMETHODIMP
WebAuthnController::FinishSign(uint64_t aTransactionId,
nsICtapSignResult* aResult) {
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
("WebAuthnController::FinishSign"));
nsCOMPtr<nsIRunnable> r(
NewRunnableMethod<uint64_t, RefPtr<nsICtapSignResult>>(
"WebAuthnController::RunFinishSign", this,
@ -482,50 +313,46 @@ WebAuthnController::FinishSign(uint64_t aTransactionId,
void WebAuthnController::RunFinishSign(
uint64_t aTransactionId, const RefPtr<nsICtapSignResult>& aResult) {
MOZ_ASSERT(XRE_IsParentProcess());
mozilla::ipc::AssertIsOnBackgroundThread();
if (mTransaction.isNothing() ||
aTransactionId != mTransaction.ref().mTransactionId) {
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
("WebAuthnController::RunFinishSign"));
if (mTransactionId.isNothing() || mPendingClientData.isNothing() ||
aTransactionId != mTransactionId.ref()) {
return;
}
nsresult status;
nsresult rv = aResult->GetStatus(&status);
if (NS_WARN_IF(NS_FAILED(rv))) {
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
return;
}
if (NS_FAILED(status)) {
bool shouldCancelActiveDialog = true;
if (status == NS_ERROR_DOM_INVALID_STATE_ERR) {
// PIN-related errors, e.g. blocked token. Let the dialog show to inform
// the user
shouldCancelActiveDialog = false;
}
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
u"CTAPSignAbort"_ns, 1);
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR,
shouldCancelActiveDialog);
AbortTransaction(status);
return;
}
nsTArray<uint8_t> credentialId;
rv = aResult->GetCredentialId(credentialId);
if (NS_WARN_IF(NS_FAILED(rv))) {
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
return;
}
nsTArray<uint8_t> signature;
rv = aResult->GetSignature(signature);
if (NS_WARN_IF(NS_FAILED(rv))) {
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
return;
}
nsTArray<uint8_t> authenticatorData;
rv = aResult->GetAuthenticatorData(authenticatorData);
if (NS_WARN_IF(NS_FAILED(rv))) {
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
return;
}
@ -537,86 +364,51 @@ void WebAuthnController::RunFinishSign(
rv = aResult->GetUsedAppId(&usedAppId);
if (rv != NS_ERROR_NOT_AVAILABLE) {
if (NS_FAILED(rv)) {
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
return;
}
extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId));
}
WebAuthnGetAssertionResult result(mTransaction.ref().mClientDataJSON,
credentialId, signature, authenticatorData,
extensions, userHandle);
WebAuthnGetAssertionResult result(mPendingClientData.extract(), credentialId,
signature, authenticatorData, extensions,
userHandle);
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
u"CTAPSignFinish"_ns, 1);
Unused << mTransactionParent->SendConfirmSign(aTransactionId, result);
ClearTransaction(true);
ClearTransaction();
}
NS_IMETHODIMP
WebAuthnController::SignatureSelectionCallback(uint64_t aTransactionId,
uint64_t idx) {
void WebAuthnController::Cancel(PWebAuthnTransactionParent* aTransactionParent,
const Tainted<uint64_t>& aTransactionId) {
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t, uint64_t>(
"WebAuthnController::RunResumeWithSelectedSignResult", this,
&WebAuthnController::RunResumeWithSelectedSignResult, aTransactionId,
idx));
if (!gWebAuthnBackgroundThread) {
return NS_ERROR_FAILURE;
}
if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) {
return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
}
return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
}
void WebAuthnController::RunResumeWithSelectedSignResult(
uint64_t aTransactionId, uint64_t aIndex) {
mozilla::ipc::AssertIsOnBackgroundThread();
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
("WebAuthnController::Cancel (IPC)"));
// The last transaction ID also suffers from the issue described in Bug
// 1696159. A content process could cancel another content processes
// transaction by guessing the last transaction ID.
if (mTransactionParent != aTransactionParent || mTransactionId.isNothing() ||
!MOZ_IS_VALID(aTransactionId, mTransactionId.ref() == aTransactionId)) {
return;
}
if (mTransportImpl) {
mTransportImpl->SelectionCallback(aTransactionId, aIndex);
mTransportImpl->Reset();
}
}
NS_IMETHODIMP
WebAuthnController::PinCallback(uint64_t aTransactionId,
const nsACString& aPin) {
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t, nsCString>(
"WebAuthnController::RunPinCallback", this,
&WebAuthnController::RunPinCallback, aTransactionId, aPin));
if (!gWebAuthnBackgroundThread) {
return NS_ERROR_FAILURE;
}
if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) {
return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
}
return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
}
void WebAuthnController::RunPinCallback(uint64_t aTransactionId,
const nsCString& aPin) {
mozilla::ipc::AssertIsOnBackgroundThread();
if (mTransportImpl) {
mTransportImpl->PinCallback(aTransactionId, aPin);
}
ClearTransaction();
}
NS_IMETHODIMP
WebAuthnController::Cancel(uint64_t aTransactionId) {
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_ASSERT(NS_IsMainThread());
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
("WebAuthnController::Cancel (XPCOM)"));
nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t>(
"WebAuthnController::RunCancel", this, &WebAuthnController::RunCancel,
"WebAuthnController::Cancel", this, &WebAuthnController::RunCancel,
aTransactionId));
if (!gWebAuthnBackgroundThread) {
@ -629,20 +421,16 @@ WebAuthnController::Cancel(uint64_t aTransactionId) {
}
void WebAuthnController::RunCancel(uint64_t aTransactionId) {
MOZ_ASSERT(XRE_IsParentProcess());
mozilla::ipc::AssertIsOnBackgroundThread();
MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
("WebAuthnController::RunCancel (XPCOM)"));
if (mTransaction.isNothing() ||
mTransaction.ref().mTransactionId != aTransactionId) {
if (mTransactionId.isNothing() || mTransactionId.ref() != aTransactionId) {
return;
}
// Cancel the request.
if (mTransportImpl) {
mTransportImpl->Cancel();
}
// Reject the promise.
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
AbortTransaction();
}
} // namespace mozilla::dom

View File

@ -25,89 +25,57 @@ class WebAuthnController final : public nsIWebAuthnController {
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIWEBAUTHNCONTROLLER
// Main thread only
static void Initialize();
// IPDL Background thread only
static WebAuthnController* Get();
// IPDL Background thread only
void Register(PWebAuthnTransactionParent* aTransactionParent,
const uint64_t& aTransactionId,
const WebAuthnMakeCredentialInfo& aInfo);
// IPDL Background thread only
void Sign(PWebAuthnTransactionParent* aTransactionParent,
const uint64_t& aTransactionId,
const WebAuthnGetAssertionInfo& aInfo);
// IPDL Background thread only
void Cancel(PWebAuthnTransactionParent* aTransactionParent,
const Tainted<uint64_t>& aTransactionId);
// IPDL Background thread only
void MaybeClearTransaction(PWebAuthnTransactionParent* aParent);
uint64_t GetCurrentTransactionId() {
return mTransaction.isNothing() ? 0 : mTransaction.ref().mTransactionId;
}
bool CurrentTransactionIsRegister() { return mPendingRegisterInfo.isSome(); }
bool CurrentTransactionIsSign() { return mPendingSignInfo.isSome(); }
// Sends a "webauthn-prompt" observer notification with the given data.
template <typename... T>
void SendPromptNotification(const char16_t* aFormat, T... aArgs);
// Same as SendPromptNotification, but with the already formatted string
// void SendPromptNotificationPreformatted(const nsACString& aJSON);
// The main thread runnable function for "SendPromptNotification".
void RunSendPromptNotification(const nsString& aJSON);
private:
WebAuthnController();
~WebAuthnController() = default;
// All of the private functions and members are to be
// accessed on the IPDL background thread only.
nsCOMPtr<nsIWebAuthnTransport> GetTransportImpl();
nsCOMPtr<nsIWebAuthnTransport> mTransportImpl;
void AbortTransaction(const uint64_t& aTransactionId, const nsresult& aError,
bool shouldCancelActiveDialog);
void AbortOngoingTransaction();
void ClearTransaction(bool cancel_prompt);
void DoRegister(const WebAuthnMakeCredentialInfo& aInfo,
bool aForceNoneAttestation);
void DoSign(const WebAuthnGetAssertionInfo& aTransactionInfo);
void AbortTransaction(const nsresult& aError);
void ClearTransaction();
void RunCancel(uint64_t aTransactionId);
void RunFinishRegister(uint64_t aTransactionId,
const RefPtr<nsICtapRegisterResult>& aResult);
void RunFinishSign(uint64_t aTransactionId,
const RefPtr<nsICtapSignResult>& aResult);
// The main thread runnable function for "nsIU2FTokenManager.ResumeRegister".
void RunResumeRegister(uint64_t aTransactionId, bool aForceNoneAttestation);
void RunResumeSign(uint64_t aTransactionId);
void RunResumeWithSelectedSignResult(uint64_t aTransactionId, uint64_t idx);
void RunPinCallback(uint64_t aTransactionId, const nsCString& aPin);
// The main thread runnable function for "nsIU2FTokenManager.Cancel".
void RunCancel(uint64_t aTransactionId);
// 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.
PWebAuthnTransactionParent* mTransactionParent;
nsCOMPtr<nsIWebAuthnTransport> mTransportImpl;
// The current transaction ID.
Maybe<uint64_t> mTransactionId;
// Pending registration info while we wait for user input.
Maybe<WebAuthnMakeCredentialInfo> mPendingRegisterInfo;
// Pending registration info while we wait for user input.
Maybe<WebAuthnGetAssertionInfo> mPendingSignInfo;
class Transaction {
public:
Transaction(uint64_t aTransactionId, const nsCString& aClientDataJSON)
: mTransactionId(aTransactionId), mClientDataJSON(aClientDataJSON) {}
uint64_t mTransactionId;
nsCString mClientDataJSON;
bool mCredProps;
};
Maybe<Transaction> mTransaction;
// Client data associated with mTransactionId.
Maybe<nsCString> mPendingClientData;
};
} // namespace mozilla::dom

View File

@ -7,11 +7,13 @@ authors = ["Martin Sirringhaus", "John Schanck"]
[dependencies]
authenticator = { version = "0.4.0-alpha.22", features = ["gecko"] }
base64 = "^0.21"
cstr = "0.2"
log = "0.4"
moz_task = { path = "../../../xpcom/rust/moz_task" }
nserror = { path = "../../../xpcom/rust/nserror" }
nsstring = { path = "../../../xpcom/rust/nsstring" }
rand = "0.8"
serde = { version = "1", features = ["derive"] }
serde_cbor = "0.11"
serde_json = "1.0"
static_prefs = { path = "../../../modules/libpref/init/static_prefs" }

View File

@ -17,104 +17,130 @@ use authenticator::{
PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty,
ResidentKeyRequirement, UserVerificationRequirement,
},
errors::{AuthenticatorError, PinError, U2FTokenError},
errors::AuthenticatorError,
statecallback::StateCallback,
Pin, RegisterResult, SignResult, StateMachine, StatusPinUv, StatusUpdate,
};
use base64::Engine;
use moz_task::RunnableBuilder;
use cstr::cstr;
use moz_task::{get_main_thread, RunnableBuilder};
use nserror::{
nsresult, NS_ERROR_DOM_INVALID_STATE_ERR, NS_ERROR_DOM_NOT_ALLOWED_ERR,
NS_ERROR_DOM_NOT_SUPPORTED_ERR, NS_ERROR_DOM_UNKNOWN_ERR, NS_ERROR_FAILURE,
nsresult, NS_ERROR_DOM_INVALID_STATE_ERR, NS_ERROR_DOM_NOT_ALLOWED_ERR, NS_ERROR_FAILURE,
NS_ERROR_INVALID_ARG, NS_ERROR_NOT_AVAILABLE, NS_ERROR_NOT_IMPLEMENTED, NS_ERROR_NULL_POINTER,
NS_OK,
};
use nsstring::{nsACString, nsCString, nsString};
use serde::Serialize;
use serde_cbor;
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};
use thin_vec::{thin_vec, ThinVec};
use xpcom::interfaces::{
nsICredentialParameters, nsICtapRegisterArgs, nsICtapRegisterResult, nsICtapSignArgs,
nsICtapSignResult, nsIWebAuthnAttObj, nsIWebAuthnController, nsIWebAuthnTransport,
nsICtapSignResult, nsIObserverService, nsIWebAuthnAttObj, nsIWebAuthnController,
nsIWebAuthnTransport,
};
use xpcom::{xpcom_method, RefPtr};
mod test_token;
use test_token::TestTokenManager;
fn make_prompt(action: &str, tid: u64, origin: &str, browsing_context_id: u64) -> String {
format!(
r#"{{"action":"{action}","tid":{tid},"origin":"{origin}","browsingContextId":{browsing_context_id}}}"#,
)
}
fn make_uv_invalid_error_prompt(
tid: u64,
origin: &str,
browsing_context_id: u64,
retries: i64,
) -> String {
format!(
r#"{{"action":"uv-invalid","tid":{tid},"origin":"{origin}","browsingContextId":{browsing_context_id},"retriesLeft":{retries}}}"#,
)
}
fn make_pin_required_prompt(
tid: u64,
origin: &str,
browsing_context_id: u64,
was_invalid: bool,
retries: i64,
) -> String {
format!(
r#"{{"action":"pin-required","tid":{tid},"origin":"{origin}","browsingContextId":{browsing_context_id},"wasInvalid":{was_invalid},"retriesLeft":{retries}}}"#,
)
}
fn make_user_selection_prompt(
tid: u64,
origin: &str,
browsing_context_id: u64,
user_entities: &[PublicKeyCredentialUserEntity],
) -> String {
// Bug 1854280: "Unknown username" should be a localized string here.
let usernames: Vec<String> = user_entities
.iter()
.map(|entity| {
entity
.name
.clone()
.unwrap_or("<Unknown username>".to_string())
})
.collect();
let usernames_json = json!(usernames);
let out = format!(
r#"{{"action":"select-sign-result","tid":{tid},"origin":"{origin}","browsingContextId":{browsing_context_id},"usernames":{usernames_json}}}"#,
);
out
}
fn authrs_to_nserror(e: &AuthenticatorError) -> nsresult {
match e {
AuthenticatorError::U2FToken(U2FTokenError::NotSupported) => NS_ERROR_DOM_NOT_SUPPORTED_ERR,
AuthenticatorError::U2FToken(U2FTokenError::InvalidState) => NS_ERROR_DOM_INVALID_STATE_ERR,
AuthenticatorError::U2FToken(U2FTokenError::NotAllowed) => NS_ERROR_DOM_NOT_ALLOWED_ERR,
AuthenticatorError::PinError(PinError::PinRequired) => NS_ERROR_DOM_INVALID_STATE_ERR,
AuthenticatorError::PinError(PinError::InvalidPin(_)) => NS_ERROR_DOM_INVALID_STATE_ERR,
AuthenticatorError::PinError(PinError::PinAuthBlocked) => NS_ERROR_DOM_INVALID_STATE_ERR,
AuthenticatorError::PinError(PinError::PinBlocked) => NS_ERROR_DOM_INVALID_STATE_ERR,
AuthenticatorError::PinError(PinError::PinNotSet) => NS_ERROR_DOM_INVALID_STATE_ERR,
AuthenticatorError::CredentialExcluded => NS_ERROR_DOM_INVALID_STATE_ERR,
_ => NS_ERROR_DOM_UNKNOWN_ERR,
_ => NS_ERROR_DOM_NOT_ALLOWED_ERR,
}
}
fn error_cancels_prompts(e: &AuthenticatorError) -> bool {
match e {
AuthenticatorError::CredentialExcluded | AuthenticatorError::PinError(_) => false,
_ => true,
}
}
// Using serde(tag="type") makes it so that, for example, BrowserPromptType::Cancel is serialized
// as '{ type: "cancel" }', and BrowserPromptType::PinInvalid { retries: 5 } is serialized as
// '{type: "pin-invalid", retries: 5}'.
#[derive(Serialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
enum BrowserPromptType<'a> {
AlreadyRegistered,
Cancel,
DeviceBlocked,
PinAuthBlocked,
PinNotSet,
Presence,
SelectDevice,
UvBlocked,
PinRequired,
PinInvalid {
retries: Option<u8>,
},
RegisterDirect,
UvInvalid {
retries: Option<u8>,
},
SelectSignResult {
entities: &'a [PublicKeyCredentialUserEntity],
},
}
#[derive(Serialize)]
struct BrowserPromptMessage<'a> {
prompt: BrowserPromptType<'a>,
tid: u64,
origin: Option<&'a str>,
#[serde(rename = "browsingContextId")]
browsing_context_id: Option<u64>,
}
fn send_prompt(
prompt: BrowserPromptType,
tid: u64,
origin: Option<&str>,
browsing_context_id: Option<u64>,
) -> Result<(), nsresult> {
let main_thread = get_main_thread()?;
let mut json = nsString::new();
write!(
json,
"{}",
json!(&BrowserPromptMessage {
prompt,
tid,
origin,
browsing_context_id
})
)
.or(Err(NS_ERROR_FAILURE))?;
RunnableBuilder::new("AuthrsTransport::send_prompt", move || {
if let Ok(obs_svc) = xpcom::components::Observer::service::<nsIObserverService>() {
unsafe {
obs_svc.NotifyObservers(
std::ptr::null(),
cstr!("webauthn-prompt").as_ptr(),
json.as_ptr(),
);
}
}
})
.dispatch(main_thread.coerce())
}
fn cancel_prompts(tid: u64) -> Result<(), nsresult> {
send_prompt(BrowserPromptType::Cancel, tid, None, None)?;
Ok(())
}
type RegisterResultOrError = Result<RegisterResult, AuthenticatorError>;
#[xpcom(implement(nsICtapRegisterResult), atomic)]
pub struct CtapRegisterResult {
result: Result<RegisterResult, AuthenticatorError>,
result: RegisterResultOrError,
}
impl CtapRegisterResult {
@ -212,9 +238,11 @@ impl WebAuthnAttObj {
}
}
type SignResultOrError = Result<SignResult, AuthenticatorError>;
#[xpcom(implement(nsICtapSignResult), atomic)]
pub struct CtapSignResult {
result: Result<SignResult, AuthenticatorError>,
result: SignResultOrError,
}
impl CtapSignResult {
@ -288,22 +316,7 @@ impl Controller {
Ok(())
}
fn send_prompt(&self, tid: u64, msg: &str) {
if (*self.0.borrow()).is_null() {
warn!("Controller not initialized");
return;
}
let notification_str = nsCString::from(msg);
unsafe {
(**(self.0.borrow())).SendPromptNotificationPreformatted(tid, &*notification_str);
}
}
fn finish_register(
&self,
tid: u64,
result: Result<RegisterResult, AuthenticatorError>,
) -> Result<(), nsresult> {
fn finish_register(&self, tid: u64, result: RegisterResultOrError) -> Result<(), nsresult> {
if (*self.0.borrow()).is_null() {
return Err(NS_ERROR_FAILURE);
}
@ -316,11 +329,7 @@ impl Controller {
Ok(())
}
fn finish_sign(
&self,
tid: u64,
result: Result<SignResult, AuthenticatorError>,
) -> Result<(), nsresult> {
fn finish_sign(&self, tid: u64, result: SignResultOrError) -> Result<(), nsresult> {
if (*self.0.borrow()).is_null() {
return Err(NS_ERROR_FAILURE);
}
@ -332,6 +341,16 @@ impl Controller {
}
Ok(())
}
fn cancel(&self, tid: u64) -> Result<(), nsresult> {
if (*self.0.borrow()).is_null() {
return Err(NS_ERROR_FAILURE);
}
unsafe {
(**(self.0.borrow())).Cancel(tid);
}
Ok(())
}
}
// A transaction may create a channel to ask a user for additional input, e.g. a PIN. The Sender
@ -346,66 +365,88 @@ fn status_callback(
tid: u64,
origin: &String,
browsing_context_id: u64,
controller: Controller,
pin_receiver: Arc<Mutex<PinReceiver>>, /* Shared with an AuthrsTransport */
selection_receiver: Arc<Mutex<SelectionReceiver>>, /* Shared with an AuthrsTransport */
) {
) -> Result<(), nsresult> {
let origin = Some(origin.as_str());
let browsing_context_id = Some(browsing_context_id);
loop {
match status_rx.recv() {
Ok(StatusUpdate::SelectDeviceNotice) => {
debug!("STATUS: Please select a device by touching one of them.");
let notification_str =
make_prompt("select-device", tid, origin, browsing_context_id);
controller.send_prompt(tid, &notification_str);
send_prompt(
BrowserPromptType::SelectDevice,
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PresenceRequired) => {
debug!("STATUS: Waiting for user presence");
let notification_str = make_prompt("presence", tid, origin, browsing_context_id);
controller.send_prompt(tid, &notification_str);
send_prompt(
BrowserPromptType::Presence,
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
pin_receiver.lock().unwrap().replace((tid, sender));
let notification_str =
make_pin_required_prompt(tid, origin, browsing_context_id, false, -1);
controller.send_prompt(tid, &notification_str);
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
pin_receiver.lock().unwrap().replace((tid, sender));
let notification_str = make_pin_required_prompt(
send_prompt(
BrowserPromptType::PinRequired,
tid,
origin,
browsing_context_id,
true,
attempts.map_or(-1, |x| x as i64),
);
controller.send_prompt(tid, &notification_str);
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, retries))) => {
pin_receiver.lock().unwrap().replace((tid, sender));
send_prompt(
BrowserPromptType::PinInvalid { retries },
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
let notification_str =
make_prompt("pin-auth-blocked", tid, origin, browsing_context_id);
controller.send_prompt(tid, &notification_str);
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
let notification_str =
make_prompt("device-blocked", tid, origin, browsing_context_id);
controller.send_prompt(tid, &notification_str);
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinNotSet)) => {
let notification_str = make_prompt("pin-not-set", tid, origin, browsing_context_id);
controller.send_prompt(tid, &notification_str);
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => {
let notification_str = make_uv_invalid_error_prompt(
send_prompt(
BrowserPromptType::PinAuthBlocked,
tid,
origin,
browsing_context_id,
attempts.map_or(-1, |x| x as i64),
);
controller.send_prompt(tid, &notification_str);
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
send_prompt(
BrowserPromptType::DeviceBlocked,
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinNotSet)) => {
send_prompt(
BrowserPromptType::PinNotSet,
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(retries))) => {
send_prompt(
BrowserPromptType::UvInvalid { retries },
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => {
let notification_str = make_prompt("uv-blocked", tid, origin, browsing_context_id);
controller.send_prompt(tid, &notification_str);
send_prompt(
BrowserPromptType::UvBlocked,
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinIsTooShort))
| Ok(StatusUpdate::PinUvError(StatusPinUv::PinIsTooLong(..))) => {
@ -415,19 +456,37 @@ fn status_callback(
Ok(StatusUpdate::InteractiveManagement(_)) => {
debug!("STATUS: interactive management");
}
Ok(StatusUpdate::SelectResultNotice(sender, choices)) => {
Ok(StatusUpdate::SelectResultNotice(sender, entities)) => {
debug!("STATUS: select result notice");
selection_receiver.lock().unwrap().replace((tid, sender));
let notification_str =
make_user_selection_prompt(tid, origin, browsing_context_id, &choices);
controller.send_prompt(tid, &notification_str);
send_prompt(
BrowserPromptType::SelectSignResult {
entities: &entities,
},
tid,
origin,
browsing_context_id,
)?;
}
Err(RecvError) => {
debug!("STATUS: end");
return;
break;
}
}
}
Ok(())
}
enum TransactionArgs {
Register(/* timeout */ u64, RegisterArgs),
// Bug 1838932 - we'll need to cache SignArgs once we support conditional mediation
// Sign(/* timeout */ u64, SignArgs),
}
struct TransactionState {
tid: u64,
browsing_context_id: u64,
pending_args: Option<TransactionArgs>,
}
// AuthrsTransport provides an nsIWebAuthnTransport interface to an AuthenticatorService. This
@ -442,6 +501,7 @@ pub struct AuthrsTransport {
controller: Controller,
pin_receiver: Arc<Mutex<PinReceiver>>,
selection_receiver: Arc<Mutex<SelectionReceiver>>,
transaction: Arc<Mutex<Option<TransactionState>>>,
}
impl AuthrsTransport {
@ -498,6 +558,8 @@ impl AuthrsTransport {
browsing_context_id: u64,
args: *const nsICtapRegisterArgs,
) -> Result<(), nsresult> {
self.reset()?;
if args.is_null() {
return Err(NS_ERROR_NULL_POINTER);
}
@ -581,7 +643,9 @@ impl AuthrsTransport {
let mut attestation_conveyance_preference = nsString::new();
unsafe { args.GetAttestationConveyancePreference(&mut *attestation_conveyance_preference) }
.to_result()?;
let none_attestation = attestation_conveyance_preference.eq("none");
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()?;
@ -597,13 +661,14 @@ impl AuthrsTransport {
// _ => (),
// }
let origin = origin.to_string();
let info = RegisterArgs {
client_data_hash: client_data_hash_arr,
relying_party: RelyingParty {
id: relying_party_id.to_string(),
name: None,
},
origin: origin.to_string(),
origin: origin.clone(),
user: PublicKeyCredentialUserEntity {
id: user_id.to_vec(),
name: Some(user_name.to_string()),
@ -622,56 +687,103 @@ impl AuthrsTransport {
use_ctap1_fallback: !static_prefs::pref!("security.webauthn.ctap2"),
};
*self.transaction.lock().unwrap() = Some(TransactionState {
tid,
browsing_context_id,
pending_args: Some(TransactionArgs::Register(timeout_ms as u64, info)),
});
if none_attestation
|| static_prefs::pref!("security.webauth.webauthn_testing_allow_direct_attestation")
{
// TODO(Bug 1855290) Remove this presence prompt
send_prompt(
BrowserPromptType::Presence,
tid,
Some(&origin),
Some(browsing_context_id),
)?;
self.resume_make_credential(tid, none_attestation)
} else {
send_prompt(
BrowserPromptType::RegisterDirect,
tid,
Some(&origin),
Some(browsing_context_id),
)?;
Ok(())
}
}
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 (timeout_ms, info) = match state.pending_args.take() {
Some(TransactionArgs::Register(timeout_ms, info)) => (timeout_ms, info),
_ => return Err(NS_ERROR_FAILURE),
};
let (status_tx, status_rx) = channel::<StatusUpdate>();
let pin_receiver = self.pin_receiver.clone();
let selection_receiver = self.selection_receiver.clone();
let controller = self.controller.clone();
let status_origin = origin.to_string();
let status_origin = info.origin.clone();
RunnableBuilder::new(
"AuthrsTransport::MakeCredential::StatusReceiver",
move || {
status_callback(
let _ = status_callback(
status_rx,
tid,
&status_origin,
browsing_context_id,
controller,
pin_receiver,
selection_receiver,
)
);
},
)
.may_block(true)
.dispatch_background_task()?;
let controller = self.controller.clone();
let callback_origin = origin.to_string();
let state_callback = StateCallback::<Result<RegisterResult, AuthenticatorError>>::new(
Box::new(move |result| {
let result = match result {
Ok(mut make_cred_res) => {
// Tokens always provide attestation, but the user may have asked we not
// include the attestation statement in the response.
if none_attestation {
make_cred_res.att_obj.anonymize();
}
Ok(make_cred_res)
let callback_origin = info.origin.clone();
let state_callback = StateCallback::<RegisterResultOrError>::new(Box::new(move |result| {
let result = match result {
Ok(mut make_cred_res) => {
// Tokens always provide attestation, but the user may have asked we not
// include the attestation statement in the response.
if force_none_attestation {
make_cred_res.att_obj.anonymize();
}
Err(e @ AuthenticatorError::CredentialExcluded) => {
let notification_str = make_prompt(
"already-registered",
tid,
&callback_origin,
browsing_context_id,
);
controller.send_prompt(tid, &notification_str);
Err(e)
}
Err(e) => Err(e),
};
let _ = controller.finish_register(tid, result);
}),
);
Ok(make_cred_res)
}
Err(e @ AuthenticatorError::CredentialExcluded) => {
let _ = send_prompt(
BrowserPromptType::AlreadyRegistered,
tid,
Some(&callback_origin),
Some(browsing_context_id),
);
Err(e)
}
Err(e) => Err(e),
};
// Some errors are accompanied by prompts that should persist after the
// operation terminates.
if result.is_ok() || error_cancels_prompts(&result.as_ref().unwrap_err()) {
let _ = cancel_prompts(tid);
}
let _ = controller.finish_register(tid, result);
}));
// The authenticator crate provides an `AuthenticatorService` which can dispatch a request
// in parallel to any number of transports. We only support the USB transport in production
@ -679,18 +791,14 @@ impl AuthrsTransport {
// We disable the USB transport in tests that use virtual devices.
if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") {
self.usb_token_manager.borrow_mut().register(
timeout_ms as u64,
timeout_ms,
info.into(),
status_tx,
state_callback,
);
} else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") {
self.test_token_manager.register(
timeout_ms as u64,
info.into(),
status_tx,
state_callback,
);
self.test_token_manager
.register(timeout_ms, info.into(), status_tx, state_callback);
} else {
return Err(NS_ERROR_FAILURE);
}
@ -709,6 +817,8 @@ impl AuthrsTransport {
browsing_context_id: u64,
args: *const nsICtapSignArgs,
) -> Result<(), nsresult> {
self.reset()?;
if args.is_null() {
return Err(NS_ERROR_NULL_POINTER);
}
@ -758,18 +868,16 @@ impl AuthrsTransport {
let (status_tx, status_rx) = channel::<StatusUpdate>();
let pin_receiver = self.pin_receiver.clone();
let selection_receiver = self.selection_receiver.clone();
let controller = self.controller.clone();
let status_origin = origin.to_string();
RunnableBuilder::new("AuthrsTransport::GetAssertion::StatusReceiver", move || {
status_callback(
let _ = status_callback(
status_rx,
tid,
&status_origin,
browsing_context_id,
controller,
pin_receiver,
selection_receiver,
)
);
})
.may_block(true)
.dispatch_background_task()?;
@ -781,8 +889,8 @@ impl AuthrsTransport {
};
let controller = self.controller.clone();
let state_callback = StateCallback::<Result<SignResult, AuthenticatorError>>::new(
Box::new(move |mut result| {
let state_callback =
StateCallback::<SignResultOrError>::new(Box::new(move |mut result| {
if uniq_allowed_cred.is_some() {
// In CTAP 2.0, but not CTAP 2.1, the assertion object's credential field
// "May be omitted if the allowList has exactly one credential." If we had
@ -791,9 +899,13 @@ impl AuthrsTransport {
inner.assertion.credentials = uniq_allowed_cred;
}
}
// Some errors are accompanied by prompts that should persist after the
// operation terminates.
if result.is_ok() || error_cancels_prompts(&result.as_ref().unwrap_err()) {
let _ = cancel_prompts(tid);
}
let _ = controller.finish_sign(tid, result);
}),
);
}));
let info = SignArgs {
client_data_hash: client_data_hash_arr,
@ -810,6 +922,20 @@ impl AuthrsTransport {
use_ctap1_fallback: !static_prefs::pref!("security.webauthn.ctap2"),
};
// TODO(Bug 1855290) Remove this presence prompt
send_prompt(
BrowserPromptType::Presence,
tid,
Some(&info.origin),
Some(browsing_context_id),
)?;
*self.transaction.lock().unwrap() = Some(TransactionState {
tid,
browsing_context_id,
pending_args: None,
});
// As in `register`, we are intentionally avoiding `AuthenticatorService` here.
if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") {
self.usb_token_manager.borrow_mut().sign(
@ -828,14 +954,27 @@ impl AuthrsTransport {
Ok(())
}
// # Safety
//
// This will mutably borrow usb_token_manager through a RefCell. The caller must ensure that at
// most one WebAuthn transaction is active at any given time.
xpcom_method!(cancel => Cancel());
fn cancel(&self) -> Result<(), nsresult> {
// The transaction thread may be waiting for user input. Dropping the associated channel
// will cause the transaction to error out with a "CancelledByUser" result.
xpcom_method!(cancel => Cancel(aTransactionId: u64));
fn cancel(&self, tid: u64) -> Result<(), nsresult> {
let mut guard = self.transaction.lock().unwrap();
if guard.as_ref().is_some_and(|state| state.tid == tid) {
self.reset_helper()?;
self.controller.cancel(tid)?;
*guard = None;
}
Ok(())
}
xpcom_method!(reset => Reset());
fn reset(&self) -> Result<(), nsresult> {
if let Some(transaction) = self.transaction.lock().unwrap().take() {
self.reset_helper()?;
cancel_prompts(transaction.tid)?;
}
Ok(())
}
fn reset_helper(&self) -> Result<(), nsresult> {
drop(self.pin_receiver.lock().or(Err(NS_ERROR_FAILURE))?.take());
drop(
self.selection_receiver
@ -843,9 +982,7 @@ impl AuthrsTransport {
.or(Err(NS_ERROR_FAILURE))?
.take(),
);
self.usb_token_manager.borrow_mut().cancel();
Ok(())
}
@ -980,6 +1117,7 @@ pub extern "C" fn authrs_transport_constructor(
controller: Controller(RefCell::new(std::ptr::null())),
pin_receiver: Arc::new(Mutex::new(None)),
selection_receiver: Arc::new(Mutex::new(None)),
transaction: Arc::new(Mutex::new(None)),
});
#[cfg(feature = "fuzzing")]

View File

@ -170,26 +170,11 @@ interface nsICtapSignResult : nsISupports {
[must_use] readonly attribute bool usedAppId;
};
// The nsIWebAuthnController interface coordinates interactions between the user
// and the authenticator to drive a WebAuthn transaction forward.
// It allows an nsIWebAuthnTransport to
// 1) prompt the user for input,
// 2) receive a callback from a prompt, and
// 3) return results to the content process.
//
// Callbacks for sending results from an nsIWebAuthnTransport to the IPC parent.
[scriptable, uuid(c0744f48-ad64-11ed-b515-cf5149f4d6a6)]
interface nsIWebAuthnController : nsISupports
{
// Prompt callbacks
void pinCallback(in uint64_t aTransactionId, in ACString aPin);
void signatureSelectionCallback(in uint64_t aTransactionId, in uint64_t aIndex);
void resumeRegister(in uint64_t aTransactionID, in bool aForceNoneAttestation);
// Cancel the transaction with the given ID.
void cancel(in uint64_t aTransactionID);
// Authenticator callbacks
[noscript] void sendPromptNotificationPreformatted(in uint64_t aTransactionId, in ACString aJSON);
[noscript] void cancel(in uint64_t aTransactionID);
[noscript] void finishRegister(in uint64_t aTransactionId, in nsICtapRegisterResult aResult);
[noscript] void finishSign(in uint64_t aTransactionId, in nsICtapSignResult aResult);
};
@ -215,6 +200,14 @@ interface nsIWebAuthnTransport : nsISupports
void makeCredential(in uint64_t aTransactionId, in uint64_t browsingContextId, in nsICtapRegisterArgs args);
void getAssertion(in uint64_t aTransactionId, in uint64_t browsingContextId, in nsICtapSignArgs args);
[noscript] void reset();
// Prompt callbacks
void cancel(in uint64_t aTransactionId);
void pinCallback(in uint64_t aTransactionId, in ACString aPin);
void resumeMakeCredential(in uint64_t aTransactionId, in bool aForceNoneAttestation);
void selectionCallback(in uint64_t aTransactionId, in uint64_t aIndex);
// Adds a virtual (software) authenticator for use in tests (particularly
// tests run via WebDriver). See
// https://w3c.github.io/webauthn/#sctn-automation-add-virtual-authenticator.
@ -257,10 +250,4 @@ interface nsIWebAuthnTransport : nsISupports
// Sets the "isUserVerified" bit on a virtual authenticator. See
// https://w3c.github.io/webauthn/#sctn-automation-set-user-verified
void setUserVerified(in uint64_t authenticatorId, in bool isUserVerified);
// These are prompt callbacks but they're not intended to be called directly from
// JavaScript---they are proxied through the nsIWebAuthnController first.
[noscript] void selectionCallback(in uint64_t aTransactionId, in uint64_t aIndex);
[noscript] void pinCallback(in uint64_t aTransactionId, in ACString aPin);
[noscript] void cancel();
};

View File

@ -13683,6 +13683,7 @@
type: RelaxedAtomicBool
value: false
mirror: always
rust: true
# Block Worker/SharedWorker scripts with wrong MIME type.
- name: security.block_Worker_with_wrong_mime