gecko-dev/dom/u2f/U2F.cpp

390 строки
12 KiB
C++
Исходник Обычный вид История

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/dom/U2F.h"
#include "mozilla/dom/WebCryptoCommon.h"
#include "nsContentUtils.h"
#include "nsNetCID.h"
#include "nsNetUtil.h"
#include "nsURLParsers.h"
#include "U2FManager.h"
namespace mozilla {
namespace dom {
static mozilla::LazyLogModule gU2FLog("u2fmanager");
NS_NAMED_LITERAL_STRING(kFinishEnrollment, "navigator.id.finishEnrollment");
NS_NAMED_LITERAL_STRING(kGetAssertion, "navigator.id.getAssertion");
NS_NAMED_LITERAL_STRING(kRequiredU2FVersion, "U2F_V2");
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(U2F)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(U2F)
NS_IMPL_CYCLE_COLLECTING_RELEASE(U2F)
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(U2F, mParent)
static uint32_t
AdjustedTimeoutMillis(const Optional<Nullable<int32_t>>& opt_aSeconds)
{
uint32_t adjustedTimeoutMillis = 30000u;
if (opt_aSeconds.WasPassed() && !opt_aSeconds.Value().IsNull()) {
adjustedTimeoutMillis = opt_aSeconds.Value().Value() * 1000u;
adjustedTimeoutMillis = std::max(15000u, adjustedTimeoutMillis);
adjustedTimeoutMillis = std::min(120000u, adjustedTimeoutMillis);
}
return adjustedTimeoutMillis;
}
static nsresult
AssembleClientData(const nsAString& aOrigin, const nsAString& aTyp,
const nsAString& aChallenge,
/* out */ nsString& aClientData)
{
MOZ_ASSERT(NS_IsMainThread());
U2FClientData clientDataObject;
clientDataObject.mTyp.Construct(aTyp); // "Typ" from the U2F specification
clientDataObject.mChallenge.Construct(aChallenge);
clientDataObject.mOrigin.Construct(aOrigin);
if (NS_WARN_IF(!clientDataObject.ToJSON(aClientData))) {
return NS_ERROR_FAILURE;
}
return NS_OK;
}
static void
RegisteredKeysToScopedCredentialList(const nsAString& aAppId,
const nsTArray<RegisteredKey>& aKeys,
nsTArray<WebAuthnScopedCredentialDescriptor>& aList)
{
for (const RegisteredKey& key : aKeys) {
// Check for required attributes
if (!key.mVersion.WasPassed() || !key.mKeyHandle.WasPassed() ||
key.mVersion.Value() != kRequiredU2FVersion) {
continue;
}
// If this key's mAppId doesn't match the invocation, we can't handle it.
if (key.mAppId.WasPassed() && !key.mAppId.Value().Equals(aAppId)) {
continue;
}
CryptoBuffer keyHandle;
nsresult rv = keyHandle.FromJwkBase64(key.mKeyHandle.Value());
if (NS_WARN_IF(NS_FAILED(rv))) {
continue;
}
WebAuthnScopedCredentialDescriptor c;
c.id() = keyHandle;
aList.AppendElement(c);
}
}
static ErrorCode
EvaluateAppID(const nsString& aOrigin, /* in/out */ nsString& aAppId)
{
// Facet is the specification's way of referring to the web origin.
nsAutoCString facetString = NS_ConvertUTF16toUTF8(aOrigin);
nsCOMPtr<nsIURI> facetUri;
if (NS_FAILED(NS_NewURI(getter_AddRefs(facetUri), facetString))) {
return ErrorCode::BAD_REQUEST;
}
// If the facetId (origin) is not HTTPS, reject
bool facetIsHttps = false;
if (NS_FAILED(facetUri->SchemeIs("https", &facetIsHttps)) || !facetIsHttps) {
return ErrorCode::BAD_REQUEST;
}
// If the appId is empty or null, overwrite it with the facetId and accept
if (aAppId.IsEmpty() || aAppId.EqualsLiteral("null")) {
aAppId.Assign(aOrigin);
return ErrorCode::OK;
}
// AppID is user-supplied. It's quite possible for this parse to fail.
nsAutoCString appIdString = NS_ConvertUTF16toUTF8(aAppId);
nsCOMPtr<nsIURI> appIdUri;
if (NS_FAILED(NS_NewURI(getter_AddRefs(appIdUri), appIdString))) {
return ErrorCode::BAD_REQUEST;
}
// if the appId URL is not HTTPS, reject.
bool appIdIsHttps = false;
if (NS_FAILED(appIdUri->SchemeIs("https", &appIdIsHttps)) || !appIdIsHttps) {
return ErrorCode::BAD_REQUEST;
}
// If the facetId and the appId hosts match, accept
nsAutoCString facetHost;
if (NS_FAILED(facetUri->GetHost(facetHost))) {
return ErrorCode::BAD_REQUEST;
}
nsAutoCString appIdHost;
if (NS_FAILED(appIdUri->GetHost(appIdHost))) {
return ErrorCode::BAD_REQUEST;
}
if (facetHost.Equals(appIdHost)) {
return ErrorCode::OK;
}
// TODO(Bug 1244959) Implement the remaining algorithm.
return ErrorCode::BAD_REQUEST;
}
template<typename T, typename C>
static void
ExecuteCallback(T& aResp, Maybe<nsMainThreadPtrHandle<C>>& aCb)
{
MOZ_ASSERT(NS_IsMainThread());
if (aCb.isNothing()) {
return;
}
ErrorResult error;
aCb.ref()->Call(aResp, error);
NS_WARNING_ASSERTION(!error.Failed(), "dom::U2F::Promise callback failed");
error.SuppressException(); // Useful exceptions already emitted
aCb.reset();
MOZ_ASSERT(aCb.isNothing());
}
U2F::U2F(nsPIDOMWindowInner* aParent)
: mParent(aParent)
{
}
U2F::~U2F()
{
mPromiseHolder.DisconnectIfExists();
mRegisterCallback.reset();
mSignCallback.reset();
}
void
U2F::Init(ErrorResult& aRv)
{
MOZ_ASSERT(mParent);
MOZ_ASSERT(!mEventTarget);
nsCOMPtr<nsIDocument> doc = mParent->GetDoc();
MOZ_ASSERT(doc);
if (!doc) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
nsIPrincipal* principal = doc->NodePrincipal();
aRv = nsContentUtils::GetUTFOrigin(principal, mOrigin);
if (NS_WARN_IF(aRv.Failed())) {
return;
}
if (NS_WARN_IF(mOrigin.IsEmpty())) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
mEventTarget = doc->EventTargetFor(TaskCategory::Other);
MOZ_ASSERT(mEventTarget);
}
/* virtual */ JSObject*
U2F::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
return U2FBinding::Wrap(aCx, this, aGivenProto);
}
void
U2F::Register(const nsAString& aAppId,
const Sequence<RegisterRequest>& aRegisterRequests,
const Sequence<RegisteredKey>& aRegisteredKeys,
U2FRegisterCallback& aCallback,
const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds,
ErrorResult& aRv)
{
MOZ_ASSERT(NS_IsMainThread());
RefPtr<U2FManager> mgr = U2FManager::GetOrCreate();
MOZ_ASSERT(mgr);
if (!mgr || mRegisterCallback.isSome()) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
MOZ_ASSERT(!mPromiseHolder.Exists());
MOZ_ASSERT(mRegisterCallback.isNothing());
mRegisterCallback = Some(nsMainThreadPtrHandle<U2FRegisterCallback>(
new nsMainThreadPtrHolder<U2FRegisterCallback>(
"U2F::Register::callback", &aCallback)));
uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);
// Evaluate the AppID
nsString adjustedAppId;
adjustedAppId.Assign(aAppId);
ErrorCode appIdResult = EvaluateAppID(mOrigin, adjustedAppId);
if (appIdResult != ErrorCode::OK) {
RegisterResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(appIdResult));
ExecuteCallback(response, mRegisterCallback);
return;
}
// Produce the AppParam from the current AppID
nsCString cAppId = NS_ConvertUTF16toUTF8(adjustedAppId);
nsAutoString clientDataJSON;
// Pick the first valid RegisterRequest; we can only work with one.
for (const RegisterRequest& req : aRegisterRequests) {
if (!req.mChallenge.WasPassed() || !req.mVersion.WasPassed() ||
req.mVersion.Value() != kRequiredU2FVersion) {
continue;
}
nsresult rv = AssembleClientData(mOrigin, kFinishEnrollment,
req.mChallenge.Value(), clientDataJSON);
if (NS_WARN_IF(NS_FAILED(rv))) {
continue;
}
}
// Did we not get a valid RegisterRequest? Abort.
if (clientDataJSON.IsEmpty()) {
RegisterResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
ExecuteCallback(response, mRegisterCallback);
return;
}
// Build the exclusion list, if any
nsTArray<WebAuthnScopedCredentialDescriptor> excludeList;
RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys,
excludeList);
auto& localReqHolder = mPromiseHolder;
auto& localCb = mRegisterCallback;
RefPtr<U2FPromise> p = mgr->Register(mParent, cAppId,
NS_ConvertUTF16toUTF8(clientDataJSON),
adjustedTimeoutMillis, excludeList);
p->Then(mEventTarget, "dom::U2F::Register::Promise::Resolve",
[&localCb, &localReqHolder](nsString aResponse) {
MOZ_LOG(gU2FLog, LogLevel::Debug,
("dom::U2F::Register::Promise::Resolve, response was %s",
NS_ConvertUTF16toUTF8(aResponse).get()));
RegisterResponse response;
response.Init(aResponse);
ExecuteCallback(response, localCb);
localReqHolder.Complete();
},
[&localCb, &localReqHolder](ErrorCode aErrorCode) {
MOZ_LOG(gU2FLog, LogLevel::Debug,
("dom::U2F::Register::Promise::Reject, response was %d",
static_cast<uint32_t>(aErrorCode)));
RegisterResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(aErrorCode));
ExecuteCallback(response, localCb);
localReqHolder.Complete();
})
->Track(mPromiseHolder);
}
void
U2F::Sign(const nsAString& aAppId,
const nsAString& aChallenge,
const Sequence<RegisteredKey>& aRegisteredKeys,
U2FSignCallback& aCallback,
const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds,
ErrorResult& aRv)
{
MOZ_ASSERT(NS_IsMainThread());
RefPtr<U2FManager> mgr = U2FManager::GetOrCreate();
MOZ_ASSERT(mgr);
if (!mgr || mSignCallback.isSome()) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
MOZ_ASSERT(!mPromiseHolder.Exists());
MOZ_ASSERT(mSignCallback.isNothing());
mSignCallback = Some(nsMainThreadPtrHandle<U2FSignCallback>(
new nsMainThreadPtrHolder<U2FSignCallback>(
"U2F::Sign::callback", &aCallback)));
uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);
// Evaluate the AppID
nsString adjustedAppId;
adjustedAppId.Assign(aAppId);
ErrorCode appIdResult = EvaluateAppID(mOrigin, adjustedAppId);
if (appIdResult != ErrorCode::OK) {
SignResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(appIdResult));
ExecuteCallback(response, mSignCallback);
return;
}
// Produce the AppParam from the current AppID
nsCString cAppId = NS_ConvertUTF16toUTF8(adjustedAppId);
nsAutoString clientDataJSON;
nsresult rv = AssembleClientData(mOrigin, kGetAssertion, aChallenge,
clientDataJSON);
if (NS_WARN_IF(NS_FAILED(rv))) {
SignResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
ExecuteCallback(response, mSignCallback);
return;
}
// Build the key list, if any
nsTArray<WebAuthnScopedCredentialDescriptor> permittedList;
RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys,
permittedList);
auto& localReqHolder = mPromiseHolder;
auto& localCb = mSignCallback;
RefPtr<U2FPromise> p = mgr->Sign(mParent, cAppId,
NS_ConvertUTF16toUTF8(clientDataJSON),
adjustedTimeoutMillis, permittedList);
p->Then(mEventTarget, "dom::U2F::Sign::Promise::Resolve",
[&localCb, &localReqHolder](nsString aResponse) {
MOZ_LOG(gU2FLog, LogLevel::Debug,
("dom::U2F::Sign::Promise::Resolve, response was %s",
NS_ConvertUTF16toUTF8(aResponse).get()));
SignResponse response;
response.Init(aResponse);
ExecuteCallback(response, localCb);
localReqHolder.Complete();
},
[&localCb, &localReqHolder](ErrorCode aErrorCode) {
MOZ_LOG(gU2FLog, LogLevel::Debug,
("dom::U2F::Sign::Promise::Reject, response was %d",
static_cast<uint32_t>(aErrorCode)));
SignResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(aErrorCode));
ExecuteCallback(response, localCb);
localReqHolder.Complete();
})
->Track(mPromiseHolder);
}
} // namespace dom
} // namespace mozilla