Bug 1231681 - "Implement window.u2f interface". r=baku, r=dkeeler

This commit is contained in:
J.C. Jones 2016-02-09 16:43:00 +01:00
parent 40a9c94aaa
commit 70bc936cba
35 changed files with 1867 additions and 1 deletions

View File

@ -436,6 +436,7 @@ LOCAL_INCLUDES += [
'/dom/ipc',
'/dom/storage',
'/dom/svg',
'/dom/u2f',
'/dom/workers',
'/dom/xbl',
'/dom/xml',

View File

@ -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)
{

View File

@ -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

View File

@ -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)
{

View File

@ -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);

View File

@ -114,6 +114,7 @@ DIRS += [
'manifest',
'vr',
'newapps',
'u2f',
]
if CONFIG['OS_ARCH'] == 'WINNT':

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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']

View File

@ -0,0 +1,8 @@
{
"trustedFacets" : [{
"version": { "major": 1, "minor" : 0 },
"ids": [
"https://fido.example.com"
]
}]
}

View File

@ -0,0 +1 @@
Content-Type: application/fido.trusted-apps+json

View File

@ -0,0 +1,6 @@
# This file isn't actually JSON, so it shouldn't successfully parse.
{
"trustedFacets" : [{
"version": { "major": 1, "minor" : 0 },
},{}]
}

View File

@ -0,0 +1 @@
Content-Type: application/fido.trusted-apps+json

View File

@ -0,0 +1,9 @@
{
"trustedFacets" : [{
"version": { "major": 1, "minor" : 0 },
"ids": [
"https://example.net",
"http://www.example.com"
]
}]
}

View File

@ -0,0 +1 @@
Content-Type: application/fido.trusted-apps+json

View File

@ -0,0 +1,8 @@
{
"trustedFacets" : [{
"version": { "major": 1, "minor" : 0 },
"ids": [
"https://fido.example.com"
]
}]
}

View 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]

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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);
};

View File

@ -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]

View File

@ -572,6 +572,7 @@ WEBIDL_FILES = [
'TVProgram.webidl',
'TVSource.webidl',
'TVTuner.webidl',
'U2F.webidl',
'UDPMessageEvent.webidl',
'UDPSocket.webidl',
'UIEvent.webidl',

View File

@ -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);