mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 13:21:05 +00:00
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:
parent
58364b15c1
commit
58260a36c4
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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",
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -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 =
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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" }
|
||||
|
@ -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, ¬ification_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, ¬ification_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, ¬ification_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, ¬ification_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, ¬ification_str);
|
||||
}
|
||||
Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
|
||||
let notification_str =
|
||||
make_prompt("device-blocked", tid, origin, browsing_context_id);
|
||||
controller.send_prompt(tid, ¬ification_str);
|
||||
}
|
||||
Ok(StatusUpdate::PinUvError(StatusPinUv::PinNotSet)) => {
|
||||
let notification_str = make_prompt("pin-not-set", tid, origin, browsing_context_id);
|
||||
controller.send_prompt(tid, ¬ification_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, ¬ification_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, ¬ification_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, ¬ification_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, ¬ification_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")]
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user