2017-01-09 23:22:49 +03:00
|
|
|
|
/* -*- 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/WebAuthentication.h"
|
|
|
|
|
#include "mozilla/dom/WebAuthnAssertion.h"
|
|
|
|
|
#include "mozilla/dom/WebAuthnAttestation.h"
|
|
|
|
|
|
|
|
|
|
#include "mozilla/dom/Promise.h"
|
|
|
|
|
#include "nsICryptoHash.h"
|
|
|
|
|
#include "pkix/Input.h"
|
|
|
|
|
#include "pkixutil.h"
|
|
|
|
|
|
2017-01-12 01:09:03 +03:00
|
|
|
|
#define PREF_WEBAUTHN_SOFTTOKEN_ENABLED "security.webauth.webauthn_enable_softtoken"
|
|
|
|
|
#define PREF_WEBAUTHN_USBTOKEN_ENABLED "security.webauth.webauthn_enable_usbtoken"
|
2017-01-11 23:38:08 +03:00
|
|
|
|
|
2017-01-09 23:22:49 +03:00
|
|
|
|
namespace mozilla {
|
|
|
|
|
namespace dom {
|
|
|
|
|
|
2017-01-11 23:38:08 +03:00
|
|
|
|
static mozilla::LazyLogModule gWebauthLog("webauthn");
|
2017-01-09 23:22:49 +03:00
|
|
|
|
|
|
|
|
|
// Only needed for refcounted objects.
|
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WebAuthentication, mParent)
|
|
|
|
|
NS_IMPL_CYCLE_COLLECTING_ADDREF(WebAuthentication)
|
|
|
|
|
NS_IMPL_CYCLE_COLLECTING_RELEASE(WebAuthentication)
|
|
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebAuthentication)
|
|
|
|
|
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
|
|
|
|
|
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
|
|
|
|
NS_INTERFACE_MAP_END
|
|
|
|
|
|
|
|
|
|
template<class OOS>
|
|
|
|
|
static nsresult
|
|
|
|
|
GetAlgorithmName(JSContext* aCx, const OOS& aAlgorithm,
|
|
|
|
|
/* out */ nsString& aName)
|
|
|
|
|
{
|
|
|
|
|
MOZ_ASSERT(aAlgorithm.IsString()); // TODO: remove assertion when we coerce.
|
|
|
|
|
|
|
|
|
|
if (aAlgorithm.IsString()) {
|
|
|
|
|
// If string, then treat as algorithm name
|
|
|
|
|
aName.Assign(aAlgorithm.GetAsString());
|
|
|
|
|
} else {
|
|
|
|
|
// TODO: Coerce to string and extract name. See WebCryptoTask.cpp
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!NormalizeToken(aName, aName)) {
|
|
|
|
|
return NS_ERROR_DOM_SYNTAX_ERR;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NS_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static nsresult
|
|
|
|
|
HashCString(nsICryptoHash* aHashService, const nsACString& aIn,
|
|
|
|
|
/* out */ CryptoBuffer& aOut)
|
|
|
|
|
{
|
|
|
|
|
MOZ_ASSERT(aHashService);
|
|
|
|
|
|
|
|
|
|
nsresult rv = aHashService->Init(nsICryptoHash::SHA256);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rv = aHashService->Update(
|
|
|
|
|
reinterpret_cast<const uint8_t*>(aIn.BeginReading()),aIn.Length());
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nsAutoCString fullHash;
|
|
|
|
|
// Passing false below means we will get a binary result rather than a
|
|
|
|
|
// base64-encoded string.
|
|
|
|
|
rv = aHashService->Finish(false, fullHash);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
aOut.Assign(fullHash);
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static nsresult
|
|
|
|
|
AssembleClientData(const nsAString& aOrigin, const CryptoBuffer& aChallenge,
|
|
|
|
|
/* out */ nsACString& aJsonOut)
|
|
|
|
|
{
|
|
|
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
|
|
|
|
|
|
nsString challengeBase64;
|
|
|
|
|
nsresult rv = aChallenge.ToJwkBase64(challengeBase64);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
|
|
|
return NS_ERROR_FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WebAuthnClientData clientDataObject;
|
|
|
|
|
clientDataObject.mOrigin.Assign(aOrigin);
|
|
|
|
|
clientDataObject.mHashAlg.SetAsString().Assign(NS_LITERAL_STRING("S256"));
|
|
|
|
|
clientDataObject.mChallenge.Assign(challengeBase64);
|
|
|
|
|
|
|
|
|
|
nsAutoString temp;
|
|
|
|
|
if (NS_WARN_IF(!clientDataObject.ToJSON(temp))) {
|
|
|
|
|
return NS_ERROR_FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
aJsonOut.Assign(NS_ConvertUTF16toUTF8(temp));
|
|
|
|
|
return NS_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static nsresult
|
|
|
|
|
ScopedCredentialGetData(const ScopedCredentialDescriptor& aSCD,
|
|
|
|
|
/* out */ uint8_t** aBuf, /* out */ uint32_t* aBufLen)
|
|
|
|
|
{
|
|
|
|
|
MOZ_ASSERT(aBuf);
|
|
|
|
|
MOZ_ASSERT(aBufLen);
|
|
|
|
|
|
|
|
|
|
if (aSCD.mId.IsArrayBufferView()) {
|
2016-12-16 20:44:56 +03:00
|
|
|
|
const ArrayBufferView& view = aSCD.mId.GetAsArrayBufferView();
|
|
|
|
|
view.ComputeLengthAndData();
|
|
|
|
|
*aBuf = view.Data();
|
|
|
|
|
*aBufLen = view.Length();
|
2017-01-09 23:22:49 +03:00
|
|
|
|
} else if (aSCD.mId.IsArrayBuffer()) {
|
2016-12-16 20:44:56 +03:00
|
|
|
|
const ArrayBuffer& buffer = aSCD.mId.GetAsArrayBuffer();
|
|
|
|
|
buffer.ComputeLengthAndData();
|
|
|
|
|
*aBuf = buffer.Data();
|
|
|
|
|
*aBufLen = buffer.Length();
|
2017-01-09 23:22:49 +03:00
|
|
|
|
} else {
|
|
|
|
|
MOZ_ASSERT(false);
|
|
|
|
|
return NS_ERROR_FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NS_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static nsresult
|
|
|
|
|
ReadToCryptoBuffer(pkix::Reader& aSrc, /* out */ CryptoBuffer& aDest,
|
|
|
|
|
uint32_t aLen)
|
|
|
|
|
{
|
|
|
|
|
if (aSrc.EnsureLength(aLen) != pkix::Success) {
|
|
|
|
|
return NS_ERROR_DOM_UNKNOWN_ERR;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
aDest.ClearAndRetainStorage();
|
|
|
|
|
|
|
|
|
|
for (uint32_t offset = 0; offset < aLen; ++offset) {
|
|
|
|
|
uint8_t b;
|
|
|
|
|
if (aSrc.Read(b) != pkix::Success) {
|
|
|
|
|
return NS_ERROR_DOM_UNKNOWN_ERR;
|
|
|
|
|
}
|
|
|
|
|
if (!aDest.AppendElement(b, mozilla::fallible)) {
|
|
|
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NS_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static nsresult
|
|
|
|
|
U2FAssembleAuthenticatorData(/* out */ CryptoBuffer& aAuthenticatorData,
|
|
|
|
|
const CryptoBuffer& aRpIdHash,
|
|
|
|
|
const CryptoBuffer& aSignatureData)
|
|
|
|
|
{
|
|
|
|
|
// The AuthenticatorData for U2F devices is the concatenation of the
|
|
|
|
|
// RP ID with the output of the U2F Sign operation.
|
|
|
|
|
if (aRpIdHash.Length() != 32) {
|
|
|
|
|
return NS_ERROR_INVALID_ARG;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!aAuthenticatorData.AppendElements(aRpIdHash, mozilla::fallible)) {
|
|
|
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!aAuthenticatorData.AppendElements(aSignatureData, mozilla::fallible)) {
|
|
|
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NS_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static nsresult
|
|
|
|
|
U2FDecomposeRegistrationResponse(const CryptoBuffer& aResponse,
|
|
|
|
|
/* out */ CryptoBuffer& aPubKeyBuf,
|
|
|
|
|
/* out */ CryptoBuffer& aKeyHandleBuf,
|
|
|
|
|
/* out */ CryptoBuffer& aAttestationCertBuf,
|
|
|
|
|
/* out */ CryptoBuffer& aSignatureBuf)
|
|
|
|
|
{
|
|
|
|
|
// U2F v1.1 Format via
|
|
|
|
|
// http://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html
|
|
|
|
|
//
|
|
|
|
|
// Bytes Value
|
|
|
|
|
// 1 0x05
|
|
|
|
|
// 65 public key
|
|
|
|
|
// 1 key handle length
|
|
|
|
|
// * key handle
|
|
|
|
|
// ASN.1 attestation certificate
|
|
|
|
|
// * attestation signature
|
|
|
|
|
|
|
|
|
|
pkix::Input u2fResponse;
|
|
|
|
|
u2fResponse.Init(aResponse.Elements(), aResponse.Length());
|
|
|
|
|
|
|
|
|
|
pkix::Reader input(u2fResponse);
|
|
|
|
|
|
|
|
|
|
uint8_t b;
|
|
|
|
|
if (input.Read(b) != pkix::Success) {
|
|
|
|
|
return NS_ERROR_DOM_UNKNOWN_ERR;
|
|
|
|
|
}
|
|
|
|
|
if (b != 0x05) {
|
|
|
|
|
return NS_ERROR_DOM_UNKNOWN_ERR;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nsresult rv = ReadToCryptoBuffer(input, aPubKeyBuf, 65);
|
|
|
|
|
if (NS_FAILED(rv)) {
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint8_t handleLen;
|
|
|
|
|
if (input.Read(handleLen) != pkix::Success) {
|
|
|
|
|
return NS_ERROR_DOM_UNKNOWN_ERR;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rv = ReadToCryptoBuffer(input, aKeyHandleBuf, handleLen);
|
|
|
|
|
if (NS_FAILED(rv)) {
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We have to parse the ASN.1 SEQUENCE on the outside to determine the cert's
|
|
|
|
|
// length.
|
|
|
|
|
pkix::Input cert;
|
|
|
|
|
if (pkix::der::ExpectTagAndGetValue(input, pkix::der::SEQUENCE, cert)
|
|
|
|
|
!= pkix::Success) {
|
|
|
|
|
return NS_ERROR_DOM_UNKNOWN_ERR;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pkix::Reader certInput(cert);
|
|
|
|
|
rv = ReadToCryptoBuffer(certInput, aAttestationCertBuf, cert.GetLength());
|
|
|
|
|
if (NS_FAILED(rv)) {
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The remainder of u2fResponse is the signature
|
|
|
|
|
pkix::Input u2fSig;
|
|
|
|
|
input.SkipToEnd(u2fSig);
|
|
|
|
|
pkix::Reader sigInput(u2fSig);
|
|
|
|
|
rv = ReadToCryptoBuffer(sigInput, aSignatureBuf, u2fSig.GetLength());
|
|
|
|
|
if (NS_FAILED(rv)) {
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NS_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WebAuthentication::WebAuthentication(nsPIDOMWindowInner* aParent)
|
|
|
|
|
: mInitialized(false)
|
|
|
|
|
{
|
|
|
|
|
mParent = do_QueryInterface(aParent);
|
|
|
|
|
MOZ_ASSERT(mParent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WebAuthentication::~WebAuthentication()
|
|
|
|
|
{}
|
|
|
|
|
|
|
|
|
|
nsresult
|
|
|
|
|
WebAuthentication::InitLazily()
|
|
|
|
|
{
|
|
|
|
|
if (mInitialized) {
|
|
|
|
|
return NS_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MOZ_ASSERT(mParent);
|
|
|
|
|
if (!mParent) {
|
|
|
|
|
return NS_ERROR_FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nsCOMPtr<nsIDocument> doc = mParent->GetDoc();
|
|
|
|
|
MOZ_ASSERT(doc);
|
|
|
|
|
|
|
|
|
|
nsIPrincipal* principal = doc->NodePrincipal();
|
|
|
|
|
nsresult rv = nsContentUtils::GetUTFOrigin(principal, mOrigin);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
|
|
|
return NS_ERROR_FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (NS_WARN_IF(mOrigin.IsEmpty())) {
|
|
|
|
|
return NS_ERROR_FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This only functions in e10s mode
|
|
|
|
|
// TODO: Remove in Bug 1323339
|
|
|
|
|
if (XRE_IsParentProcess()) {
|
|
|
|
|
MOZ_LOG(gWebauthLog, LogLevel::Debug,
|
|
|
|
|
("Is non-e10s Process, WebAuthn not available"));
|
|
|
|
|
return NS_ERROR_FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-12 01:09:03 +03:00
|
|
|
|
if (Preferences::GetBool(PREF_WEBAUTHN_SOFTTOKEN_ENABLED)) {
|
2017-01-09 23:22:49 +03:00
|
|
|
|
if (!mAuthenticators.AppendElement(new NSSU2FTokenRemote(),
|
|
|
|
|
mozilla::fallible)) {
|
|
|
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mInitialized = true;
|
|
|
|
|
return NS_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
JSObject*
|
|
|
|
|
WebAuthentication::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
|
|
|
|
|
{
|
|
|
|
|
return WebAuthenticationBinding::Wrap(aCx, this, aGivenProto);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NOTE: This method represents a theoretical way to use a U2F-compliant token
|
|
|
|
|
// to produce the result of the WebAuthn MakeCredential method. The exact
|
|
|
|
|
// mapping of U2F data fields to WebAuthn data fields is still a matter of
|
|
|
|
|
// ongoing discussion, and this should not be taken as anything but a point-in-
|
|
|
|
|
// time possibility.
|
|
|
|
|
void
|
|
|
|
|
WebAuthentication::U2FAuthMakeCredential(
|
|
|
|
|
const RefPtr<CredentialRequest>& aRequest,
|
|
|
|
|
const Authenticator& aToken, CryptoBuffer& aRpIdHash,
|
|
|
|
|
const nsACString& aClientData, CryptoBuffer& aClientDataHash,
|
|
|
|
|
const Account& aAccount,
|
|
|
|
|
const nsTArray<ScopedCredentialParameters>& aNormalizedParams,
|
|
|
|
|
const Optional<Sequence<ScopedCredentialDescriptor>>& aExcludeList,
|
|
|
|
|
const WebAuthnExtensions& aExtensions)
|
|
|
|
|
{
|
|
|
|
|
MOZ_LOG(gWebauthLog, LogLevel::Debug, ("U2FAuthMakeCredential"));
|
|
|
|
|
aRequest->AddActiveToken(__func__);
|
|
|
|
|
|
|
|
|
|
// 5.1.1 When this operation is invoked, the authenticator must perform the
|
|
|
|
|
// following procedure:
|
|
|
|
|
|
|
|
|
|
// 5.1.1.a Check if all the supplied parameters are syntactically well-
|
|
|
|
|
// formed and of the correct length. If not, return an error code equivalent
|
|
|
|
|
// to UnknownError and terminate the operation.
|
|
|
|
|
|
|
|
|
|
if ((aRpIdHash.Length() != SHA256_LENGTH) ||
|
|
|
|
|
(aClientDataHash.Length() != SHA256_LENGTH)) {
|
|
|
|
|
aRequest->SetFailure(NS_ERROR_DOM_UNKNOWN_ERR);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5.1.1.b Check if at least one of the specified combinations of
|
|
|
|
|
// ScopedCredentialType and cryptographic parameters is supported. If not,
|
|
|
|
|
// return an error code equivalent to NotSupportedError and terminate the
|
|
|
|
|
// operation.
|
|
|
|
|
|
|
|
|
|
bool isValidCombination = false;
|
|
|
|
|
|
|
|
|
|
for (size_t a = 0; a < aNormalizedParams.Length(); ++a) {
|
|
|
|
|
if (aNormalizedParams[a].mType == ScopedCredentialType::ScopedCred &&
|
|
|
|
|
aNormalizedParams[a].mAlgorithm.IsString() &&
|
|
|
|
|
aNormalizedParams[a].mAlgorithm.GetAsString().EqualsLiteral(
|
|
|
|
|
WEBCRYPTO_NAMED_CURVE_P256)) {
|
|
|
|
|
isValidCombination = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!isValidCombination) {
|
|
|
|
|
aRequest->SetFailure(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5.1.1.c Check if a credential matching any of the supplied
|
|
|
|
|
// ScopedCredential identifiers is present on this authenticator. If so,
|
|
|
|
|
// return an error code equivalent to NotAllowedError and terminate the
|
|
|
|
|
// operation.
|
|
|
|
|
|
|
|
|
|
if (aExcludeList.WasPassed()) {
|
|
|
|
|
const Sequence<ScopedCredentialDescriptor>& list = aExcludeList.Value();
|
|
|
|
|
|
|
|
|
|
for (const ScopedCredentialDescriptor& scd : list) {
|
|
|
|
|
bool isRegistered = false;
|
|
|
|
|
|
|
|
|
|
uint8_t *data;
|
|
|
|
|
uint32_t len;
|
|
|
|
|
|
|
|
|
|
// data is owned by the Descriptor, do don't free it here.
|
|
|
|
|
if (NS_FAILED(ScopedCredentialGetData(scd, &data, &len))) {
|
|
|
|
|
aRequest->SetFailure(NS_ERROR_DOM_UNKNOWN_ERR);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nsresult rv = aToken->IsRegistered(data, len, &isRegistered);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
|
|
|
aRequest->SetFailure(rv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isRegistered) {
|
|
|
|
|
aRequest->SetFailure(NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5.1.1.d Prompt the user for consent to create a new credential. The
|
|
|
|
|
// prompt for obtaining this consent is shown by the authenticator if it has
|
|
|
|
|
// its own output capability, or by the user agent otherwise. If the user
|
|
|
|
|
// denies consent, return an error code equivalent to NotAllowedError and
|
|
|
|
|
// terminate the operation.
|
|
|
|
|
|
|
|
|
|
// 5.1.1.d Once user consent has been obtained, generate a new credential
|
|
|
|
|
// object
|
|
|
|
|
|
|
|
|
|
// 5.1.1.e If any error occurred while creating the new credential object,
|
|
|
|
|
// return an error code equivalent to UnknownError and terminate the
|
|
|
|
|
// operation.
|
|
|
|
|
|
|
|
|
|
// 5.1.1.f Process all the supported extensions requested by the client, and
|
|
|
|
|
// generate an attestation statement. If no authority key is available to
|
|
|
|
|
// sign such an attestation statement, then the authenticator performs self
|
|
|
|
|
// attestation of the credential with its own private key. For more details
|
|
|
|
|
// on attestation, see §5.3 Credential Attestation Statements.
|
|
|
|
|
|
|
|
|
|
// No extensions are supported
|
|
|
|
|
|
|
|
|
|
// 4.1.1.11 While issuedRequests is not empty, perform the following actions
|
|
|
|
|
// depending upon the adjustedTimeout timer and responses from the
|
|
|
|
|
// authenticators:
|
|
|
|
|
|
|
|
|
|
// 4.1.1.11.a If the adjustedTimeout timer expires, then for each entry in
|
|
|
|
|
// issuedRequests invoke the authenticatorCancel operation on that
|
|
|
|
|
// authenticator and remove its entry from the list.
|
|
|
|
|
|
|
|
|
|
uint8_t* buffer;
|
|
|
|
|
uint32_t bufferlen;
|
|
|
|
|
|
|
|
|
|
nsresult rv = aToken->Register(aRpIdHash.Elements(), aRpIdHash.Length(),
|
|
|
|
|
aClientDataHash.Elements(),
|
|
|
|
|
aClientDataHash.Length(), &buffer, &bufferlen);
|
|
|
|
|
|
|
|
|
|
// 4.1.1.11.b If any authenticator returns a status indicating that the user
|
|
|
|
|
// cancelled the operation, delete that authenticator’s entry from
|
|
|
|
|
// issuedRequests. For each remaining entry in issuedRequests invoke the
|
|
|
|
|
// authenticatorCancel operation on that authenticator and remove its entry
|
|
|
|
|
// from the list.
|
|
|
|
|
|
|
|
|
|
// 4.1.1.11.c If any authenticator returns an error status, delete the
|
|
|
|
|
// corresponding entry from issuedRequests.
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
|
|
|
aRequest->SetFailure(NS_ERROR_DOM_UNKNOWN_ERR);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MOZ_ASSERT(buffer);
|
|
|
|
|
CryptoBuffer regData;
|
|
|
|
|
if (NS_WARN_IF(!regData.Assign(buffer, bufferlen))) {
|
|
|
|
|
free(buffer);
|
|
|
|
|
aRequest->SetFailure(NS_ERROR_OUT_OF_MEMORY);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
free(buffer);
|
|
|
|
|
|
|
|
|
|
// Decompose the U2F registration packet
|
|
|
|
|
CryptoBuffer pubKeyBuf;
|
|
|
|
|
CryptoBuffer keyHandleBuf;
|
|
|
|
|
CryptoBuffer attestationCertBuf;
|
|
|
|
|
CryptoBuffer signatureBuf;
|
|
|
|
|
|
|
|
|
|
rv = U2FDecomposeRegistrationResponse(regData, pubKeyBuf, keyHandleBuf,
|
|
|
|
|
attestationCertBuf, signatureBuf);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
|
|
|
aRequest->SetFailure(rv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sign the aClientDataHash explicitly to get the format needed for
|
|
|
|
|
// the AuthenticatorData parameter of WebAuthnAttestation. This might
|
|
|
|
|
// be temporary while the spec settles down how to incorporate U2F.
|
|
|
|
|
rv = aToken->Sign(aRpIdHash.Elements(), aRpIdHash.Length(),
|
|
|
|
|
aClientDataHash.Elements(), aClientDataHash.Length(),
|
|
|
|
|
keyHandleBuf.Elements(), keyHandleBuf.Length(), &buffer,
|
|
|
|
|
&bufferlen);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
|
|
|
aRequest->SetFailure(rv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MOZ_ASSERT(buffer);
|
|
|
|
|
CryptoBuffer signatureData;
|
|
|
|
|
if (NS_WARN_IF(!signatureData.Assign(buffer, bufferlen))) {
|
|
|
|
|
free(buffer);
|
|
|
|
|
aRequest->SetFailure(NS_ERROR_OUT_OF_MEMORY);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
free(buffer);
|
|
|
|
|
|
|
|
|
|
CryptoBuffer clientDataBuf;
|
|
|
|
|
if (!clientDataBuf.Assign(aClientData)) {
|
|
|
|
|
aRequest->SetFailure(NS_ERROR_OUT_OF_MEMORY);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CryptoBuffer authenticatorDataBuf;
|
|
|
|
|
rv = U2FAssembleAuthenticatorData(authenticatorDataBuf, aRpIdHash,
|
|
|
|
|
signatureData);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
|
|
|
aRequest->SetFailure(rv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.1.11.d If any authenticator indicates success:
|
|
|
|
|
|
|
|
|
|
// 4.1.1.11.d.1 Remove this authenticator’s entry from issuedRequests.
|
|
|
|
|
|
|
|
|
|
// 4.1.1.11.d.2 Create a new ScopedCredentialInfo object named value and
|
|
|
|
|
// populate its fields with the values returned from the authenticator as well
|
|
|
|
|
// as the clientDataJSON computed earlier.
|
|
|
|
|
|
|
|
|
|
RefPtr<ScopedCredential> credential = new ScopedCredential(this);
|
|
|
|
|
credential->SetType(ScopedCredentialType::ScopedCred);
|
|
|
|
|
credential->SetId(keyHandleBuf);
|
|
|
|
|
|
|
|
|
|
RefPtr<WebAuthnAttestation> attestation = new WebAuthnAttestation(this);
|
|
|
|
|
attestation->SetFormat(NS_LITERAL_STRING("u2f"));
|
|
|
|
|
attestation->SetClientData(clientDataBuf);
|
|
|
|
|
attestation->SetAuthenticatorData(authenticatorDataBuf);
|
|
|
|
|
attestation->SetAttestation(regData);
|
|
|
|
|
|
|
|
|
|
CredentialPtr info = new ScopedCredentialInfo(this);
|
|
|
|
|
info->SetCredential(credential);
|
|
|
|
|
info->SetAttestation(attestation);
|
|
|
|
|
|
|
|
|
|
// 4.1.1.11.d.3 For each remaining entry in issuedRequests invoke the
|
|
|
|
|
// authenticatorCancel operation on that authenticator and remove its entry
|
|
|
|
|
// from the list.
|
|
|
|
|
|
|
|
|
|
// 4.1.1.11.d.4 Resolve promise with value and terminate this algorithm.
|
|
|
|
|
aRequest->SetSuccess(info);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NOTE: This method represents a theoretical way to use a U2F-compliant token
|
|
|
|
|
// to produce the result of the WebAuthn GetAssertion method. The exact mapping
|
|
|
|
|
// of U2F data fields to WebAuthn data fields is still a matter of ongoing
|
|
|
|
|
// discussion, and this should not be taken as anything but a point-in- time
|
|
|
|
|
// possibility.
|
|
|
|
|
void
|
|
|
|
|
WebAuthentication::U2FAuthGetAssertion(const RefPtr<AssertionRequest>& aRequest,
|
|
|
|
|
const Authenticator& aToken, CryptoBuffer& aRpIdHash,
|
|
|
|
|
const nsACString& aClientData, CryptoBuffer& aClientDataHash,
|
|
|
|
|
nsTArray<CryptoBuffer>& aAllowList,
|
|
|
|
|
const WebAuthnExtensions& aExtensions)
|
|
|
|
|
{
|
|
|
|
|
MOZ_LOG(gWebauthLog, LogLevel::Debug, ("U2FAuthGetAssertion"));
|
|
|
|
|
|
|
|
|
|
// 4.1.2.7.e Add an entry to issuedRequests, corresponding to this request.
|
|
|
|
|
aRequest->AddActiveToken(__func__);
|
|
|
|
|
|
|
|
|
|
// 4.1.2.8 While issuedRequests is not empty, perform the following actions
|
|
|
|
|
// depending upon the adjustedTimeout timer and responses from the
|
|
|
|
|
// authenticators:
|
|
|
|
|
|
|
|
|
|
// 4.1.2.8.a If the timer for adjustedTimeout expires, then for each entry
|
|
|
|
|
// in issuedRequests invoke the authenticatorCancel operation on that
|
|
|
|
|
// authenticator and remove its entry from the list.
|
|
|
|
|
|
|
|
|
|
for (CryptoBuffer& allowedCredential : aAllowList) {
|
|
|
|
|
bool isRegistered = false;
|
|
|
|
|
nsresult rv = aToken->IsRegistered(allowedCredential.Elements(),
|
|
|
|
|
allowedCredential.Length(),
|
|
|
|
|
&isRegistered);
|
|
|
|
|
|
|
|
|
|
// 4.1.2.8.b If any authenticator returns a status indicating that the user
|
|
|
|
|
// cancelled the operation, delete that authenticator’s entry from
|
|
|
|
|
// issuedRequests. For each remaining entry in issuedRequests invoke the
|
|
|
|
|
// authenticatorCancel operation on that authenticator, and remove its entry
|
|
|
|
|
// from the list.
|
|
|
|
|
|
|
|
|
|
// 4.1.2.8.c If any authenticator returns an error status, delete the
|
|
|
|
|
// corresponding entry from issuedRequests.
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
|
|
|
aRequest->SetFailure(rv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isRegistered) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sign
|
|
|
|
|
uint8_t* buffer;
|
|
|
|
|
uint32_t bufferlen;
|
|
|
|
|
rv = aToken->Sign(aRpIdHash.Elements(), aRpIdHash.Length(),
|
|
|
|
|
aClientDataHash.Elements(), aClientDataHash.Length(),
|
|
|
|
|
allowedCredential.Elements(), allowedCredential.Length(),
|
|
|
|
|
&buffer, &bufferlen);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
|
|
|
aRequest->SetFailure(rv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MOZ_ASSERT(buffer);
|
|
|
|
|
CryptoBuffer signatureData;
|
|
|
|
|
if (NS_WARN_IF(!signatureData.Assign(buffer, bufferlen))) {
|
|
|
|
|
free(buffer);
|
|
|
|
|
aRequest->SetFailure(NS_ERROR_OUT_OF_MEMORY);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
free(buffer);
|
|
|
|
|
|
|
|
|
|
// 4.1.2.8.d If any authenticator returns success:
|
|
|
|
|
|
|
|
|
|
// 4.1.2.8.d.1 Remove this authenticator’s entry from issuedRequests.
|
|
|
|
|
|
|
|
|
|
// 4.1.2.8.d.2 Create a new WebAuthnAssertion object named value and
|
|
|
|
|
// populate its fields with the values returned from the authenticator as
|
|
|
|
|
// well as the clientDataJSON computed earlier.
|
|
|
|
|
|
|
|
|
|
CryptoBuffer clientDataBuf;
|
|
|
|
|
if (!clientDataBuf.Assign(aClientData)) {
|
|
|
|
|
aRequest->SetFailure(NS_ERROR_OUT_OF_MEMORY);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CryptoBuffer authenticatorDataBuf;
|
|
|
|
|
rv = U2FAssembleAuthenticatorData(authenticatorDataBuf, aRpIdHash,
|
|
|
|
|
signatureData);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
|
|
|
aRequest->SetFailure(rv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RefPtr<ScopedCredential> credential = new ScopedCredential(this);
|
|
|
|
|
credential->SetType(ScopedCredentialType::ScopedCred);
|
|
|
|
|
credential->SetId(allowedCredential);
|
|
|
|
|
|
|
|
|
|
AssertionPtr assertion = new WebAuthnAssertion(this);
|
|
|
|
|
assertion->SetCredential(credential);
|
|
|
|
|
assertion->SetClientData(clientDataBuf);
|
|
|
|
|
assertion->SetAuthenticatorData(authenticatorDataBuf);
|
|
|
|
|
assertion->SetSignature(signatureData);
|
|
|
|
|
|
|
|
|
|
// 4.1.2.8.d.3 For each remaining entry in issuedRequests invoke the
|
|
|
|
|
// authenticatorCancel operation on that authenticator and remove its entry
|
|
|
|
|
// from the list.
|
|
|
|
|
|
|
|
|
|
// 4.1.2.8.d.4 Resolve promise with value and terminate this algorithm.
|
|
|
|
|
aRequest->SetSuccess(assertion);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.2.9 Reject promise with a DOMException whose name is "NotAllowedError",
|
|
|
|
|
// and terminate this algorithm.
|
|
|
|
|
aRequest->SetFailure(NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-09 23:55:59 +03:00
|
|
|
|
nsresult
|
|
|
|
|
WebAuthentication::RelaxSameOrigin(const nsAString& aInputRpId,
|
|
|
|
|
/* out */ nsACString& aRelaxedRpId)
|
|
|
|
|
{
|
|
|
|
|
MOZ_ASSERT(mParent);
|
|
|
|
|
nsCOMPtr<nsIDocument> document = mParent->GetDoc();
|
|
|
|
|
if (!document || !document->IsHTMLDocument()) {
|
|
|
|
|
return NS_ERROR_FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Bug 1329764: Invoke the Relax Algorithm, once properly defined
|
|
|
|
|
aRelaxedRpId.Assign(NS_ConvertUTF16toUTF8(aInputRpId));
|
|
|
|
|
return NS_OK;
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-09 23:22:49 +03:00
|
|
|
|
already_AddRefed<Promise>
|
|
|
|
|
WebAuthentication::MakeCredential(JSContext* aCx, const Account& aAccount,
|
|
|
|
|
const Sequence<ScopedCredentialParameters>& aCryptoParameters,
|
|
|
|
|
const ArrayBufferViewOrArrayBuffer& aChallenge,
|
|
|
|
|
const ScopedCredentialOptions& aOptions)
|
|
|
|
|
{
|
|
|
|
|
MOZ_ASSERT(mParent);
|
|
|
|
|
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject());
|
|
|
|
|
if (!global) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ErrorResult rv;
|
|
|
|
|
RefPtr<Promise> promise = Promise::Create(global, rv);
|
|
|
|
|
|
|
|
|
|
nsresult initRv = InitLazily();
|
|
|
|
|
if (NS_FAILED(initRv)) {
|
|
|
|
|
promise->MaybeReject(initRv);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.1.1 If timeoutSeconds was specified, check if its value lies within a
|
|
|
|
|
// reasonable range as defined by the platform and if not, correct it to the
|
|
|
|
|
// closest value lying within that range.
|
|
|
|
|
|
|
|
|
|
double adjustedTimeout = 30.0;
|
|
|
|
|
if (aOptions.mTimeoutSeconds.WasPassed()) {
|
|
|
|
|
adjustedTimeout = aOptions.mTimeoutSeconds.Value();
|
|
|
|
|
adjustedTimeout = std::max(15.0, adjustedTimeout);
|
|
|
|
|
adjustedTimeout = std::min(120.0, adjustedTimeout);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.1.2 Let promise be a new Promise. Return promise and start a timer for
|
|
|
|
|
// adjustedTimeout seconds.
|
|
|
|
|
|
|
|
|
|
RefPtr<CredentialRequest> requestMonitor = new CredentialRequest();
|
|
|
|
|
requestMonitor->SetDeadline(TimeDuration::FromSeconds(adjustedTimeout));
|
|
|
|
|
|
|
|
|
|
if (mOrigin.EqualsLiteral("null")) {
|
|
|
|
|
// 4.1.1.3 If callerOrigin is an opaque origin, reject promise with a
|
|
|
|
|
// DOMException whose name is "NotAllowedError", and terminate this
|
|
|
|
|
// algorithm
|
|
|
|
|
MOZ_LOG(gWebauthLog, LogLevel::Debug, ("Rejecting due to opaque origin"));
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nsCString rpId;
|
|
|
|
|
if (!aOptions.mRpId.WasPassed()) {
|
|
|
|
|
// 4.1.1.3.a If rpId is not specified, then set rpId to callerOrigin, and
|
|
|
|
|
// rpIdHash to the SHA-256 hash of rpId.
|
|
|
|
|
rpId.Assign(NS_ConvertUTF16toUTF8(mOrigin));
|
|
|
|
|
} else {
|
|
|
|
|
// 4.1.1.3.b If rpId is specified, then invoke the procedure used for
|
|
|
|
|
// relaxing the same-origin restriction by setting the document.domain
|
|
|
|
|
// attribute, using rpId as the given value but without changing the current
|
|
|
|
|
// document’s domain. If no errors are thrown, set rpId to the value of host
|
|
|
|
|
// as computed by this procedure, and rpIdHash to the SHA-256 hash of rpId.
|
|
|
|
|
// Otherwise, reject promise with a DOMException whose name is
|
|
|
|
|
// "SecurityError", and terminate this algorithm.
|
|
|
|
|
|
2017-01-09 23:55:59 +03:00
|
|
|
|
if (NS_FAILED(RelaxSameOrigin(aOptions.mRpId.Value(), rpId))) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
2017-01-09 23:22:49 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CryptoBuffer rpIdHash;
|
|
|
|
|
if (!rpIdHash.SetLength(SHA256_LENGTH, fallible)) {
|
2016-12-16 20:44:56 +03:00
|
|
|
|
promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
|
2017-01-09 23:22:49 +03:00
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nsresult srv;
|
|
|
|
|
nsCOMPtr<nsICryptoHash> hashService =
|
|
|
|
|
do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(srv))) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
srv = HashCString(hashService, rpId, rpIdHash);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(srv))) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.1.4 Process each element of cryptoParameters using the following steps,
|
|
|
|
|
// to produce a new sequence normalizedParameters.
|
|
|
|
|
nsTArray<ScopedCredentialParameters> normalizedParams;
|
|
|
|
|
for (size_t a = 0; a < aCryptoParameters.Length(); ++a) {
|
|
|
|
|
// 4.1.1.4.a Let current be the currently selected element of
|
|
|
|
|
// cryptoParameters.
|
|
|
|
|
|
|
|
|
|
// 4.1.1.4.b If current.type does not contain a ScopedCredentialType
|
|
|
|
|
// supported by this implementation, then stop processing current and move
|
|
|
|
|
// on to the next element in cryptoParameters.
|
|
|
|
|
if (aCryptoParameters[a].mType != ScopedCredentialType::ScopedCred) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.1.4.c Let normalizedAlgorithm be the result of normalizing an
|
|
|
|
|
// algorithm using the procedure defined in [WebCryptoAPI], with alg set to
|
|
|
|
|
// current.algorithm and op set to 'generateKey'. If an error occurs during
|
|
|
|
|
// this procedure, then stop processing current and move on to the next
|
|
|
|
|
// element in cryptoParameters.
|
|
|
|
|
|
|
|
|
|
nsString algName;
|
|
|
|
|
if (NS_FAILED(GetAlgorithmName(aCx, aCryptoParameters[a].mAlgorithm,
|
|
|
|
|
algName))) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.1.4.d Add a new object of type ScopedCredentialParameters to
|
|
|
|
|
// normalizedParameters, with type set to current.type and algorithm set to
|
|
|
|
|
// normalizedAlgorithm.
|
|
|
|
|
ScopedCredentialParameters normalizedObj;
|
|
|
|
|
normalizedObj.mType = aCryptoParameters[a].mType;
|
|
|
|
|
normalizedObj.mAlgorithm.SetAsString().Assign(algName);
|
|
|
|
|
|
2016-12-16 20:44:56 +03:00
|
|
|
|
if (!normalizedParams.AppendElement(normalizedObj, mozilla::fallible)){
|
|
|
|
|
promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
2017-01-09 23:22:49 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.1.5 If normalizedAlgorithm is empty and cryptoParameters was not empty,
|
|
|
|
|
// cancel the timer started in step 2, reject promise with a DOMException
|
|
|
|
|
// whose name is "NotSupportedError", and terminate this algorithm.
|
2016-12-16 20:44:56 +03:00
|
|
|
|
if (normalizedParams.IsEmpty() && !aCryptoParameters.IsEmpty()) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
2017-01-09 23:22:49 +03:00
|
|
|
|
|
|
|
|
|
// 4.1.1.6 If excludeList is undefined, set it to the empty list.
|
|
|
|
|
|
|
|
|
|
// 4.1.1.7 If extensions was specified, process any extensions supported by
|
|
|
|
|
// this client platform, to produce the extension data that needs to be sent
|
|
|
|
|
// to the authenticator. If an error is encountered while processing an
|
|
|
|
|
// extension, skip that extension and do not produce any extension data for
|
|
|
|
|
// it. Call the result of this processing clientExtensions.
|
|
|
|
|
|
2016-12-16 20:44:56 +03:00
|
|
|
|
// Currently no extensions are supported
|
|
|
|
|
|
2017-01-09 23:22:49 +03:00
|
|
|
|
// 4.1.1.8 Use attestationChallenge, callerOrigin and rpId, along with the
|
|
|
|
|
// token binding key associated with callerOrigin (if any), to create a
|
|
|
|
|
// ClientData structure representing this request. Choose a hash algorithm for
|
|
|
|
|
// hashAlg and compute the clientDataJSON and clientDataHash.
|
|
|
|
|
|
|
|
|
|
CryptoBuffer challenge;
|
|
|
|
|
if (!challenge.Assign(aChallenge)) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nsAutoCString clientDataJSON;
|
|
|
|
|
srv = AssembleClientData(mOrigin, challenge, clientDataJSON);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(srv))) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CryptoBuffer clientDataHash;
|
|
|
|
|
if (!clientDataHash.SetLength(SHA256_LENGTH, fallible)) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
srv = HashCString(hashService, clientDataJSON, clientDataHash);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(srv))) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.1.9 Initialize issuedRequests to an empty list.
|
|
|
|
|
RefPtr<CredentialPromise> monitorPromise = requestMonitor->Ensure();
|
|
|
|
|
|
|
|
|
|
// 4.1.1.10 For each authenticator currently available on this platform:
|
|
|
|
|
// asynchronously invoke the authenticatorMakeCredential operation on that
|
|
|
|
|
// authenticator with rpIdHash, clientDataHash, accountInformation,
|
|
|
|
|
// normalizedParameters, excludeList and clientExtensions as parameters. Add a
|
|
|
|
|
// corresponding entry to issuedRequests.
|
|
|
|
|
for (Authenticator u2ftoken : mAuthenticators) {
|
|
|
|
|
// 4.1.1.10.a For each credential C in excludeList that has a non-empty
|
|
|
|
|
// transports list, optionally use only the specified transports to test for
|
|
|
|
|
// the existence of C.
|
|
|
|
|
U2FAuthMakeCredential(requestMonitor, u2ftoken, rpIdHash, clientDataJSON,
|
|
|
|
|
clientDataHash, aAccount, normalizedParams,
|
|
|
|
|
aOptions.mExcludeList, aOptions.mExtensions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requestMonitor->CompleteTask();
|
|
|
|
|
|
2016-11-22 09:19:06 +03:00
|
|
|
|
monitorPromise->Then(
|
|
|
|
|
global->AbstractMainThreadFor(TaskCategory::Other), __func__,
|
2017-01-09 23:22:49 +03:00
|
|
|
|
[promise] (CredentialPtr aInfo) {
|
|
|
|
|
promise->MaybeResolve(aInfo);
|
|
|
|
|
},
|
|
|
|
|
[promise] (nsresult aErrorCode) {
|
|
|
|
|
promise->MaybeReject(aErrorCode);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
already_AddRefed<Promise>
|
|
|
|
|
WebAuthentication::GetAssertion(const ArrayBufferViewOrArrayBuffer& aChallenge,
|
|
|
|
|
const AssertionOptions& aOptions)
|
|
|
|
|
{
|
|
|
|
|
MOZ_ASSERT(mParent);
|
|
|
|
|
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject());
|
|
|
|
|
if (!global) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.2.1 If timeoutSeconds was specified, check if its value lies within a
|
|
|
|
|
// reasonable range as defined by the platform and if not, correct it to the
|
|
|
|
|
// closest value lying within that range.
|
|
|
|
|
|
|
|
|
|
double adjustedTimeout = 30.0;
|
|
|
|
|
if (aOptions.mTimeoutSeconds.WasPassed()) {
|
|
|
|
|
adjustedTimeout = aOptions.mTimeoutSeconds.Value();
|
|
|
|
|
adjustedTimeout = std::max(15.0, adjustedTimeout);
|
|
|
|
|
adjustedTimeout = std::min(120.0, adjustedTimeout);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.2.2 Let promise be a new Promise. Return promise and start a timer for
|
|
|
|
|
// adjustedTimeout seconds.
|
|
|
|
|
|
|
|
|
|
RefPtr<AssertionRequest> requestMonitor = new AssertionRequest();
|
|
|
|
|
requestMonitor->SetDeadline(TimeDuration::FromSeconds(adjustedTimeout));
|
|
|
|
|
|
|
|
|
|
ErrorResult rv;
|
|
|
|
|
RefPtr<Promise> promise = Promise::Create(global, rv);
|
|
|
|
|
|
|
|
|
|
nsresult initRv = InitLazily();
|
|
|
|
|
if (NS_FAILED(initRv)) {
|
|
|
|
|
promise->MaybeReject(initRv);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mOrigin.EqualsLiteral("null")) {
|
|
|
|
|
// 4.1.2.3 If callerOrigin is an opaque origin, reject promise with a
|
|
|
|
|
// DOMException whose name is "NotAllowedError", and terminate this algorithm
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nsCString rpId;
|
|
|
|
|
if (!aOptions.mRpId.WasPassed()) {
|
|
|
|
|
// 4.1.2.3.a If rpId is not specified, then set rpId to callerOrigin, and
|
|
|
|
|
// rpIdHash to the SHA-256 hash of rpId.
|
|
|
|
|
rpId.Assign(NS_ConvertUTF16toUTF8(mOrigin));
|
|
|
|
|
} else {
|
|
|
|
|
// 4.1.2.3.b If rpId is specified, then invoke the procedure used for
|
|
|
|
|
// relaxing the same-origin restriction by setting the document.domain
|
|
|
|
|
// attribute, using rpId as the given value but without changing the current
|
|
|
|
|
// document’s domain. If no errors are thrown, set rpId to the value of host
|
|
|
|
|
// as computed by this procedure, and rpIdHash to the SHA-256 hash of rpId.
|
|
|
|
|
// Otherwise, reject promise with a DOMException whose name is
|
|
|
|
|
// "SecurityError", and terminate this algorithm.
|
|
|
|
|
|
2017-01-09 23:55:59 +03:00
|
|
|
|
if (NS_FAILED(RelaxSameOrigin(aOptions.mRpId.Value(), rpId))) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
2017-01-09 23:22:49 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CryptoBuffer rpIdHash;
|
|
|
|
|
if (!rpIdHash.SetLength(SHA256_LENGTH, fallible)) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nsresult srv;
|
|
|
|
|
nsCOMPtr<nsICryptoHash> hashService =
|
|
|
|
|
do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(srv))) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
srv = HashCString(hashService, rpId, rpIdHash);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(srv))) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.2.4 If extensions was specified, process any extensions supported by
|
|
|
|
|
// this client platform, to produce the extension data that needs to be sent
|
|
|
|
|
// to the authenticator. If an error is encountered while processing an
|
|
|
|
|
// extension, skip that extension and do not produce any extension data for
|
|
|
|
|
// it. Call the result of this processing clientExtensions.
|
|
|
|
|
|
|
|
|
|
// TODO
|
|
|
|
|
|
|
|
|
|
// 4.1.2.5 Use assertionChallenge, callerOrigin and rpId, along with the token
|
|
|
|
|
// binding key associated with callerOrigin (if any), to create a ClientData
|
|
|
|
|
// structure representing this request. Choose a hash algorithm for hashAlg
|
|
|
|
|
// and compute the clientDataJSON and clientDataHash.
|
|
|
|
|
CryptoBuffer challenge;
|
|
|
|
|
if (!challenge.Assign(aChallenge)) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nsAutoCString clientDataJSON;
|
|
|
|
|
srv = AssembleClientData(mOrigin, challenge, clientDataJSON);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(srv))) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CryptoBuffer clientDataHash;
|
|
|
|
|
if (!clientDataHash.SetLength(SHA256_LENGTH, fallible)) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
srv = HashCString(hashService, clientDataJSON, clientDataHash);
|
|
|
|
|
if (NS_WARN_IF(NS_FAILED(srv))) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-16 20:44:56 +03:00
|
|
|
|
// Note: we only support U2F-style authentication for now, so we effectively
|
|
|
|
|
// require an AllowList.
|
|
|
|
|
if (!aOptions.mAllowList.WasPassed()) {
|
|
|
|
|
promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const Sequence<ScopedCredentialDescriptor>& allowList =
|
|
|
|
|
aOptions.mAllowList.Value();
|
|
|
|
|
|
2017-01-09 23:22:49 +03:00
|
|
|
|
// 4.1.2.6 Initialize issuedRequests to an empty list.
|
|
|
|
|
RefPtr<AssertionPromise> monitorPromise = requestMonitor->Ensure();
|
|
|
|
|
|
|
|
|
|
// 4.1.2.7 For each authenticator currently available on this platform,
|
|
|
|
|
// perform the following steps:
|
|
|
|
|
for(Authenticator u2ftoken : mAuthenticators) {
|
|
|
|
|
// 4.1.2.7.a If allowList is undefined or empty, let credentialList be an
|
|
|
|
|
// empty list. Otherwise, execute a platform-specific procedure to determine
|
|
|
|
|
// which, if any, credentials listed in allowList might be present on this
|
|
|
|
|
// authenticator, and set credentialList to this filtered list. If no such
|
|
|
|
|
// filtering is possible, set credentialList to an empty list.
|
|
|
|
|
|
|
|
|
|
nsTArray<CryptoBuffer> credentialList;
|
|
|
|
|
|
|
|
|
|
for (const ScopedCredentialDescriptor& scd : allowList) {
|
|
|
|
|
CryptoBuffer buf;
|
2016-12-16 20:44:56 +03:00
|
|
|
|
if (NS_WARN_IF(!buf.Assign(scd.mId))) {
|
|
|
|
|
continue;
|
2017-01-09 23:22:49 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.2.7.b For each credential C within the credentialList that has a
|
|
|
|
|
// non- empty transports list, optionally use only the specified
|
|
|
|
|
// transports to get assertions using credential C.
|
|
|
|
|
|
|
|
|
|
// TODO: Filter using Transport
|
2016-12-16 20:44:56 +03:00
|
|
|
|
if (!credentialList.AppendElement(buf, mozilla::fallible)) {
|
|
|
|
|
requestMonitor->CancelNow();
|
|
|
|
|
promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
2017-01-09 23:22:49 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.2.7.c If the above filtering process concludes that none of the
|
|
|
|
|
// credentials on allowList can possibly be on this authenticator, do not
|
|
|
|
|
// perform any of the following steps for this authenticator, and proceed to
|
|
|
|
|
// the next authenticator (if any).
|
|
|
|
|
if (credentialList.IsEmpty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4.1.2.7.d Asynchronously invoke the authenticatorGetAssertion operation
|
|
|
|
|
// on this authenticator with rpIdHash, clientDataHash, credentialList, and
|
|
|
|
|
// clientExtensions as parameters.
|
|
|
|
|
U2FAuthGetAssertion(requestMonitor, u2ftoken, rpIdHash, clientDataJSON,
|
|
|
|
|
clientDataHash, credentialList, aOptions.mExtensions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requestMonitor->CompleteTask();
|
|
|
|
|
|
2016-11-22 09:19:06 +03:00
|
|
|
|
monitorPromise->Then(
|
|
|
|
|
global->AbstractMainThreadFor(TaskCategory::Other), __func__,
|
2017-01-09 23:22:49 +03:00
|
|
|
|
[promise] (AssertionPtr aAssertion) {
|
|
|
|
|
promise->MaybeResolve(aAssertion);
|
|
|
|
|
},
|
|
|
|
|
[promise] (nsresult aErrorCode) {
|
|
|
|
|
promise->MaybeReject(aErrorCode);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return promise.forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace dom
|
|
|
|
|
} // namespace mozilla
|