mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1231681 - "Implement window.u2f interface". r=baku, r=dkeeler
This commit is contained in:
parent
8d6de99469
commit
da7ad6c27d
@ -436,6 +436,7 @@ LOCAL_INCLUDES += [
|
||||
'/dom/ipc',
|
||||
'/dom/storage',
|
||||
'/dom/svg',
|
||||
'/dom/u2f',
|
||||
'/dom/workers',
|
||||
'/dom/xbl',
|
||||
'/dom/xml',
|
||||
|
@ -226,6 +226,7 @@
|
||||
#include "mozilla/dom/NavigatorBinding.h"
|
||||
#include "mozilla/dom/ImageBitmap.h"
|
||||
#include "mozilla/dom/ServiceWorkerRegistration.h"
|
||||
#include "mozilla/dom/U2F.h"
|
||||
#ifdef HAVE_SIDEBAR
|
||||
#include "mozilla/dom/ExternalBinding.h"
|
||||
#endif
|
||||
@ -1885,6 +1886,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(nsGlobalWindow)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStatusbar)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mScrollbars)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCrypto)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mU2F)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsole)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExternal)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMozSelfSupport)
|
||||
@ -1959,6 +1961,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsGlobalWindow)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mStatusbar)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mScrollbars)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCrypto)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mU2F)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsole)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mExternal)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mMozSelfSupport)
|
||||
@ -4348,6 +4351,23 @@ nsGlobalWindow::GetCrypto(ErrorResult& aError)
|
||||
return mCrypto;
|
||||
}
|
||||
|
||||
mozilla::dom::U2F*
|
||||
nsGlobalWindow::GetU2f(ErrorResult& aError)
|
||||
{
|
||||
MOZ_RELEASE_ASSERT(IsInnerWindow());
|
||||
|
||||
if (!mU2F) {
|
||||
RefPtr<U2F> u2f = new U2F();
|
||||
u2f->Init(AsInner(), aError);
|
||||
if (NS_WARN_IF(aError.Failed())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
mU2F = u2f;
|
||||
}
|
||||
return mU2F;
|
||||
}
|
||||
|
||||
nsIControllers*
|
||||
nsGlobalWindow::GetControllersOuter(ErrorResult& aError)
|
||||
{
|
||||
|
@ -106,7 +106,6 @@ class Crypto;
|
||||
class External;
|
||||
class Function;
|
||||
class Gamepad;
|
||||
class VRDevice;
|
||||
class MediaQueryList;
|
||||
class MozSelfSupport;
|
||||
class Navigator;
|
||||
@ -117,6 +116,8 @@ struct RequestInit;
|
||||
class RequestOrUSVString;
|
||||
class Selection;
|
||||
class SpeechSynthesis;
|
||||
class U2F;
|
||||
class VRDevice;
|
||||
class WakeLock;
|
||||
#if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GONK)
|
||||
class WindowOrientationObserver;
|
||||
@ -1071,6 +1072,7 @@ public:
|
||||
void SizeToContentOuter(mozilla::ErrorResult& aError, bool aCallerIsChrome);
|
||||
void SizeToContent(mozilla::ErrorResult& aError);
|
||||
mozilla::dom::Crypto* GetCrypto(mozilla::ErrorResult& aError);
|
||||
mozilla::dom::U2F* GetU2f(mozilla::ErrorResult& aError);
|
||||
nsIControllers* GetControllersOuter(mozilla::ErrorResult& aError);
|
||||
nsIControllers* GetControllers(mozilla::ErrorResult& aError);
|
||||
nsresult GetControllers(nsIControllers** aControllers) override;
|
||||
@ -1766,6 +1768,7 @@ protected:
|
||||
nsString mDefaultStatus;
|
||||
RefPtr<nsGlobalWindowObserver> mObserver; // Inner windows only.
|
||||
RefPtr<mozilla::dom::Crypto> mCrypto;
|
||||
RefPtr<mozilla::dom::U2F> mU2F;
|
||||
RefPtr<mozilla::dom::cache::CacheStorage> mCacheStorage;
|
||||
RefPtr<mozilla::dom::Console> mConsole;
|
||||
// We need to store an nsISupports pointer to this object because the
|
||||
|
@ -27,6 +27,13 @@ CryptoBuffer::Assign(const uint8_t* aData, uint32_t aLength)
|
||||
return ReplaceElementsAt(0, Length(), aData, aLength, fallible);
|
||||
}
|
||||
|
||||
uint8_t*
|
||||
CryptoBuffer::Assign(const nsACString& aString)
|
||||
{
|
||||
return Assign(reinterpret_cast<uint8_t const*>(aString.BeginReading()),
|
||||
aString.Length());
|
||||
}
|
||||
|
||||
uint8_t*
|
||||
CryptoBuffer::Assign(const SECItem* aItem)
|
||||
{
|
||||
|
@ -22,6 +22,7 @@ class CryptoBuffer : public FallibleTArray<uint8_t>
|
||||
public:
|
||||
uint8_t* Assign(const CryptoBuffer& aData);
|
||||
uint8_t* Assign(const uint8_t* aData, uint32_t aLength);
|
||||
uint8_t* Assign(const nsACString& aString);
|
||||
uint8_t* Assign(const SECItem* aItem);
|
||||
uint8_t* Assign(const ArrayBuffer& aData);
|
||||
uint8_t* Assign(const ArrayBufferView& aData);
|
||||
|
@ -114,6 +114,7 @@ DIRS += [
|
||||
'manifest',
|
||||
'vr',
|
||||
'newapps',
|
||||
'u2f',
|
||||
]
|
||||
|
||||
if CONFIG['OS_ARCH'] == 'WINNT':
|
||||
|
@ -1377,6 +1377,8 @@ var interfaceNamesInGlobalScope =
|
||||
{name: "TVSource", b2g: true, permission: ["tv"]},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
{name: "TVTuner", b2g: true, permission: ["tv"]},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
{name: "U2F", release: false},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
{name: "UDPMessageEvent", b2g: true, permission: ["udp-socket"]},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
|
172
dom/u2f/NSSToken.cpp
Normal file
172
dom/u2f/NSSToken.cpp
Normal file
@ -0,0 +1,172 @@
|
||||
/* -*- 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 "NSSToken.h"
|
||||
|
||||
#include "nsNSSComponent.h"
|
||||
#include "pk11pub.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
const nsString NSSToken::mVersion = NS_LITERAL_STRING("U2F_V2");
|
||||
|
||||
const uint32_t kParamLen = 32;
|
||||
const uint32_t kPublicKeyLen = 65;
|
||||
const uint32_t kSignedDataLen = (2 * kParamLen) + 1 + 4;
|
||||
|
||||
NSSToken::NSSToken()
|
||||
: mInitialized(false)
|
||||
, mMutex("NSSToken::mMutex")
|
||||
{}
|
||||
|
||||
NSSToken::~NSSToken()
|
||||
{
|
||||
nsNSSShutDownPreventionLock locker;
|
||||
|
||||
if (isAlreadyShutDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
destructorSafeDestroyNSSReference();
|
||||
shutdown(calledFromObject);
|
||||
}
|
||||
|
||||
void
|
||||
NSSToken::virtualDestroyNSSReference()
|
||||
{
|
||||
destructorSafeDestroyNSSReference();
|
||||
}
|
||||
|
||||
void
|
||||
NSSToken::destructorSafeDestroyNSSReference()
|
||||
{
|
||||
mSlot = nullptr;
|
||||
}
|
||||
|
||||
nsresult
|
||||
NSSToken::Init()
|
||||
{
|
||||
MOZ_ASSERT(!mInitialized);
|
||||
if (mInitialized) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsNSSShutDownPreventionLock locker;
|
||||
if (isAlreadyShutDown()) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
MutexAutoLock lock(mMutex);
|
||||
|
||||
if (!EnsureNSSInitializedChromeOrContent()) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
mSlot = PK11_GetInternalSlot();
|
||||
if (!mSlot.get()) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
mInitialized = true;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool
|
||||
NSSToken::IsCompatibleVersion(const nsString& aVersionParam) const
|
||||
{
|
||||
MOZ_ASSERT(mInitialized);
|
||||
return mVersion == aVersionParam;
|
||||
}
|
||||
|
||||
/*
|
||||
* IsRegistered determines if the provided key handle is usable by this token.
|
||||
*/
|
||||
bool
|
||||
NSSToken::IsRegistered(const CryptoBuffer& aKeyHandle) const
|
||||
{
|
||||
MOZ_ASSERT(mInitialized);
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* A U2F Register operation causes a new key pair to be generated by the token.
|
||||
* The token then returns the public key of the key pair, and a handle to the
|
||||
* private key. The input parameters are used only for attestation, which this
|
||||
* token does not provide. (We'll see how that works!)
|
||||
*
|
||||
* The format of the return registration data is as follows:
|
||||
*
|
||||
* Bytes Value
|
||||
* 1 0x05
|
||||
* 65 public key
|
||||
* 1 key handle length
|
||||
* * key handle
|
||||
* * attestation certificate (omitted for now)
|
||||
* * attestation signature (omitted for now)
|
||||
*
|
||||
*/
|
||||
nsresult
|
||||
NSSToken::Register(const CryptoBuffer& /* aChallengeParam */,
|
||||
const CryptoBuffer& /* aApplicationParam */,
|
||||
CryptoBuffer& aRegistrationData)
|
||||
{
|
||||
MOZ_ASSERT(mInitialized);
|
||||
nsNSSShutDownPreventionLock locker;
|
||||
if (isAlreadyShutDown()) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
MutexAutoLock lock(mMutex);
|
||||
|
||||
if (!mInitialized) {
|
||||
return NS_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* A U2F Sign operation creates a signature over the "param" arguments (plus
|
||||
* some other stuff) using the private key indicated in the key handle argument.
|
||||
*
|
||||
* The format of the signed data is as follows:
|
||||
*
|
||||
* 32 Application parameter
|
||||
* 1 User presence (0x01)
|
||||
* 4 Counter
|
||||
* 32 Challenge parameter
|
||||
*
|
||||
* The format of the signature data is as follows:
|
||||
*
|
||||
* 1 User presence
|
||||
* 4 Counter
|
||||
* * Signature
|
||||
*
|
||||
*/
|
||||
nsresult
|
||||
NSSToken::Sign(const CryptoBuffer& aApplicationParam,
|
||||
const CryptoBuffer& aChallengeParam,
|
||||
const CryptoBuffer& aKeyHandle,
|
||||
CryptoBuffer& aSignatureData)
|
||||
{
|
||||
MOZ_ASSERT(mInitialized);
|
||||
nsNSSShutDownPreventionLock locker;
|
||||
if (isAlreadyShutDown()) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
MutexAutoLock lock(mMutex);
|
||||
|
||||
if (!mInitialized) {
|
||||
return NS_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
57
dom/u2f/NSSToken.h
Normal file
57
dom/u2f/NSSToken.h
Normal file
@ -0,0 +1,57 @@
|
||||
/* -*- 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/. */
|
||||
|
||||
#ifndef mozilla_dom_NSSToken_h
|
||||
#define mozilla_dom_NSSToken_h
|
||||
|
||||
#include "mozilla/dom/CryptoBuffer.h"
|
||||
#include "mozilla/Mutex.h"
|
||||
#include "nsNSSShutDown.h"
|
||||
#include "ScopedNSSTypes.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
// NSSToken will support FIDO U2F operations using NSS for the crypto layer.
|
||||
// This is a stub. It will be implemented in bug 1244960.
|
||||
class NSSToken final : public nsNSSShutDownObject
|
||||
{
|
||||
public:
|
||||
NSSToken();
|
||||
|
||||
~NSSToken();
|
||||
|
||||
nsresult Init();
|
||||
|
||||
bool IsCompatibleVersion(const nsString& aVersionParam) const;
|
||||
|
||||
bool IsRegistered(const CryptoBuffer& aKeyHandle) const;
|
||||
|
||||
nsresult Register(const CryptoBuffer& aApplicationParam,
|
||||
const CryptoBuffer& aChallengeParam,
|
||||
CryptoBuffer& aRegistrationData);
|
||||
|
||||
nsresult Sign(const CryptoBuffer& aApplicationParam,
|
||||
const CryptoBuffer& aChallengeParam,
|
||||
const CryptoBuffer& aKeyHandle,
|
||||
CryptoBuffer& aSignatureData);
|
||||
|
||||
// For nsNSSShutDownObject
|
||||
virtual void virtualDestroyNSSReference() override;
|
||||
void destructorSafeDestroyNSSReference();
|
||||
|
||||
private:
|
||||
bool mInitialized;
|
||||
ScopedPK11SlotInfo mSlot;
|
||||
mozilla::Mutex mMutex;
|
||||
|
||||
static const nsString mVersion;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_dom_NSSToken_h
|
583
dom/u2f/U2F.cpp
Normal file
583
dom/u2f/U2F.cpp
Normal file
@ -0,0 +1,583 @@
|
||||
/* -*- 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/CryptoBuffer.h"
|
||||
#include "mozilla/dom/U2F.h"
|
||||
#include "mozilla/dom/U2FBinding.h"
|
||||
#include "mozilla/Preferences.h"
|
||||
#include "nsContentUtils.h"
|
||||
#include "nsIEffectiveTLDService.h"
|
||||
#include "nsURLParsers.h"
|
||||
#include "nsNetCID.h"
|
||||
#include "pk11pub.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
// These enumerations are defined in the FIDO U2F Javascript API under the
|
||||
// interface "ErrorCode" as constant integers, and thus in the U2F.webidl file.
|
||||
// Any changes to these must occur in both locations.
|
||||
enum class ErrorCode {
|
||||
OK = 0,
|
||||
OTHER_ERROR = 1,
|
||||
BAD_REQUEST = 2,
|
||||
CONFIGURATION_UNSUPPORTED = 3,
|
||||
DEVICE_INELIGIBLE = 4,
|
||||
TIMEOUT = 5
|
||||
};
|
||||
|
||||
#define PREF_U2F_SOFTTOKEN_ENABLED "security.webauth.u2f.softtoken"
|
||||
#define PREF_U2F_USBTOKEN_ENABLED "security.webauth.u2f.usbtoken"
|
||||
|
||||
const nsString
|
||||
U2F::FinishEnrollment = NS_LITERAL_STRING("navigator.id.finishEnrollment");
|
||||
|
||||
const nsString
|
||||
U2F::GetAssertion = NS_LITERAL_STRING("navigator.id.getAssertion");
|
||||
|
||||
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)
|
||||
|
||||
U2F::U2F()
|
||||
{}
|
||||
|
||||
U2F::~U2F()
|
||||
{
|
||||
nsNSSShutDownPreventionLock locker;
|
||||
|
||||
if (isAlreadyShutDown()) {
|
||||
return;
|
||||
}
|
||||
shutdown(calledFromObject);
|
||||
}
|
||||
|
||||
/* virtual */ JSObject*
|
||||
U2F::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
|
||||
{
|
||||
return U2FBinding::Wrap(aCx, this, aGivenProto);
|
||||
}
|
||||
|
||||
void
|
||||
U2F::Init(nsPIDOMWindowInner* aParent, ErrorResult& aRv)
|
||||
{
|
||||
MOZ_ASSERT(!mParent);
|
||||
mParent = do_QueryInterface(aParent);
|
||||
MOZ_ASSERT(mParent);
|
||||
|
||||
nsCOMPtr<nsIDocument> doc = mParent->GetDoc();
|
||||
MOZ_ASSERT(doc);
|
||||
|
||||
nsIPrincipal* principal = doc->NodePrincipal();
|
||||
aRv = nsContentUtils::GetUTFOrigin(principal, mOrigin);
|
||||
if (NS_WARN_IF(aRv.Failed())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (NS_WARN_IF(mOrigin.IsEmpty())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EnsureNSSInitializedChromeOrContent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
aRv = mSoftToken.Init();
|
||||
if (NS_WARN_IF(aRv.Failed())) {
|
||||
return;
|
||||
}
|
||||
|
||||
aRv = mUSBToken.Init();
|
||||
if (NS_WARN_IF(aRv.Failed())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
nsresult
|
||||
U2F::AssembleClientData(const nsAString& aTyp,
|
||||
const nsAString& aChallenge,
|
||||
CryptoBuffer& aClientData) const
|
||||
{
|
||||
ClientData clientDataObject;
|
||||
clientDataObject.mTyp.Construct(aTyp); // "Typ" from the U2F specification
|
||||
clientDataObject.mChallenge.Construct(aChallenge);
|
||||
clientDataObject.mOrigin.Construct(mOrigin);
|
||||
|
||||
nsAutoString json;
|
||||
if (NS_WARN_IF(!clientDataObject.ToJSON(json))) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
if (NS_WARN_IF(!aClientData.Assign(NS_ConvertUTF16toUTF8(json)))) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool
|
||||
U2F::ValidAppID(/* in/out */ nsString& aAppId) const
|
||||
{
|
||||
nsCOMPtr<nsIURLParser> urlParser =
|
||||
do_GetService(NS_STDURLPARSER_CONTRACTID);
|
||||
nsCOMPtr<nsIEffectiveTLDService> tldService =
|
||||
do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
|
||||
|
||||
MOZ_ASSERT(urlParser);
|
||||
MOZ_ASSERT(tldService);
|
||||
|
||||
uint32_t facetSchemePos;
|
||||
int32_t facetSchemeLen;
|
||||
uint32_t facetAuthPos;
|
||||
int32_t facetAuthLen;
|
||||
// Facet is the specification's way of referring to the web origin.
|
||||
nsAutoCString facetUrl = NS_ConvertUTF16toUTF8(mOrigin);
|
||||
nsresult rv = urlParser->ParseURL(facetUrl.get(), mOrigin.Length(),
|
||||
&facetSchemePos, &facetSchemeLen,
|
||||
&facetAuthPos, &facetAuthLen,
|
||||
nullptr, nullptr); // ignore path
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nsAutoCString facetScheme(Substring(facetUrl, facetSchemePos, facetSchemeLen));
|
||||
nsAutoCString facetAuth(Substring(facetUrl, facetAuthPos, facetAuthLen));
|
||||
|
||||
uint32_t appIdSchemePos;
|
||||
int32_t appIdSchemeLen;
|
||||
uint32_t appIdAuthPos;
|
||||
int32_t appIdAuthLen;
|
||||
nsAutoCString appIdUrl = NS_ConvertUTF16toUTF8(aAppId);
|
||||
rv = urlParser->ParseURL(appIdUrl.get(), aAppId.Length(),
|
||||
&appIdSchemePos, &appIdSchemeLen,
|
||||
&appIdAuthPos, &appIdAuthLen,
|
||||
nullptr, nullptr); // ignore path
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nsAutoCString appIdScheme(Substring(appIdUrl, appIdSchemePos, appIdSchemeLen));
|
||||
nsAutoCString appIdAuth(Substring(appIdUrl, appIdAuthPos, appIdAuthLen));
|
||||
|
||||
// If the facetId (origin) is not HTTPS, reject
|
||||
if (!facetScheme.LowerCaseEqualsLiteral("https")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the appId is empty or null, overwrite it with the facetId and accept
|
||||
if (aAppId.IsEmpty() || aAppId.EqualsLiteral("null")) {
|
||||
aAppId.Assign(mOrigin);
|
||||
return true;
|
||||
}
|
||||
|
||||
// if the appId URL is not HTTPS, reject.
|
||||
if (!appIdScheme.LowerCaseEqualsLiteral("https")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the facetId and the appId auths match, accept
|
||||
if (facetAuth == appIdAuth) {
|
||||
return true;
|
||||
}
|
||||
|
||||
nsAutoCString appIdTld;
|
||||
nsAutoCString facetTld;
|
||||
|
||||
rv = tldService->GetBaseDomainFromHost(appIdAuth, 0, appIdTld);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return false;
|
||||
}
|
||||
rv = tldService->GetBaseDomainFromHost(facetAuth, 0, facetTld);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If this AppID's registered domain matches the Facet's, accept
|
||||
if (!facetTld.IsEmpty() && !appIdTld.IsEmpty() &&
|
||||
(facetTld == appIdTld)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO(Bug 1244959) Implement the remaining algorithm.
|
||||
return false;
|
||||
}
|
||||
|
||||
template <class CB, class Rsp>
|
||||
void
|
||||
SendError(CB& aCallback, ErrorCode aErrorCode)
|
||||
{
|
||||
Rsp response;
|
||||
response.mErrorCode.Construct(static_cast<uint32_t>(aErrorCode));
|
||||
|
||||
ErrorResult rv;
|
||||
aCallback.Call(response, rv);
|
||||
NS_WARN_IF(rv.Failed());
|
||||
// Useful exceptions already got reported.
|
||||
rv.SuppressException();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
nsNSSShutDownPreventionLock locker;
|
||||
if (isAlreadyShutDown()) {
|
||||
SendError<U2FRegisterCallback, RegisterResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool softTokenEnabled =
|
||||
Preferences::GetBool(PREF_U2F_SOFTTOKEN_ENABLED);
|
||||
|
||||
const bool usbTokenEnabled =
|
||||
Preferences::GetBool(PREF_U2F_USBTOKEN_ENABLED);
|
||||
|
||||
nsAutoString appId(aAppId);
|
||||
|
||||
// Verify the global appId first.
|
||||
if (!ValidAppID(appId)) {
|
||||
SendError<U2FRegisterCallback, RegisterResponse>(aCallback,
|
||||
ErrorCode::BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < aRegisteredKeys.Length(); ++i) {
|
||||
RegisteredKey request(aRegisteredKeys[i]);
|
||||
|
||||
// Check for equired attributes
|
||||
if (!(request.mKeyHandle.WasPassed() &&
|
||||
request.mVersion.WasPassed())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify the appId for this Registered Key, if set
|
||||
if (request.mAppId.WasPassed() &&
|
||||
!ValidAppID(request.mAppId.Value())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decode the key handle
|
||||
CryptoBuffer keyHandle;
|
||||
nsresult rv = keyHandle.FromJwkBase64(request.mKeyHandle.Value());
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
SendError<U2FRegisterCallback, RegisterResponse>(aCallback,
|
||||
ErrorCode::BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
|
||||
// We ignore mTransports, as it is intended to be used for sorting the
|
||||
// available devices by preference, but is not an exclusion factor.
|
||||
|
||||
// Determine if the provided keyHandle is registered at any device. If so,
|
||||
// then we'll return DEVICE_INELIGIBLE to signify we're already registered.
|
||||
if (usbTokenEnabled &&
|
||||
mUSBToken.IsCompatibleVersion(request.mVersion.Value()) &&
|
||||
mUSBToken.IsRegistered(keyHandle)) {
|
||||
SendError<U2FRegisterCallback, RegisterResponse>(aCallback,
|
||||
ErrorCode::DEVICE_INELIGIBLE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (softTokenEnabled &&
|
||||
mSoftToken.IsCompatibleVersion(request.mVersion.Value()) &&
|
||||
mSoftToken.IsRegistered(keyHandle)) {
|
||||
SendError<U2FRegisterCallback, RegisterResponse>(aCallback,
|
||||
ErrorCode::DEVICE_INELIGIBLE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Search the requests in order for the first some token can fulfill
|
||||
for (size_t i = 0; i < aRegisterRequests.Length(); ++i) {
|
||||
RegisterRequest request(aRegisterRequests[i]);
|
||||
|
||||
// Check for equired attributes
|
||||
if (!(request.mVersion.WasPassed() &&
|
||||
request.mChallenge.WasPassed())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CryptoBuffer clientData;
|
||||
nsresult rv = AssembleClientData(FinishEnrollment,
|
||||
request.mChallenge.Value(),
|
||||
clientData);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
SendError<U2FRegisterCallback, RegisterResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// Hash the AppID and the ClientData into the AppParam and ChallengeParam
|
||||
SECStatus srv;
|
||||
nsCString cAppId = NS_ConvertUTF16toUTF8(appId);
|
||||
CryptoBuffer appParam;
|
||||
CryptoBuffer challengeParam;
|
||||
if (!appParam.SetLength(SHA256_LENGTH, fallible) ||
|
||||
!challengeParam.SetLength(SHA256_LENGTH, fallible)) {
|
||||
SendError<U2FRegisterCallback, RegisterResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
srv = PK11_HashBuf(SEC_OID_SHA256, appParam.Elements(),
|
||||
reinterpret_cast<const uint8_t*>(cAppId.BeginReading()),
|
||||
cAppId.Length());
|
||||
if (srv != SECSuccess) {
|
||||
SendError<U2FRegisterCallback, RegisterResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
srv = PK11_HashBuf(SEC_OID_SHA256, challengeParam.Elements(),
|
||||
clientData.Elements(), clientData.Length());
|
||||
if (srv != SECSuccess) {
|
||||
SendError<U2FRegisterCallback, RegisterResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the registration data from the token
|
||||
CryptoBuffer registrationData;
|
||||
bool registerSuccess = false;
|
||||
|
||||
if (usbTokenEnabled &&
|
||||
mUSBToken.IsCompatibleVersion(request.mVersion.Value())) {
|
||||
rv = mUSBToken.Register(opt_aTimeoutSeconds, challengeParam,
|
||||
appParam, registrationData);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
SendError<U2FRegisterCallback, RegisterResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
registerSuccess = true;
|
||||
}
|
||||
|
||||
if (!registerSuccess && softTokenEnabled &&
|
||||
mSoftToken.IsCompatibleVersion(request.mVersion.Value())) {
|
||||
rv = mSoftToken.Register(challengeParam, appParam, registrationData);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
SendError<U2FRegisterCallback, RegisterResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
registerSuccess = true;
|
||||
}
|
||||
|
||||
if (!registerSuccess) {
|
||||
// Try another request
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assemble a response object to return
|
||||
nsString clientDataBase64, registrationDataBase64;
|
||||
nsresult rvClientData =
|
||||
clientData.ToJwkBase64(clientDataBase64);
|
||||
nsresult rvRegistrationData =
|
||||
registrationData.ToJwkBase64(registrationDataBase64);
|
||||
if (NS_WARN_IF(NS_FAILED(rvClientData)) ||
|
||||
NS_WARN_IF(NS_FAILED(rvRegistrationData))) {
|
||||
SendError<U2FRegisterCallback, RegisterResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
RegisterResponse response;
|
||||
response.mClientData.Construct(clientDataBase64);
|
||||
response.mRegistrationData.Construct(registrationDataBase64);
|
||||
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OK));
|
||||
|
||||
ErrorResult result;
|
||||
aCallback.Call(response, result);
|
||||
NS_WARN_IF(result.Failed());
|
||||
// Useful exceptions already got reported.
|
||||
result.SuppressException();
|
||||
return;
|
||||
}
|
||||
|
||||
// Nothing could satisfy
|
||||
SendError<U2FRegisterCallback, RegisterResponse>(aCallback,
|
||||
ErrorCode::BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
|
||||
void
|
||||
U2F::Sign(const nsAString& aAppId,
|
||||
const nsAString& aChallenge,
|
||||
const Sequence<RegisteredKey>& aRegisteredKeys,
|
||||
U2FSignCallback& aCallback,
|
||||
const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
nsNSSShutDownPreventionLock locker;
|
||||
if (isAlreadyShutDown()) {
|
||||
SendError<U2FSignCallback, SignResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool softTokenEnabled =
|
||||
Preferences::GetBool(PREF_U2F_SOFTTOKEN_ENABLED);
|
||||
|
||||
const bool usbTokenEnabled =
|
||||
Preferences::GetBool(PREF_U2F_USBTOKEN_ENABLED);
|
||||
|
||||
nsAutoString appId(aAppId);
|
||||
|
||||
// Verify the global appId first.
|
||||
if (!ValidAppID(appId)) {
|
||||
SendError<U2FSignCallback, SignResponse>(aCallback,
|
||||
ErrorCode::BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
|
||||
// Search the requests for one a token can fulfill
|
||||
for (size_t i = 0; i < aRegisteredKeys.Length(); i += 1) {
|
||||
RegisteredKey request(aRegisteredKeys[i]);
|
||||
|
||||
// Check for required attributes
|
||||
if (!(request.mVersion.WasPassed() &&
|
||||
request.mKeyHandle.WasPassed())) {
|
||||
SendError<U2FSignCallback, SignResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allow an individual RegisteredKey to assert a different AppID
|
||||
nsAutoString regKeyAppId(appId);
|
||||
if (request.mAppId.WasPassed()) {
|
||||
regKeyAppId.Assign(request.mAppId.Value());
|
||||
if (!ValidAppID(regKeyAppId)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble a clientData object
|
||||
CryptoBuffer clientData;
|
||||
nsresult rv = AssembleClientData(GetAssertion, aChallenge, clientData);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
SendError<U2FSignCallback, SignResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// Hash the AppID and the ClientData into the AppParam and ChallengeParam
|
||||
SECStatus srv;
|
||||
nsCString cAppId = NS_ConvertUTF16toUTF8(regKeyAppId);
|
||||
CryptoBuffer appParam;
|
||||
CryptoBuffer challengeParam;
|
||||
if (!appParam.SetLength(SHA256_LENGTH, fallible) ||
|
||||
!challengeParam.SetLength(SHA256_LENGTH, fallible)) {
|
||||
SendError<U2FSignCallback, SignResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
srv = PK11_HashBuf(SEC_OID_SHA256, appParam.Elements(),
|
||||
reinterpret_cast<const uint8_t*>(cAppId.BeginReading()),
|
||||
cAppId.Length());
|
||||
if (srv != SECSuccess) {
|
||||
SendError<U2FSignCallback, SignResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
srv = PK11_HashBuf(SEC_OID_SHA256, challengeParam.Elements(),
|
||||
clientData.Elements(), clientData.Length());
|
||||
if (srv != SECSuccess) {
|
||||
SendError<U2FSignCallback, SignResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode the key handle
|
||||
CryptoBuffer keyHandle;
|
||||
rv = keyHandle.FromJwkBase64(request.mKeyHandle.Value());
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
SendError<U2FSignCallback, SignResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the signature from the token
|
||||
CryptoBuffer signatureData;
|
||||
bool signSuccess = false;
|
||||
|
||||
// We ignore mTransports, as it is intended to be used for sorting the
|
||||
// available devices by preference, but is not an exclusion factor.
|
||||
|
||||
if (usbTokenEnabled &&
|
||||
mUSBToken.IsCompatibleVersion(request.mVersion.Value())) {
|
||||
rv = mUSBToken.Sign(opt_aTimeoutSeconds, appParam, challengeParam,
|
||||
keyHandle, signatureData);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
SendError<U2FSignCallback, SignResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
signSuccess = true;
|
||||
}
|
||||
|
||||
if (!signSuccess && softTokenEnabled &&
|
||||
mSoftToken.IsCompatibleVersion(request.mVersion.Value())) {
|
||||
rv = mSoftToken.Sign(appParam, challengeParam, keyHandle, signatureData);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
SendError<U2FSignCallback, SignResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
signSuccess = true;
|
||||
}
|
||||
|
||||
if (!signSuccess) {
|
||||
// Try another request
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assemble a response object to return
|
||||
nsString clientDataBase64, signatureDataBase64;
|
||||
nsresult rvClientData =
|
||||
clientData.ToJwkBase64(clientDataBase64);
|
||||
nsresult rvSignatureData =
|
||||
signatureData.ToJwkBase64(signatureDataBase64);
|
||||
if (NS_WARN_IF(NS_FAILED(rvClientData)) ||
|
||||
NS_WARN_IF(NS_FAILED(rvSignatureData))) {
|
||||
SendError<U2FSignCallback, SignResponse>(aCallback,
|
||||
ErrorCode::OTHER_ERROR);
|
||||
return;
|
||||
}
|
||||
SignResponse response;
|
||||
response.mKeyHandle.Construct(request.mKeyHandle.Value());
|
||||
response.mClientData.Construct(clientDataBase64);
|
||||
response.mSignatureData.Construct(signatureDataBase64);
|
||||
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OK));
|
||||
|
||||
ErrorResult result;
|
||||
aCallback.Call(response, result);
|
||||
NS_WARN_IF(result.Failed());
|
||||
// Useful exceptions already got reported.
|
||||
result.SuppressException();
|
||||
return;
|
||||
}
|
||||
|
||||
// Nothing could satisfy
|
||||
SendError<U2FSignCallback, SignResponse>(aCallback,
|
||||
ErrorCode::DEVICE_INELIGIBLE);
|
||||
return;
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
106
dom/u2f/U2F.h
Normal file
106
dom/u2f/U2F.h
Normal file
@ -0,0 +1,106 @@
|
||||
/* -*- 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/. */
|
||||
|
||||
#ifndef mozilla_dom_U2F_h
|
||||
#define mozilla_dom_U2F_h
|
||||
|
||||
#include "js/TypeDecls.h"
|
||||
#include "mozilla/Attributes.h"
|
||||
#include "mozilla/dom/BindingDeclarations.h"
|
||||
#include "mozilla/dom/Nullable.h"
|
||||
#include "mozilla/ErrorResult.h"
|
||||
#include "nsCycleCollectionParticipant.h"
|
||||
#include "nsPIDOMWindow.h"
|
||||
#include "nsWrapperCache.h"
|
||||
|
||||
#include "NSSToken.h"
|
||||
#include "USBToken.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
struct RegisterRequest;
|
||||
struct RegisteredKey;
|
||||
class U2FRegisterCallback;
|
||||
class U2FSignCallback;
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
class U2F final : public nsISupports,
|
||||
public nsWrapperCache,
|
||||
public nsNSSShutDownObject
|
||||
{
|
||||
public:
|
||||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
||||
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(U2F)
|
||||
|
||||
U2F();
|
||||
|
||||
nsPIDOMWindowInner*
|
||||
GetParentObject() const
|
||||
{
|
||||
return mParent;
|
||||
}
|
||||
|
||||
void
|
||||
Init(nsPIDOMWindowInner* aParent, ErrorResult& aRv);
|
||||
|
||||
virtual JSObject*
|
||||
WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
void
|
||||
Register(const nsAString& aAppId,
|
||||
const Sequence<RegisterRequest>& aRegisterRequests,
|
||||
const Sequence<RegisteredKey>& aRegisteredKeys,
|
||||
U2FRegisterCallback& aCallback,
|
||||
const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds,
|
||||
ErrorResult& aRv);
|
||||
|
||||
void
|
||||
Sign(const nsAString& aAppId,
|
||||
const nsAString& aChallenge,
|
||||
const Sequence<RegisteredKey>& aRegisteredKeys,
|
||||
U2FSignCallback& aCallback,
|
||||
const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds,
|
||||
ErrorResult& aRv);
|
||||
|
||||
// No NSS resources to release.
|
||||
virtual
|
||||
void virtualDestroyNSSReference() override {};
|
||||
|
||||
private:
|
||||
nsCOMPtr<nsPIDOMWindowInner> mParent;
|
||||
nsString mOrigin;
|
||||
NSSToken mSoftToken;
|
||||
USBToken mUSBToken;
|
||||
|
||||
static const nsString FinishEnrollment;
|
||||
static const nsString GetAssertion;
|
||||
|
||||
~U2F();
|
||||
|
||||
nsresult
|
||||
AssembleClientData(const nsAString& aTyp,
|
||||
const nsAString& aChallenge,
|
||||
CryptoBuffer& aClientData) const;
|
||||
|
||||
// ValidAppID determines whether the supplied FIDO AppID is valid for
|
||||
// the current FacetID, e.g., the current origin. If the supplied
|
||||
// aAppId param is null or empty, it will be filled in per the algorithm.
|
||||
// See https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-appid-and-facets.html
|
||||
// for a description of the algorithm.
|
||||
bool
|
||||
ValidAppID(/* in/out */ nsString& aAppId) const;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_dom_U2F_h
|
72
dom/u2f/USBToken.cpp
Normal file
72
dom/u2f/USBToken.cpp
Normal file
@ -0,0 +1,72 @@
|
||||
/* -*- 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 "USBToken.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
USBToken::USBToken()
|
||||
: mInitialized(false)
|
||||
{}
|
||||
|
||||
USBToken::~USBToken()
|
||||
{}
|
||||
|
||||
nsresult
|
||||
USBToken::Init()
|
||||
{
|
||||
// This routine does nothing at present, but Bug 1245527 will
|
||||
// integrate the upcoming USB HID service here, which will likely
|
||||
// require an initialization upon load.
|
||||
MOZ_ASSERT(!mInitialized);
|
||||
if (mInitialized) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
mInitialized = true;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
const nsString USBToken::mVersion = NS_LITERAL_STRING("U2F_V2");
|
||||
|
||||
bool
|
||||
USBToken::IsCompatibleVersion(const nsString& aVersionParam) const
|
||||
{
|
||||
MOZ_ASSERT(mInitialized);
|
||||
return mVersion == aVersionParam;
|
||||
}
|
||||
|
||||
bool
|
||||
USBToken::IsRegistered(const CryptoBuffer& aKeyHandle) const
|
||||
{
|
||||
MOZ_ASSERT(mInitialized);
|
||||
return false;
|
||||
}
|
||||
|
||||
nsresult
|
||||
USBToken::Register(const Optional<Nullable<int32_t>>& opt_timeoutSeconds,
|
||||
const CryptoBuffer& /* aChallengeParam */,
|
||||
const CryptoBuffer& /* aApplicationParam */,
|
||||
CryptoBuffer& aRegistrationData) const
|
||||
{
|
||||
MOZ_ASSERT(mInitialized);
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
nsresult
|
||||
USBToken::Sign(const Optional<Nullable<int32_t>>& opt_timeoutSeconds,
|
||||
const CryptoBuffer& aApplicationParam,
|
||||
const CryptoBuffer& aChallengeParam,
|
||||
const CryptoBuffer& aKeyHandle,
|
||||
CryptoBuffer& aSignatureData) const
|
||||
{
|
||||
MOZ_ASSERT(mInitialized);
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
49
dom/u2f/USBToken.h
Normal file
49
dom/u2f/USBToken.h
Normal file
@ -0,0 +1,49 @@
|
||||
/* -*- 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/. */
|
||||
|
||||
#ifndef mozilla_dom_USBToken_h
|
||||
#define mozilla_dom_USBToken_h
|
||||
|
||||
#include "mozilla/dom/CryptoBuffer.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
// USBToken implements FIDO operations using a USB device.
|
||||
class USBToken final
|
||||
{
|
||||
public:
|
||||
USBToken();
|
||||
|
||||
~USBToken();
|
||||
|
||||
nsresult Init();
|
||||
|
||||
bool IsCompatibleVersion(const nsString& aVersionParam) const;
|
||||
|
||||
bool IsRegistered(const CryptoBuffer& aKeyHandle) const;
|
||||
|
||||
nsresult Register(const Optional<Nullable<int32_t>>& opt_timeoutSeconds,
|
||||
const CryptoBuffer& aApplicationParam,
|
||||
const CryptoBuffer& aChallengeParam,
|
||||
CryptoBuffer& aRegistrationData) const;
|
||||
|
||||
nsresult Sign(const Optional<Nullable<int32_t>>& opt_timeoutSeconds,
|
||||
const CryptoBuffer& aApplicationParam,
|
||||
const CryptoBuffer& aChallengeParam,
|
||||
const CryptoBuffer& aKeyHandle,
|
||||
CryptoBuffer& aSignatureData) const;
|
||||
|
||||
private:
|
||||
bool mInitialized;
|
||||
|
||||
static const nsString mVersion;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_dom_USBToken_h
|
28
dom/u2f/moz.build
Normal file
28
dom/u2f/moz.build
Normal file
@ -0,0 +1,28 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
EXPORTS.mozilla.dom += [
|
||||
'NSSToken.h',
|
||||
'U2F.h',
|
||||
'USBToken.h',
|
||||
]
|
||||
|
||||
UNIFIED_SOURCES += [
|
||||
'NSSToken.cpp',
|
||||
'U2F.cpp',
|
||||
'USBToken.cpp',
|
||||
]
|
||||
|
||||
FINAL_LIBRARY = 'xul'
|
||||
|
||||
LOCAL_INCLUDES += [
|
||||
'/dom/base',
|
||||
'/dom/crypto',
|
||||
'/security/manager/ssl',
|
||||
'/security/pkix/include',
|
||||
]
|
||||
|
||||
MOCHITEST_MANIFESTS += ['tests/mochitest.ini']
|
8
dom/u2f/tests/facet/facetList-good
Normal file
8
dom/u2f/tests/facet/facetList-good
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"trustedFacets" : [{
|
||||
"version": { "major": 1, "minor" : 0 },
|
||||
"ids": [
|
||||
"https://fido.example.com"
|
||||
]
|
||||
}]
|
||||
}
|
1
dom/u2f/tests/facet/facetList-good^headers^
Normal file
1
dom/u2f/tests/facet/facetList-good^headers^
Normal file
@ -0,0 +1 @@
|
||||
Content-Type: application/fido.trusted-apps+json
|
6
dom/u2f/tests/facet/facetList-invalid_format
Normal file
6
dom/u2f/tests/facet/facetList-invalid_format
Normal file
@ -0,0 +1,6 @@
|
||||
# This file isn't actually JSON, so it shouldn't successfully parse.
|
||||
{
|
||||
"trustedFacets" : [{
|
||||
"version": { "major": 1, "minor" : 0 },
|
||||
},{}]
|
||||
}
|
1
dom/u2f/tests/facet/facetList-invalid_format^headers^
Normal file
1
dom/u2f/tests/facet/facetList-invalid_format^headers^
Normal file
@ -0,0 +1 @@
|
||||
Content-Type: application/fido.trusted-apps+json
|
9
dom/u2f/tests/facet/facetList-no_overlap
Normal file
9
dom/u2f/tests/facet/facetList-no_overlap
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"trustedFacets" : [{
|
||||
"version": { "major": 1, "minor" : 0 },
|
||||
"ids": [
|
||||
"https://example.net",
|
||||
"http://www.example.com"
|
||||
]
|
||||
}]
|
||||
}
|
1
dom/u2f/tests/facet/facetList-no_overlap^headers^
Normal file
1
dom/u2f/tests/facet/facetList-no_overlap^headers^
Normal file
@ -0,0 +1 @@
|
||||
Content-Type: application/fido.trusted-apps+json
|
8
dom/u2f/tests/facet/facetList.txt
Normal file
8
dom/u2f/tests/facet/facetList.txt
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"trustedFacets" : [{
|
||||
"version": { "major": 1, "minor" : 0 },
|
||||
"ids": [
|
||||
"https://fido.example.com"
|
||||
]
|
||||
}]
|
||||
}
|
19
dom/u2f/tests/mochitest.ini
Normal file
19
dom/u2f/tests/mochitest.ini
Normal file
@ -0,0 +1,19 @@
|
||||
[DEFAULT]
|
||||
support-files =
|
||||
u2futil.js
|
||||
test_frame_appid_facet.html
|
||||
test_frame_register.html
|
||||
test_frame_appid_facet_remoteload.html
|
||||
test_frame_appid_facet_insecure.html
|
||||
test_frame_appid_facet_subdomain.html
|
||||
facet/facetList.txt
|
||||
facet/facetList-good
|
||||
facet/facetList-good^headers^
|
||||
facet/facetList-no_overlap
|
||||
facet/facetList-no_overlap^headers^
|
||||
facet/facetList-invalid_format
|
||||
facet/facetList-invalid_format^headers^
|
||||
|
||||
[test_util_methods.html]
|
||||
[test_no_token.html]
|
||||
[test_frame.html]
|
67
dom/u2f/tests/test_frame.html
Normal file
67
dom/u2f/tests/test_frame.html
Normal file
@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<head>
|
||||
<title>Test for AppID / FacetID behavior for FIDO Universal Second Factor</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="u2futil.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1231681">Mozilla Bug 1231681</a>
|
||||
<p id="display"></p>
|
||||
|
||||
<div id="content" style="display: none"></div>
|
||||
|
||||
<div id="framediv">
|
||||
<iframe id="testing_frame"></iframe>
|
||||
</div>
|
||||
|
||||
<pre id="log"></pre>
|
||||
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f", true);
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f.softtoken", true);
|
||||
|
||||
var testList = [
|
||||
"https://example.com/tests/dom/u2f/tests/test_frame_register.html",
|
||||
"http://mochi.test:8888/tests/dom/u2f/tests/test_frame_appid_facet_insecure.html",
|
||||
"https://example.com/tests/dom/u2f/tests/test_frame_appid_facet.html",
|
||||
"https://example.com/tests/dom/u2f/tests/test_frame_appid_facet_remoteload.html",
|
||||
"https://test1.example.com/tests/dom/u2f/tests/test_frame_appid_facet_subdomain.html"
|
||||
];
|
||||
|
||||
function log(msg) {
|
||||
document.getElementById("log").textContent += "\n" + msg;
|
||||
}
|
||||
|
||||
function nextTest() {
|
||||
if (testList.length < 1) {
|
||||
SimpleTest.finish();
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('testing_frame').src = testList.shift();
|
||||
}
|
||||
|
||||
// listen for a messages from the mixed content test harness
|
||||
function receiveMessage(event) {
|
||||
if ("test" in event.data) {
|
||||
var summary = event.data.test + ": " + event.data.msg;
|
||||
log(event.data.status + ": " + summary);
|
||||
ok(event.data.status, summary);
|
||||
} else if ("done" in event.data) {
|
||||
nextTest();
|
||||
}
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
window.addEventListener("message", receiveMessage, false);
|
||||
nextTest();
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
69
dom/u2f/tests/test_frame_appid_facet.html
Normal file
69
dom/u2f/tests/test_frame_appid_facet.html
Normal file
@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<head>
|
||||
<script src="u2futil.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for AppID / FacetID behavior for FIDO Universal Second Factor</p>
|
||||
<script class="testbody" type="text/javascript">
|
||||
"use strict";
|
||||
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f", true);
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f.softtoken", true);
|
||||
|
||||
local_is(window.location.origin, "https://example.com", "Is loaded correctly");
|
||||
|
||||
var version = "U2F_V2";
|
||||
var challenge = new Uint8Array(16);
|
||||
|
||||
u2f.register(null, [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_is(res.errorCode, 0, "Null AppID should work.");
|
||||
});
|
||||
|
||||
u2f.register("", [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_is(res.errorCode, 0, "Empty AppID should work.");
|
||||
});
|
||||
|
||||
// Test: Correct TLD, but incorrect scheme
|
||||
u2f.register("http://example.com/appId", [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_isnot(res.errorCode, 0, "HTTP scheme is disallowed");
|
||||
});
|
||||
|
||||
// Test: Correct TLD, and also HTTPS
|
||||
u2f.register("https://example.com/appId", [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_is(res.errorCode, 0, "HTTPS origin for example.com should work");
|
||||
});
|
||||
|
||||
// Test: Dynamic origin
|
||||
u2f.register(window.location.origin + "/otherAppId", [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_is(res.errorCode, 0, "Direct window origin should work");
|
||||
});
|
||||
|
||||
// eTLD+1 check
|
||||
u2f.register("https://test1.example.com/appId", [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_is(res.errorCode, 0, "Subdomain AppID should work");
|
||||
});
|
||||
|
||||
local_finished();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
58
dom/u2f/tests/test_frame_appid_facet_insecure.html
Normal file
58
dom/u2f/tests/test_frame_appid_facet_insecure.html
Normal file
@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<head>
|
||||
<script src="u2futil.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for AppID / FacetID behavior for FIDO Universal Second Factor</p>
|
||||
<script class="testbody" type="text/javascript">
|
||||
"use strict";
|
||||
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f", true);
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f.softtoken", true);
|
||||
|
||||
local_is(window.location.origin, "http://mochi.test:8888", "Is loaded correctly");
|
||||
|
||||
var version = "U2F_V2";
|
||||
var challenge = new Uint8Array(16);
|
||||
|
||||
u2f.register(null, [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_isnot(res.errorCode, 0, "Insecure origin disallowed for null AppID");
|
||||
});
|
||||
|
||||
u2f.register("", [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_isnot(res.errorCode, 0, "Insecure origin disallowed for empty AppID");
|
||||
});
|
||||
|
||||
u2f.register("http://example.com/appId", [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_isnot(res.errorCode, 0, "Insecure origin disallowed for HTTP AppID");
|
||||
});
|
||||
|
||||
u2f.register("https://example.com/appId", [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_isnot(res.errorCode, 0, "Insecure origin disallowed for HTTPS AppID from HTTP origin");
|
||||
});
|
||||
|
||||
u2f.register(window.location.origin + "/otherAppId", [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_isnot(res.errorCode, 0, "Insecure origin disallowed for HTTP origin");
|
||||
});
|
||||
|
||||
local_finished();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
60
dom/u2f/tests/test_frame_appid_facet_remoteload.html
Normal file
60
dom/u2f/tests/test_frame_appid_facet_remoteload.html
Normal file
@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<head>
|
||||
<script src="u2futil.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for Remote AppId Load behavior for FIDO Universal Second Factor</p>
|
||||
<script class="testbody" type="text/javascript">
|
||||
"use strict";
|
||||
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f", true);
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f.softtoken", true);
|
||||
|
||||
var version = "U2F_V2";
|
||||
var challenge = new Uint8Array(16);
|
||||
|
||||
local_is(window.location.origin, "https://example.com", "Is loaded correctly");
|
||||
|
||||
// TODO: Must support remote loads of AppID manifests first.
|
||||
//
|
||||
// u2f.register("https://test1.example.com/dom/u2f/tests/facet/facetList.txt", [{
|
||||
// version: version,
|
||||
// challenge: bytesToBase64UrlSafe(challenge),
|
||||
// }], [], function(res){
|
||||
// local_is(res.errorCode, 2, "Should not permit this AppId contentType");
|
||||
// });
|
||||
|
||||
// u2f.register("https://test1.example.com/dom/u2f/tests/facet/facetListMissing", [{
|
||||
// version: version,
|
||||
// challenge: bytesToBase64UrlSafe(challenge),
|
||||
// }], [], function(res){
|
||||
// local_is(res.errorCode, 2, "Should not permit with a missing AppID list");
|
||||
// });
|
||||
|
||||
// u2f.register("https://test1.example.com/dom/u2f/tests/facet/facetList-good", [{
|
||||
// version: version,
|
||||
// challenge: bytesToBase64UrlSafe(challenge),
|
||||
// }], [], function(res){
|
||||
// local_is(res.errorCode, 0, "The AppId should permit example.com");
|
||||
// });
|
||||
|
||||
// u2f.register("https://test1.example.com/dom/u2f/tests/facet/facetList-no_overlap", [{
|
||||
// version: version,
|
||||
// challenge: bytesToBase64UrlSafe(challenge),
|
||||
// }], [], function(res){
|
||||
// local_is(res.errorCode, 2, "Should not permit with a missing AppID list");
|
||||
// });
|
||||
|
||||
// u2f.register("https://test1.example.com/dom/u2f/tests/facet/facetList-invalid_format", [{
|
||||
// version: version,
|
||||
// challenge: bytesToBase64UrlSafe(challenge),
|
||||
// }], [], function(res){
|
||||
// local_is(res.errorCode, 2, "Should not fail gracefully on invalid formatted facet lists");
|
||||
// });
|
||||
|
||||
local_finished();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
38
dom/u2f/tests/test_frame_appid_facet_subdomain.html
Normal file
38
dom/u2f/tests/test_frame_appid_facet_subdomain.html
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<head>
|
||||
<script src="u2futil.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for AppID / FacetID behavior for FIDO Universal Second Factor</p>
|
||||
<script class="testbody" type="text/javascript">
|
||||
"use strict";
|
||||
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f", true);
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f.softtoken", true);
|
||||
|
||||
var version = "U2F_V2";
|
||||
var challenge = new Uint8Array(16);
|
||||
|
||||
local_is(window.location.origin, "https://test1.example.com", "Is loaded correctly");
|
||||
|
||||
// eTLD+1 check
|
||||
u2f.register("https://example.com/appId", [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_is(res.errorCode, 0, "AppID should work from a subdomain");
|
||||
});
|
||||
|
||||
u2f.register("https://example.net/appId", [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_isnot(res.errorCode, 0, "AppID should not work from other domains");
|
||||
});
|
||||
|
||||
local_finished();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
77
dom/u2f/tests/test_frame_register.html
Normal file
77
dom/u2f/tests/test_frame_register.html
Normal file
@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<head>
|
||||
<script src="u2futil.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Test for Register behavior for FIDO Universal Second Factor</p>
|
||||
<script class="testbody" type="text/javascript">
|
||||
"use strict";
|
||||
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f", true);
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f.softtoken", true);
|
||||
|
||||
var version = "U2F_V2";
|
||||
var challenge = new Uint8Array(16);
|
||||
|
||||
local_is(window.location.origin, "https://example.com", "Is loaded correctly");
|
||||
|
||||
// eTLD+1 check
|
||||
u2f.register("https://example.com/appId", [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_is(res.errorCode, 0, "AppID should work from a subdomain");
|
||||
});
|
||||
|
||||
u2f.register("https://example.net/appId", [{
|
||||
version: version,
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_is(res.errorCode, 2, "AppID should not work from other domains");
|
||||
});
|
||||
|
||||
u2f.register("", [], [], function(res){
|
||||
local_is(res.errorCode, 2, "Empty register requests");
|
||||
});
|
||||
|
||||
local_doesThrow(function(){
|
||||
u2f.register("", null, [], null);
|
||||
}, "Non-array register requests");
|
||||
|
||||
local_doesThrow(function(){
|
||||
u2f.register("", [], null, null);
|
||||
}, "Non-array sign requests");
|
||||
|
||||
local_doesThrow(function(){
|
||||
u2f.register("", null, null, null);
|
||||
}, "Non-array for both arguments");
|
||||
|
||||
u2f.register("", [{}], [], function(res){
|
||||
local_is(res.errorCode, 2, "Empty request");
|
||||
});
|
||||
|
||||
u2f.register("https://example.net/appId", [{
|
||||
version: version,
|
||||
}], [], function(res){
|
||||
local_is(res.errorCode, 2, "Missing challenge");
|
||||
});
|
||||
|
||||
u2f.register("https://example.net/appId", [{
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_is(res.errorCode, 2, "Missing version");
|
||||
});
|
||||
|
||||
u2f.register("https://example.net/appId", [{
|
||||
version: "a_version_00",
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
}], [], function(res){
|
||||
local_is(res.errorCode, 2, "Invalid version");
|
||||
});
|
||||
|
||||
local_finished();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
38
dom/u2f/tests/test_no_token.html
Normal file
38
dom/u2f/tests/test_no_token.html
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<head>
|
||||
<title>Test for FIDO Universal Second Factor No Token</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="u2futil.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1231681">Mozilla Bug 1231681</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
</div>
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript">
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f", true);
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f.softtoken", false);
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f.usbtoken", false);
|
||||
|
||||
var challenge = new Uint8Array(16);
|
||||
window.crypto.getRandomValues(challenge);
|
||||
|
||||
var regRequest = {
|
||||
version: "U2F_V2",
|
||||
challenge: bytesToBase64UrlSafe(challenge),
|
||||
};
|
||||
|
||||
u2f.register(window.location.origin, [regRequest], [], function (regResponse) {
|
||||
isnot(regResponse.errorCode, 0, "The registration should be rejected.");
|
||||
|
||||
SimpleTest.finish();
|
||||
});
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
57
dom/u2f/tests/test_util_methods.html
Normal file
57
dom/u2f/tests/test_util_methods.html
Normal file
@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<head>
|
||||
<title>Test for Utility Methods for other FIDO Universal Second Factor tests</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/u2f/tests/u2futil.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1231681">Mozilla Bug 1231681</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
</div>
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript">
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f", true);
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f.softtoken", true);
|
||||
SpecialPowers.setBoolPref("security.webauth.u2f.usbtoken", false);
|
||||
|
||||
// Example from:
|
||||
// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html
|
||||
//
|
||||
// Run this example from the console to check that the u2futil methods work
|
||||
var pubKey = hexDecode("04d368f1b665bade3c33a20f1e429c7750d5033660c019119d29aa4ba7abc04aa7c80a46bbe11ca8cb5674d74f31f8a903f6bad105fb6ab74aefef4db8b0025e1d");
|
||||
var appId = "https://gstatic.com/securitykey/a/example.com";
|
||||
var clientData = string2buffer('{"typ":"navigator.id.getAssertion","challenge":"opsXqUifDriAAmWclinfbS0e-USY0CgyJHe_Otd7z8o","cid_pubkey":{"kty":"EC","crv":"P-256","x":"HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8","y":"XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4"},"origin":"http://example.com"}');
|
||||
var presenceAndCounter = hexDecode("0100000001");
|
||||
var signature = hexDecode("304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f");
|
||||
|
||||
// Import the key
|
||||
// Assemble the client data
|
||||
// Verify
|
||||
Promise.all([
|
||||
importPublicKey(pubKey),
|
||||
assembleSignedData(appId, presenceAndCounter, clientData)
|
||||
])
|
||||
.then(function(results) {
|
||||
var importedKey = results[0];
|
||||
var signedData = new Uint8Array(results[1]);
|
||||
return verifySignature(importedKey, signedData, signature);
|
||||
})
|
||||
.then(function(verified) {
|
||||
console.log("verified:", verified);
|
||||
ok(true, "Utility methods work")
|
||||
SimpleTest.finish();
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log("error:", err);
|
||||
ok(false, "Utility methods failed")
|
||||
SimpleTest.finish();
|
||||
});
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
144
dom/u2f/tests/u2futil.js
Normal file
144
dom/u2f/tests/u2futil.js
Normal file
@ -0,0 +1,144 @@
|
||||
function local_is(value, expected, message) {
|
||||
if (value === expected) {
|
||||
local_ok(true, message);
|
||||
} else {
|
||||
local_ok(false, message + " unexpectedly: " + value + " !== " + expected);
|
||||
}
|
||||
}
|
||||
|
||||
function local_isnot(value, expected, message) {
|
||||
if (value !== expected) {
|
||||
local_ok(true, message);
|
||||
} else {
|
||||
local_ok(false, message + " unexpectedly: " + value + " === " + expected);
|
||||
}
|
||||
}
|
||||
|
||||
function local_ok(expression, message) {
|
||||
let body = {"test": this.location.pathname, "status":expression, "msg": message}
|
||||
parent.postMessage(body, "http://mochi.test:8888");
|
||||
}
|
||||
|
||||
function local_doesThrow(fn, name) {
|
||||
var gotException = false;
|
||||
try {
|
||||
fn();
|
||||
} catch (ex) { gotException = true; }
|
||||
local_ok(gotException, name);
|
||||
};
|
||||
|
||||
function local_finished() {
|
||||
parent.postMessage({"done":true}, "http://mochi.test:8888");
|
||||
}
|
||||
|
||||
function string2buffer(str) {
|
||||
return (new Uint8Array(str.length)).map((x, i) => str.charCodeAt(i));
|
||||
}
|
||||
|
||||
function buffer2string(buf) {
|
||||
var str = "";
|
||||
buf.map(x => str += String.fromCharCode(x));
|
||||
return str;
|
||||
}
|
||||
|
||||
function bytesToBase64(u8a){
|
||||
var CHUNK_SZ = 0x8000;
|
||||
var c = [];
|
||||
for (var i = 0; i < u8a.length; i += CHUNK_SZ) {
|
||||
c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ)));
|
||||
}
|
||||
return window.btoa(c.join(""));
|
||||
}
|
||||
|
||||
function base64ToBytes(b64encoded) {
|
||||
return new Uint8Array(window.atob(b64encoded).split("").map(function(c) {
|
||||
return c.charCodeAt(0);
|
||||
}));
|
||||
}
|
||||
|
||||
function bytesToBase64UrlSafe(buf) {
|
||||
return bytesToBase64(buf)
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
}
|
||||
|
||||
function base64ToBytesUrlSafe(str) {
|
||||
if (str.length % 4 == 1) {
|
||||
throw "Improper b64 string";
|
||||
}
|
||||
|
||||
var b64 = str.replace(/\-/g, "+").replace(/\_/g, "/");
|
||||
while (b64.length % 4 != 0) {
|
||||
b64 += "=";
|
||||
}
|
||||
return base64ToBytes(b64);
|
||||
}
|
||||
|
||||
function hexEncode(buf) {
|
||||
return Array.from(buf)
|
||||
.map(x => ("0"+x.toString(16)).substr(-2))
|
||||
.join("");
|
||||
}
|
||||
|
||||
function hexDecode(str) {
|
||||
return new Uint8Array(str.match(/../g).map(x => parseInt(x, 16)));
|
||||
}
|
||||
|
||||
function importPublicKey(keyBytes) {
|
||||
if (keyBytes[0] != 0x04 || keyBytes.byteLength != 65) {
|
||||
throw "Bad public key octet string";
|
||||
}
|
||||
var jwk = {
|
||||
kty: "EC",
|
||||
crv: "P-256",
|
||||
x: bytesToBase64UrlSafe(keyBytes.slice(1, 33)),
|
||||
y: bytesToBase64UrlSafe(keyBytes.slice(33))
|
||||
};
|
||||
return crypto.subtle.importKey("jwk", jwk, {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"])
|
||||
}
|
||||
|
||||
function assembleSignedData(appId, presenceAndCounter, clientData) {
|
||||
var appIdBuf = string2buffer(appId);
|
||||
return Promise.all([
|
||||
crypto.subtle.digest("SHA-256", appIdBuf),
|
||||
crypto.subtle.digest("SHA-256", clientData)
|
||||
])
|
||||
.then(function(digests) {
|
||||
var appParam = new Uint8Array(digests[0]);
|
||||
var clientParam = new Uint8Array(digests[1]);
|
||||
|
||||
var signedData = new Uint8Array(32 + 1 + 4 + 32);
|
||||
appParam.map((x, i) => signedData[0 + i] = x);
|
||||
presenceAndCounter.map((x, i) => signedData[32 + i] = x);
|
||||
clientParam.map((x, i) => signedData[37 + i] = x);
|
||||
return signedData;
|
||||
});
|
||||
}
|
||||
|
||||
function verifySignature(key, data, derSig) {
|
||||
if (derSig.byteLength < 70) {
|
||||
console.log("bad sig: " + hexEncode(derSig))
|
||||
throw "Invalid signature length: " + derSig.byteLength;
|
||||
}
|
||||
|
||||
// Poor man's ASN.1 decode
|
||||
// R and S are always 32 bytes. If ether has a DER
|
||||
// length > 32, it's just zeros we can chop off.
|
||||
var lenR = derSig[3];
|
||||
var lenS = derSig[3 + lenR + 2];
|
||||
var padR = lenR - 32;
|
||||
var padS = lenS - 32;
|
||||
var sig = new Uint8Array(64);
|
||||
derSig.slice(4 + padR, 4 + lenR).map((x, i) => sig[i] = x);
|
||||
derSig.slice(4 + lenR + 2 + padS, 4 + lenR + 2 + lenS).map(
|
||||
(x, i) => sig[32 + i] = x
|
||||
);
|
||||
|
||||
console.log("data: " + hexEncode(data));
|
||||
console.log("der: " + hexEncode(derSig));
|
||||
console.log("raw: " + hexEncode(sig));
|
||||
|
||||
var alg = {name: "ECDSA", hash: "SHA-256"};
|
||||
return crypto.subtle.verify(alg, key, sig, data);
|
||||
}
|
95
dom/webidl/U2F.webidl
Normal file
95
dom/webidl/U2F.webidl
Normal file
@ -0,0 +1,95 @@
|
||||
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* 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/.
|
||||
*
|
||||
* The origin of this IDL file is a combination of the FIDO U2F Raw Message Formats:
|
||||
* https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html
|
||||
* and the U2F JavaScript API v1.1, not yet published. While v1.1 is not published,
|
||||
* v1.0, is located here:
|
||||
* https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-javascript-api.html
|
||||
*/
|
||||
|
||||
[NoInterfaceObject]
|
||||
interface GlobalU2F {
|
||||
[Throws, Pref="security.webauth.u2f"]
|
||||
readonly attribute U2F u2f;
|
||||
};
|
||||
|
||||
typedef unsigned short ErrorCode;
|
||||
typedef sequence<Transport> Transports;
|
||||
|
||||
enum Transport {
|
||||
"bt",
|
||||
"ble",
|
||||
"nfc",
|
||||
"usb"
|
||||
};
|
||||
|
||||
dictionary ClientData {
|
||||
DOMString typ; // Spelling is from the specification
|
||||
DOMString challenge;
|
||||
DOMString origin;
|
||||
// cid_pubkey for Token Binding is not implemented
|
||||
};
|
||||
|
||||
dictionary RegisterRequest {
|
||||
DOMString version;
|
||||
DOMString challenge;
|
||||
};
|
||||
|
||||
dictionary RegisterResponse {
|
||||
DOMString version;
|
||||
DOMString registrationData;
|
||||
DOMString clientData;
|
||||
|
||||
// From Error
|
||||
ErrorCode? errorCode;
|
||||
DOMString? errorMessage;
|
||||
};
|
||||
|
||||
dictionary RegisteredKey {
|
||||
DOMString version;
|
||||
DOMString keyHandle;
|
||||
Transports? transports;
|
||||
DOMString? appId;
|
||||
};
|
||||
|
||||
dictionary SignResponse {
|
||||
DOMString keyHandle;
|
||||
DOMString signatureData;
|
||||
DOMString clientData;
|
||||
|
||||
// From Error
|
||||
ErrorCode? errorCode;
|
||||
DOMString? errorMessage;
|
||||
};
|
||||
|
||||
callback U2FRegisterCallback = void(RegisterResponse response);
|
||||
callback U2FSignCallback = void(SignResponse response);
|
||||
|
||||
interface U2F {
|
||||
// These enumerations are defined in the FIDO U2F Javascript API under the
|
||||
// interface "ErrorCode" as constant integers, and also in the U2F.cpp file.
|
||||
// Any changes to these must occur in both locations.
|
||||
const unsigned short OK = 0;
|
||||
const unsigned short OTHER_ERROR = 1;
|
||||
const unsigned short BAD_REQUEST = 2;
|
||||
const unsigned short CONFIGURATION_UNSUPPORTED = 3;
|
||||
const unsigned short DEVICE_INELIGIBLE = 4;
|
||||
const unsigned short TIMEOUT = 5;
|
||||
|
||||
[Throws]
|
||||
void register (DOMString appId,
|
||||
sequence<RegisterRequest> registerRequests,
|
||||
sequence<RegisteredKey> registeredKeys,
|
||||
U2FRegisterCallback callback,
|
||||
optional long? opt_timeoutSeconds);
|
||||
|
||||
[Throws]
|
||||
void sign (DOMString appId,
|
||||
DOMString challenge,
|
||||
sequence<RegisteredKey> registeredKeys,
|
||||
U2FSignCallback callback,
|
||||
optional long? opt_timeoutSeconds);
|
||||
};
|
@ -246,6 +246,9 @@ partial interface Window {
|
||||
// https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html
|
||||
Window implements GlobalCrypto;
|
||||
|
||||
// https://fidoalliance.org/specifications/download/
|
||||
Window implements GlobalU2F;
|
||||
|
||||
#ifdef MOZ_WEBSPEECH
|
||||
// http://dvcs.w3.org/hg/speech-api/raw-file/tip/speechapi.html
|
||||
[NoInterfaceObject]
|
||||
|
@ -572,6 +572,7 @@ WEBIDL_FILES = [
|
||||
'TVProgram.webidl',
|
||||
'TVSource.webidl',
|
||||
'TVTuner.webidl',
|
||||
'U2F.webidl',
|
||||
'UDPMessageEvent.webidl',
|
||||
'UDPSocket.webidl',
|
||||
'UIEvent.webidl',
|
||||
|
@ -43,6 +43,10 @@ pref("security.OCSP.GET.enabled", false);
|
||||
|
||||
pref("security.pki.cert_short_lifetime_in_days", 10);
|
||||
|
||||
pref("security.webauth.u2f", false);
|
||||
pref("security.webauth.u2f.softtoken", false);
|
||||
pref("security.webauth.u2f.usbtoken", false);
|
||||
|
||||
pref("security.ssl.errorReporting.enabled", true);
|
||||
pref("security.ssl.errorReporting.url", "https://data.mozilla.com/submit/sslreports");
|
||||
pref("security.ssl.errorReporting.automatic", false);
|
||||
|
Loading…
Reference in New Issue
Block a user