зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1406471 - Web Authentication - Implement FIDO AppID Extension r=jcj,smaug
Reviewers: jcj, smaug Reviewed By: jcj Bug #: 1406471 Differential Revision: https://phabricator.services.mozilla.com/D595
This commit is contained in:
Родитель
d167e9637d
Коммит
0af61da4ec
123
dom/u2f/U2F.cpp
123
dom/u2f/U2F.cpp
|
@ -9,6 +9,7 @@
|
|||
#include "mozilla/ipc/PBackgroundChild.h"
|
||||
#include "mozilla/ipc/BackgroundChild.h"
|
||||
#include "mozilla/dom/WebAuthnTransactionChild.h"
|
||||
#include "mozilla/dom/WebAuthnUtil.h"
|
||||
#include "nsContentUtils.h"
|
||||
#include "nsICryptoHash.h"
|
||||
#include "nsIEffectiveTLDService.h"
|
||||
|
@ -128,110 +129,6 @@ RegisteredKeysToScopedCredentialList(const nsAString& aAppId,
|
|||
}
|
||||
}
|
||||
|
||||
enum class U2FOperation
|
||||
{
|
||||
Register,
|
||||
Sign
|
||||
};
|
||||
|
||||
static ErrorCode
|
||||
EvaluateAppID(nsPIDOMWindowInner* aParent, const nsString& aOrigin,
|
||||
const U2FOperation& aOp, /* in/out */ nsString& aAppId)
|
||||
{
|
||||
// Facet is the specification's way of referring to the web origin.
|
||||
nsAutoCString facetString = NS_ConvertUTF16toUTF8(aOrigin);
|
||||
nsCOMPtr<nsIURI> facetUri;
|
||||
if (NS_FAILED(NS_NewURI(getter_AddRefs(facetUri), facetString))) {
|
||||
return ErrorCode::BAD_REQUEST;
|
||||
}
|
||||
|
||||
// If the facetId (origin) is not HTTPS, reject
|
||||
bool facetIsHttps = false;
|
||||
if (NS_FAILED(facetUri->SchemeIs("https", &facetIsHttps)) || !facetIsHttps) {
|
||||
return ErrorCode::BAD_REQUEST;
|
||||
}
|
||||
|
||||
// If the appId is empty or null, overwrite it with the facetId and accept
|
||||
if (aAppId.IsEmpty() || aAppId.EqualsLiteral("null")) {
|
||||
aAppId.Assign(aOrigin);
|
||||
return ErrorCode::OK;
|
||||
}
|
||||
|
||||
// AppID is user-supplied. It's quite possible for this parse to fail.
|
||||
nsAutoCString appIdString = NS_ConvertUTF16toUTF8(aAppId);
|
||||
nsCOMPtr<nsIURI> appIdUri;
|
||||
if (NS_FAILED(NS_NewURI(getter_AddRefs(appIdUri), appIdString))) {
|
||||
return ErrorCode::BAD_REQUEST;
|
||||
}
|
||||
|
||||
// if the appId URL is not HTTPS, reject.
|
||||
bool appIdIsHttps = false;
|
||||
if (NS_FAILED(appIdUri->SchemeIs("https", &appIdIsHttps)) || !appIdIsHttps) {
|
||||
return ErrorCode::BAD_REQUEST;
|
||||
}
|
||||
|
||||
nsAutoCString appIdHost;
|
||||
if (NS_FAILED(appIdUri->GetAsciiHost(appIdHost))) {
|
||||
return ErrorCode::BAD_REQUEST;
|
||||
}
|
||||
|
||||
// Allow localhost.
|
||||
if (appIdHost.EqualsLiteral("localhost")) {
|
||||
nsAutoCString facetHost;
|
||||
if (NS_FAILED(facetUri->GetAsciiHost(facetHost))) {
|
||||
return ErrorCode::BAD_REQUEST;
|
||||
}
|
||||
|
||||
if (facetHost.EqualsLiteral("localhost")) {
|
||||
return ErrorCode::OK;
|
||||
}
|
||||
}
|
||||
|
||||
// Run the HTML5 algorithm to relax the same-origin policy, copied from W3C
|
||||
// Web Authentication. See Bug 1244959 comment #8 for context on why we are
|
||||
// doing this instead of implementing the external-fetch FacetID logic.
|
||||
nsCOMPtr<nsIDocument> document = aParent->GetDoc();
|
||||
if (!document || !document->IsHTMLDocument()) {
|
||||
return ErrorCode::BAD_REQUEST;
|
||||
}
|
||||
nsHTMLDocument* html = document->AsHTMLDocument();
|
||||
if (NS_WARN_IF(!html)) {
|
||||
return ErrorCode::BAD_REQUEST;
|
||||
}
|
||||
|
||||
// Use the base domain as the facet for evaluation. This lets this algorithm
|
||||
// relax the whole eTLD+1.
|
||||
nsCOMPtr<nsIEffectiveTLDService> tldService =
|
||||
do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
|
||||
if (!tldService) {
|
||||
return ErrorCode::BAD_REQUEST;
|
||||
}
|
||||
|
||||
nsAutoCString lowestFacetHost;
|
||||
if (NS_FAILED(tldService->GetBaseDomain(facetUri, 0, lowestFacetHost))) {
|
||||
return ErrorCode::BAD_REQUEST;
|
||||
}
|
||||
|
||||
MOZ_LOG(gU2FLog, LogLevel::Debug,
|
||||
("AppId %s Facet %s", appIdHost.get(), lowestFacetHost.get()));
|
||||
|
||||
if (html->IsRegistrableDomainSuffixOfOrEqualTo(NS_ConvertUTF8toUTF16(lowestFacetHost),
|
||||
appIdHost)) {
|
||||
return ErrorCode::OK;
|
||||
}
|
||||
|
||||
// Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
|
||||
if (aOp == U2FOperation::Sign && lowestFacetHost.EqualsLiteral("google.com") &&
|
||||
(aAppId.Equals(kGoogleAccountsAppId1) ||
|
||||
aAppId.Equals(kGoogleAccountsAppId2))) {
|
||||
MOZ_LOG(gU2FLog, LogLevel::Debug,
|
||||
("U2F permitted for Google Accounts via Bug #1436085"));
|
||||
return ErrorCode::OK;
|
||||
}
|
||||
|
||||
return ErrorCode::BAD_REQUEST;
|
||||
}
|
||||
|
||||
static nsresult
|
||||
BuildTransactionHashes(const nsCString& aRpId,
|
||||
const nsCString& aClientDataJSON,
|
||||
|
@ -375,13 +272,10 @@ U2F::Register(const nsAString& aAppId,
|
|||
}
|
||||
|
||||
// Evaluate the AppID
|
||||
nsString adjustedAppId;
|
||||
adjustedAppId.Assign(aAppId);
|
||||
ErrorCode appIdResult = EvaluateAppID(mParent, mOrigin, U2FOperation::Register,
|
||||
adjustedAppId);
|
||||
if (appIdResult != ErrorCode::OK) {
|
||||
nsString adjustedAppId(aAppId);
|
||||
if (!EvaluateAppID(mParent, mOrigin, U2FOperation::Register, adjustedAppId)) {
|
||||
RegisterResponse response;
|
||||
response.mErrorCode.Construct(static_cast<uint32_t>(appIdResult));
|
||||
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
|
||||
ExecuteCallback(response, callback);
|
||||
return;
|
||||
}
|
||||
|
@ -538,13 +432,10 @@ U2F::Sign(const nsAString& aAppId,
|
|||
}
|
||||
|
||||
// Evaluate the AppID
|
||||
nsString adjustedAppId;
|
||||
adjustedAppId.Assign(aAppId);
|
||||
ErrorCode appIdResult = EvaluateAppID(mParent, mOrigin, U2FOperation::Sign,
|
||||
adjustedAppId);
|
||||
if (appIdResult != ErrorCode::OK) {
|
||||
nsString adjustedAppId(aAppId);
|
||||
if (!EvaluateAppID(mParent, mOrigin, U2FOperation::Sign, adjustedAppId)) {
|
||||
SignResponse response;
|
||||
response.mErrorCode.Construct(static_cast<uint32_t>(appIdResult));
|
||||
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
|
||||
ExecuteCallback(response, callback);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -30,8 +30,20 @@ struct WebAuthnScopedCredential {
|
|||
uint8_t transports;
|
||||
};
|
||||
|
||||
struct WebAuthnExtension {
|
||||
/* TODO Fill in with predefined extensions */
|
||||
struct WebAuthnExtensionAppId {
|
||||
uint8_t[] AppId;
|
||||
};
|
||||
|
||||
union WebAuthnExtension {
|
||||
WebAuthnExtensionAppId;
|
||||
};
|
||||
|
||||
struct WebAuthnExtensionResultAppId {
|
||||
bool AppId;
|
||||
};
|
||||
|
||||
union WebAuthnExtensionResult {
|
||||
WebAuthnExtensionResultAppId;
|
||||
};
|
||||
|
||||
struct WebAuthnMakeCredentialInfo {
|
||||
|
@ -57,8 +69,10 @@ struct WebAuthnGetAssertionInfo {
|
|||
};
|
||||
|
||||
struct WebAuthnGetAssertionResult {
|
||||
uint8_t[] RpIdHash;
|
||||
uint8_t[] CredentialID;
|
||||
uint8_t[] SigBuffer;
|
||||
WebAuthnExtensionResult[] Extensions;
|
||||
};
|
||||
|
||||
async protocol PWebAuthnTransaction {
|
||||
|
|
|
@ -115,6 +115,19 @@ PublicKeyCredential::IsUserVerifyingPlatformAuthenticatorAvailable(GlobalObject&
|
|||
return promise.forget();
|
||||
}
|
||||
|
||||
void
|
||||
PublicKeyCredential::GetClientExtensionResults(AuthenticationExtensionsClientOutputs& aResult)
|
||||
{
|
||||
aResult = mClientExtensionOutputs;
|
||||
}
|
||||
|
||||
void
|
||||
PublicKeyCredential::SetClientExtensionResultAppId(bool aResult)
|
||||
{
|
||||
mClientExtensionOutputs.mAppid.Construct();
|
||||
mClientExtensionOutputs.mAppid.Value() = aResult;
|
||||
}
|
||||
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
|
|
@ -49,12 +49,17 @@ public:
|
|||
static already_AddRefed<Promise>
|
||||
IsUserVerifyingPlatformAuthenticatorAvailable(GlobalObject& aGlobal);
|
||||
|
||||
void
|
||||
GetClientExtensionResults(AuthenticationExtensionsClientOutputs& aResult);
|
||||
|
||||
void
|
||||
SetClientExtensionResultAppId(bool aResult);
|
||||
|
||||
private:
|
||||
CryptoBuffer mRawId;
|
||||
JS::Heap<JSObject*> mRawIdCachedObj;
|
||||
RefPtr<AuthenticatorResponse> mResponse;
|
||||
// Extensions are not supported yet.
|
||||
// <some type> mClientExtensionResults;
|
||||
AuthenticationExtensionsClientOutputs mClientExtensionOutputs;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
|
|
|
@ -127,6 +127,7 @@ U2FHIDTokenManager::Register(const nsTArray<WebAuthnScopedCredential>& aCredenti
|
|||
}
|
||||
|
||||
ClearPromises();
|
||||
mCurrentAppId = aApplication;
|
||||
mTransactionId = rust_u2f_mgr_register(mU2FManager,
|
||||
registerFlags,
|
||||
(uint64_t)aTimeoutMS,
|
||||
|
@ -164,6 +165,7 @@ RefPtr<U2FSignPromise>
|
|||
U2FHIDTokenManager::Sign(const nsTArray<WebAuthnScopedCredential>& aCredentials,
|
||||
const nsTArray<uint8_t>& aApplication,
|
||||
const nsTArray<uint8_t>& aChallenge,
|
||||
const nsTArray<WebAuthnExtension>& aExtensions,
|
||||
bool aRequireUserVerification,
|
||||
uint32_t aTimeoutMS)
|
||||
{
|
||||
|
@ -176,15 +178,25 @@ U2FHIDTokenManager::Sign(const nsTArray<WebAuthnScopedCredential>& aCredentials,
|
|||
signFlags |= U2F_FLAG_REQUIRE_USER_VERIFICATION;
|
||||
}
|
||||
|
||||
nsTArray<nsTArray<uint8_t>> appIds;
|
||||
appIds.AppendElement(aApplication);
|
||||
|
||||
// Process extensions.
|
||||
for (const WebAuthnExtension& ext: aExtensions) {
|
||||
if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) {
|
||||
appIds.AppendElement(ext.get_WebAuthnExtensionAppId().AppId());
|
||||
}
|
||||
}
|
||||
|
||||
ClearPromises();
|
||||
mCurrentAppId = aApplication;
|
||||
mTransactionId = rust_u2f_mgr_sign(mU2FManager,
|
||||
signFlags,
|
||||
(uint64_t)aTimeoutMS,
|
||||
u2f_sign_callback,
|
||||
aChallenge.Elements(),
|
||||
aChallenge.Length(),
|
||||
aApplication.Elements(),
|
||||
aApplication.Length(),
|
||||
U2FAppIds(appIds).Get(),
|
||||
U2FKeyHandles(aCredentials).Get());
|
||||
if (mTransactionId == 0) {
|
||||
return U2FSignPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
|
||||
|
@ -234,6 +246,12 @@ U2FHIDTokenManager::HandleSignResult(UniquePtr<U2FResult>&& aResult)
|
|||
|
||||
MOZ_ASSERT(!mSignPromise.IsEmpty());
|
||||
|
||||
nsTArray<uint8_t> appId;
|
||||
if (!aResult->CopyAppId(appId)) {
|
||||
mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> keyHandle;
|
||||
if (!aResult->CopyKeyHandle(keyHandle)) {
|
||||
mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
|
||||
|
@ -246,7 +264,14 @@ U2FHIDTokenManager::HandleSignResult(UniquePtr<U2FResult>&& aResult)
|
|||
return;
|
||||
}
|
||||
|
||||
WebAuthnGetAssertionResult result(keyHandle, signature);
|
||||
nsTArray<WebAuthnExtensionResult> extensions;
|
||||
|
||||
if (appId != mCurrentAppId) {
|
||||
// Indicate to the RP that we used the FIDO appId.
|
||||
extensions.AppendElement(WebAuthnExtensionResultAppId(true));
|
||||
}
|
||||
|
||||
WebAuthnGetAssertionResult result(appId, keyHandle, signature, extensions);
|
||||
mSignPromise.Resolve(Move(result), __func__);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,13 +18,32 @@
|
|||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
class U2FAppIds {
|
||||
public:
|
||||
explicit U2FAppIds(const nsTArray<nsTArray<uint8_t>>& aApplications)
|
||||
{
|
||||
mAppIds = rust_u2f_app_ids_new();
|
||||
|
||||
for (auto& app_id: aApplications) {
|
||||
rust_u2f_app_ids_add(mAppIds, app_id.Elements(), app_id.Length());
|
||||
}
|
||||
}
|
||||
|
||||
rust_u2f_app_ids* Get() { return mAppIds; }
|
||||
|
||||
~U2FAppIds() { rust_u2f_app_ids_free(mAppIds); }
|
||||
|
||||
private:
|
||||
rust_u2f_app_ids* mAppIds;
|
||||
};
|
||||
|
||||
class U2FKeyHandles {
|
||||
public:
|
||||
explicit U2FKeyHandles(const nsTArray<WebAuthnScopedCredential>& aCredentials)
|
||||
{
|
||||
mKeyHandles = rust_u2f_khs_new();
|
||||
|
||||
for (auto cred: aCredentials) {
|
||||
for (auto& cred: aCredentials) {
|
||||
rust_u2f_khs_add(mKeyHandles,
|
||||
cred.id().Elements(),
|
||||
cred.id().Length(),
|
||||
|
@ -66,6 +85,11 @@ public:
|
|||
return CopyBuffer(U2F_RESBUF_ID_SIGNATURE, aBuffer);
|
||||
}
|
||||
|
||||
bool CopyAppId(nsTArray<uint8_t>& aBuffer)
|
||||
{
|
||||
return CopyBuffer(U2F_RESBUF_ID_APPID, aBuffer);
|
||||
}
|
||||
|
||||
private:
|
||||
bool CopyBuffer(uint8_t aResBufID, nsTArray<uint8_t>& aBuffer) {
|
||||
if (!mResult) {
|
||||
|
@ -104,6 +128,7 @@ public:
|
|||
Sign(const nsTArray<WebAuthnScopedCredential>& aCredentials,
|
||||
const nsTArray<uint8_t>& aApplication,
|
||||
const nsTArray<uint8_t>& aChallenge,
|
||||
const nsTArray<WebAuthnExtension>& aExtensions,
|
||||
bool aRequireUserVerification,
|
||||
uint32_t aTimeoutMS) override;
|
||||
|
||||
|
@ -123,6 +148,7 @@ private:
|
|||
|
||||
rust_u2f_manager* mU2FManager;
|
||||
uint64_t mTransactionId;
|
||||
nsTArray<uint8_t> mCurrentAppId;
|
||||
MozPromiseHolder<U2FRegisterPromise> mRegisterPromise;
|
||||
MozPromiseHolder<U2FSignPromise> mSignPromise;
|
||||
};
|
||||
|
|
|
@ -693,6 +693,27 @@ U2FSoftTokenManager::Register(const nsTArray<WebAuthnScopedCredential>& aCredent
|
|||
return U2FRegisterPromise::CreateAndResolve(Move(result), __func__);
|
||||
}
|
||||
|
||||
bool
|
||||
U2FSoftTokenManager::FindRegisteredKeyHandle(const nsTArray<nsTArray<uint8_t>>& aAppIds,
|
||||
const nsTArray<WebAuthnScopedCredential>& aCredentials,
|
||||
/*out*/ nsTArray<uint8_t>& aKeyHandle,
|
||||
/*out*/ nsTArray<uint8_t>& aAppId)
|
||||
{
|
||||
for (const nsTArray<uint8_t>& app_id: aAppIds) {
|
||||
for (const WebAuthnScopedCredential& cred: aCredentials) {
|
||||
bool isRegistered = false;
|
||||
nsresult rv = IsRegistered(cred.id(), app_id, isRegistered);
|
||||
if (NS_SUCCEEDED(rv) && isRegistered) {
|
||||
aKeyHandle.Assign(cred.id());
|
||||
aAppId.Assign(app_id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
|
@ -713,6 +734,7 @@ RefPtr<U2FSignPromise>
|
|||
U2FSoftTokenManager::Sign(const nsTArray<WebAuthnScopedCredential>& aCredentials,
|
||||
const nsTArray<uint8_t>& aApplication,
|
||||
const nsTArray<uint8_t>& aChallenge,
|
||||
const nsTArray<WebAuthnExtension>& aExtensions,
|
||||
bool aRequireUserVerification,
|
||||
uint32_t aTimeoutMS)
|
||||
{
|
||||
|
@ -728,18 +750,21 @@ U2FSoftTokenManager::Sign(const nsTArray<WebAuthnScopedCredential>& aCredentials
|
|||
return U2FSignPromise::CreateAndReject(NS_ERROR_DOM_NOT_ALLOWED_ERR, __func__);
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> keyHandle;
|
||||
for (auto cred: aCredentials) {
|
||||
bool isRegistered = false;
|
||||
nsresult rv = IsRegistered(cred.id(), aApplication, isRegistered);
|
||||
if (NS_SUCCEEDED(rv) && isRegistered) {
|
||||
keyHandle.Assign(cred.id());
|
||||
break;
|
||||
nsTArray<nsTArray<uint8_t>> appIds;
|
||||
appIds.AppendElement(aApplication);
|
||||
|
||||
// Process extensions.
|
||||
for (const WebAuthnExtension& ext: aExtensions) {
|
||||
if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) {
|
||||
appIds.AppendElement(ext.get_WebAuthnExtensionAppId().AppId());
|
||||
}
|
||||
}
|
||||
|
||||
// Fail if we didn't recognize a key id.
|
||||
if (keyHandle.IsEmpty()) {
|
||||
nsTArray<uint8_t> chosenAppId(aApplication);
|
||||
nsTArray<uint8_t> keyHandle;
|
||||
|
||||
// Fail if we can't find a valid key handle.
|
||||
if (!FindRegisteredKeyHandle(appIds, aCredentials, keyHandle, chosenAppId)) {
|
||||
return U2FSignPromise::CreateAndReject(NS_ERROR_DOM_NOT_ALLOWED_ERR, __func__);
|
||||
}
|
||||
|
||||
|
@ -748,10 +773,10 @@ U2FSoftTokenManager::Sign(const nsTArray<WebAuthnScopedCredential>& aCredentials
|
|||
UniquePK11SlotInfo slot(PK11_GetInternalSlot());
|
||||
MOZ_ASSERT(slot.get());
|
||||
|
||||
if (NS_WARN_IF((aChallenge.Length() != kParamLen) || (aApplication.Length() != kParamLen))) {
|
||||
if (NS_WARN_IF((aChallenge.Length() != kParamLen) || (chosenAppId.Length() != kParamLen))) {
|
||||
MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
|
||||
("Parameter lengths are wrong! challenge=%d app=%d expected=%d",
|
||||
(uint32_t)aChallenge.Length(), (uint32_t)aApplication.Length(), kParamLen));
|
||||
(uint32_t)aChallenge.Length(), (uint32_t)chosenAppId.Length(), kParamLen));
|
||||
|
||||
return U2FSignPromise::CreateAndReject(NS_ERROR_ILLEGAL_VALUE, __func__);
|
||||
}
|
||||
|
@ -760,8 +785,8 @@ U2FSoftTokenManager::Sign(const nsTArray<WebAuthnScopedCredential>& aCredentials
|
|||
UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandle(slot, mWrappingKey,
|
||||
const_cast<uint8_t*>(keyHandle.Elements()),
|
||||
keyHandle.Length(),
|
||||
const_cast<uint8_t*>(aApplication.Elements()),
|
||||
aApplication.Length());
|
||||
const_cast<uint8_t*>(chosenAppId.Elements()),
|
||||
chosenAppId.Length());
|
||||
if (NS_WARN_IF(!privKey.get())) {
|
||||
MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Couldn't get the priv key!"));
|
||||
return U2FSignPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
|
||||
|
@ -790,7 +815,7 @@ U2FSoftTokenManager::Sign(const nsTArray<WebAuthnScopedCredential>& aCredentials
|
|||
|
||||
// It's OK to ignore the return values here because we're writing into
|
||||
// pre-allocated space
|
||||
signedDataBuf.AppendElements(aApplication.Elements(), aApplication.Length(),
|
||||
signedDataBuf.AppendElements(chosenAppId.Elements(), chosenAppId.Length(),
|
||||
mozilla::fallible);
|
||||
signedDataBuf.AppendElement(0x01, mozilla::fallible);
|
||||
signedDataBuf.AppendSECItem(counterItem);
|
||||
|
@ -832,7 +857,15 @@ U2FSoftTokenManager::Sign(const nsTArray<WebAuthnScopedCredential>& aCredentials
|
|||
signatureBuf.AppendSECItem(counterItem);
|
||||
signatureBuf.AppendSECItem(signatureItem);
|
||||
|
||||
WebAuthnGetAssertionResult result(keyHandle, nsTArray<uint8_t>(signatureBuf));
|
||||
nsTArray<uint8_t> signature(signatureBuf);
|
||||
nsTArray<WebAuthnExtensionResult> extensions;
|
||||
|
||||
if (chosenAppId != aApplication) {
|
||||
// Indicate to the RP that we used the FIDO appId.
|
||||
extensions.AppendElement(WebAuthnExtensionResultAppId(true));
|
||||
}
|
||||
|
||||
WebAuthnGetAssertionResult result(chosenAppId, keyHandle, signature, extensions);
|
||||
return U2FSignPromise::CreateAndResolve(Move(result), __func__);
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ public:
|
|||
Sign(const nsTArray<WebAuthnScopedCredential>& aCredentials,
|
||||
const nsTArray<uint8_t>& aApplication,
|
||||
const nsTArray<uint8_t>& aChallenge,
|
||||
const nsTArray<WebAuthnExtension>& aExtensions,
|
||||
bool aRequireUserVerification,
|
||||
uint32_t aTimeoutMS) override;
|
||||
|
||||
|
@ -47,6 +48,12 @@ private:
|
|||
const nsTArray<uint8_t>& aAppParam,
|
||||
bool& aResult);
|
||||
|
||||
bool
|
||||
FindRegisteredKeyHandle(const nsTArray<nsTArray<uint8_t>>& aAppIds,
|
||||
const nsTArray<WebAuthnScopedCredential>& aCredentials,
|
||||
/*out*/ nsTArray<uint8_t>& aKeyHandle,
|
||||
/*out*/ nsTArray<uint8_t>& aAppId);
|
||||
|
||||
bool mInitialized;
|
||||
mozilla::UniquePK11SymKey mWrappingKey;
|
||||
|
||||
|
|
|
@ -322,6 +322,7 @@ U2FTokenManager::Sign(PWebAuthnTransactionParent* aTransactionParent,
|
|||
mTokenManagerImpl->Sign(aTransactionInfo.AllowList(),
|
||||
aTransactionInfo.RpIdHash(),
|
||||
aTransactionInfo.ClientDataHash(),
|
||||
aTransactionInfo.Extensions(),
|
||||
aTransactionInfo.RequireUserVerification(),
|
||||
aTransactionInfo.TimeoutMS())
|
||||
->Then(GetCurrentThreadSerialEventTarget(), __func__,
|
||||
|
|
|
@ -38,6 +38,7 @@ public:
|
|||
Sign(const nsTArray<WebAuthnScopedCredential>& aCredentials,
|
||||
const nsTArray<uint8_t>& aApplication,
|
||||
const nsTArray<uint8_t>& aChallenge,
|
||||
const nsTArray<WebAuthnExtension>& aExtensions,
|
||||
bool aRequireUserVerification,
|
||||
uint32_t aTimeoutMS) = 0;
|
||||
|
||||
|
|
|
@ -49,8 +49,11 @@ NS_IMPL_ISUPPORTS(WebAuthnManager, nsIDOMEventListener);
|
|||
**********************************************************************/
|
||||
|
||||
static nsresult
|
||||
AssembleClientData(const nsAString& aOrigin, const CryptoBuffer& aChallenge,
|
||||
const nsAString& aType, /* out */ nsACString& aJsonOut)
|
||||
AssembleClientData(const nsAString& aOrigin,
|
||||
const CryptoBuffer& aChallenge,
|
||||
const nsAString& aType,
|
||||
const AuthenticationExtensionsClientInputs& aExtensions,
|
||||
/* out */ nsACString& aJsonOut)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
|
@ -65,6 +68,7 @@ AssembleClientData(const nsAString& aOrigin, const CryptoBuffer& aChallenge,
|
|||
clientDataObject.mChallenge.Assign(challengeBase64);
|
||||
clientDataObject.mOrigin.Assign(aOrigin);
|
||||
clientDataObject.mHashAlgorithm.AssignLiteral(u"SHA-256");
|
||||
clientDataObject.mClientExtensions = aExtensions;
|
||||
|
||||
nsAutoString temp;
|
||||
if (NS_WARN_IF(!clientDataObject.ToJSON(temp))) {
|
||||
|
@ -264,6 +268,12 @@ WebAuthnManager::MakeCredential(const PublicKeyCredentialCreationOptions& aOptio
|
|||
}
|
||||
}
|
||||
|
||||
// <https://w3c.github.io/webauthn/#sctn-appid-extension>
|
||||
if (aOptions.mExtensions.mAppid.WasPassed()) {
|
||||
promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
|
||||
return promise.forget();
|
||||
}
|
||||
|
||||
CryptoBuffer rpIdHash;
|
||||
if (!rpIdHash.SetLength(SHA256_LENGTH, fallible)) {
|
||||
promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
|
||||
|
@ -345,7 +355,8 @@ WebAuthnManager::MakeCredential(const PublicKeyCredentialCreationOptions& aOptio
|
|||
|
||||
nsAutoCString clientDataJSON;
|
||||
srv = AssembleClientData(origin, challenge,
|
||||
NS_LITERAL_STRING("webauthn.create"), clientDataJSON);
|
||||
NS_LITERAL_STRING("webauthn.create"),
|
||||
aOptions.mExtensions, clientDataJSON);
|
||||
if (NS_WARN_IF(NS_FAILED(srv))) {
|
||||
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
||||
return promise.forget();
|
||||
|
@ -537,7 +548,7 @@ WebAuthnManager::GetAssertion(const PublicKeyCredentialRequestOptions& aOptions,
|
|||
|
||||
nsAutoCString clientDataJSON;
|
||||
srv = AssembleClientData(origin, challenge, NS_LITERAL_STRING("webauthn.get"),
|
||||
clientDataJSON);
|
||||
aOptions.mExtensions, clientDataJSON);
|
||||
if (NS_WARN_IF(NS_FAILED(srv))) {
|
||||
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
||||
return promise.forget();
|
||||
|
@ -593,14 +604,40 @@ WebAuthnManager::GetAssertion(const PublicKeyCredentialRequestOptions& aOptions,
|
|||
bool requireUserVerification =
|
||||
aOptions.mUserVerification == UserVerificationRequirement::Required;
|
||||
|
||||
// TODO: Add extension list building
|
||||
// If extensions was specified, process any extensions supported by this
|
||||
// If extensions were 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.
|
||||
nsTArray<WebAuthnExtension> extensions;
|
||||
|
||||
// <https://w3c.github.io/webauthn/#sctn-appid-extension>
|
||||
if (aOptions.mExtensions.mAppid.WasPassed()) {
|
||||
nsString appId(aOptions.mExtensions.mAppid.Value());
|
||||
|
||||
// Check that the appId value is allowed.
|
||||
if (!EvaluateAppID(mParent, origin, U2FOperation::Sign, appId)) {
|
||||
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
||||
return promise.forget();
|
||||
}
|
||||
|
||||
CryptoBuffer appIdHash;
|
||||
if (!appIdHash.SetLength(SHA256_LENGTH, fallible)) {
|
||||
promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
|
||||
return promise.forget();
|
||||
}
|
||||
|
||||
// We need the SHA-256 hash of the appId.
|
||||
nsresult srv = HashCString(hashService, NS_ConvertUTF16toUTF8(appId), appIdHash);
|
||||
if (NS_WARN_IF(NS_FAILED(srv))) {
|
||||
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
|
||||
return promise.forget();
|
||||
}
|
||||
|
||||
// Append the hash and send it to the backend.
|
||||
extensions.AppendElement(WebAuthnExtensionAppId(appIdHash));
|
||||
}
|
||||
|
||||
WebAuthnGetAssertionInfo info(rpIdHash,
|
||||
clientDataHash,
|
||||
adjustedTimeout,
|
||||
|
@ -807,7 +844,7 @@ WebAuthnManager::FinishGetAssertion(const uint64_t& aTransactionId,
|
|||
}
|
||||
|
||||
CryptoBuffer rpIdHashBuf;
|
||||
if (!rpIdHashBuf.Assign(mTransaction.ref().mRpIdHash)) {
|
||||
if (!rpIdHashBuf.Assign(aResult.RpIdHash())) {
|
||||
RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
|
||||
return;
|
||||
}
|
||||
|
@ -862,6 +899,14 @@ WebAuthnManager::FinishGetAssertion(const uint64_t& aTransactionId,
|
|||
credential->SetRawId(credentialBuf);
|
||||
credential->SetResponse(assertion);
|
||||
|
||||
// Forward client extension results.
|
||||
for (auto& ext: aResult.Extensions()) {
|
||||
if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultAppId) {
|
||||
bool appid = ext.get_WebAuthnExtensionResultAppId().AppId();
|
||||
credential->SetClientExtensionResultAppId(appid);
|
||||
}
|
||||
}
|
||||
|
||||
mTransaction.ref().mPromise->MaybeResolve(credential);
|
||||
ClearTransaction();
|
||||
}
|
||||
|
|
|
@ -5,11 +5,113 @@
|
|||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "mozilla/dom/WebAuthnUtil.h"
|
||||
#include "nsIEffectiveTLDService.h"
|
||||
#include "nsNetUtil.h"
|
||||
#include "pkixutil.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
// Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
|
||||
NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId1,
|
||||
"https://www.gstatic.com/securitykey/origins.json");
|
||||
NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId2,
|
||||
"https://www.gstatic.com/securitykey/a/google.com/origins.json");
|
||||
|
||||
bool
|
||||
EvaluateAppID(nsPIDOMWindowInner* aParent, const nsString& aOrigin,
|
||||
const U2FOperation& aOp, /* in/out */ nsString& aAppId)
|
||||
{
|
||||
// Facet is the specification's way of referring to the web origin.
|
||||
nsAutoCString facetString = NS_ConvertUTF16toUTF8(aOrigin);
|
||||
nsCOMPtr<nsIURI> facetUri;
|
||||
if (NS_FAILED(NS_NewURI(getter_AddRefs(facetUri), facetString))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the facetId (origin) is not HTTPS, reject
|
||||
bool facetIsHttps = false;
|
||||
if (NS_FAILED(facetUri->SchemeIs("https", &facetIsHttps)) || !facetIsHttps) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the appId is empty or null, overwrite it with the facetId and accept
|
||||
if (aAppId.IsEmpty() || aAppId.EqualsLiteral("null")) {
|
||||
aAppId.Assign(aOrigin);
|
||||
return true;
|
||||
}
|
||||
|
||||
// AppID is user-supplied. It's quite possible for this parse to fail.
|
||||
nsAutoCString appIdString = NS_ConvertUTF16toUTF8(aAppId);
|
||||
nsCOMPtr<nsIURI> appIdUri;
|
||||
if (NS_FAILED(NS_NewURI(getter_AddRefs(appIdUri), appIdString))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the appId URL is not HTTPS, reject.
|
||||
bool appIdIsHttps = false;
|
||||
if (NS_FAILED(appIdUri->SchemeIs("https", &appIdIsHttps)) || !appIdIsHttps) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nsAutoCString appIdHost;
|
||||
if (NS_FAILED(appIdUri->GetAsciiHost(appIdHost))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow localhost.
|
||||
if (appIdHost.EqualsLiteral("localhost")) {
|
||||
nsAutoCString facetHost;
|
||||
if (NS_FAILED(facetUri->GetAsciiHost(facetHost))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (facetHost.EqualsLiteral("localhost")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Run the HTML5 algorithm to relax the same-origin policy, copied from W3C
|
||||
// Web Authentication. See Bug 1244959 comment #8 for context on why we are
|
||||
// doing this instead of implementing the external-fetch FacetID logic.
|
||||
nsCOMPtr<nsIDocument> document = aParent->GetDoc();
|
||||
if (!document || !document->IsHTMLDocument()) {
|
||||
return false;
|
||||
}
|
||||
nsHTMLDocument* html = document->AsHTMLDocument();
|
||||
if (NS_WARN_IF(!html)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use the base domain as the facet for evaluation. This lets this algorithm
|
||||
// relax the whole eTLD+1.
|
||||
nsCOMPtr<nsIEffectiveTLDService> tldService =
|
||||
do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
|
||||
if (!tldService) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nsAutoCString lowestFacetHost;
|
||||
if (NS_FAILED(tldService->GetBaseDomain(facetUri, 0, lowestFacetHost))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (html->IsRegistrableDomainSuffixOfOrEqualTo(NS_ConvertUTF8toUTF16(lowestFacetHost),
|
||||
appIdHost)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
|
||||
if (aOp == U2FOperation::Sign && lowestFacetHost.EqualsLiteral("google.com") &&
|
||||
(aAppId.Equals(kGoogleAccountsAppId1) ||
|
||||
aAppId.Equals(kGoogleAccountsAppId2))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
nsresult
|
||||
ReadToCryptoBuffer(pkix::Reader& aSrc, /* out */ CryptoBuffer& aDest,
|
||||
uint32_t aLen)
|
||||
|
|
|
@ -16,6 +16,17 @@
|
|||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
enum class U2FOperation
|
||||
{
|
||||
Register,
|
||||
Sign
|
||||
};
|
||||
|
||||
bool
|
||||
EvaluateAppID(nsPIDOMWindowInner* aParent, const nsString& aOrigin,
|
||||
const U2FOperation& aOp, /* in/out */ nsString& aAppId);
|
||||
|
||||
nsresult
|
||||
AssembleAuthenticatorData(const CryptoBuffer& rpIdHashBuf,
|
||||
const uint8_t flags,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
[DEFAULT]
|
||||
support-files =
|
||||
head.js
|
||||
tab_webauthn_result.html
|
||||
tab_webauthn_success.html
|
||||
../cbor/*
|
||||
|
@ -8,4 +9,5 @@ support-files =
|
|||
skip-if = !e10s
|
||||
|
||||
[browser_abort_visibility.js]
|
||||
[browser_fido_appid_extension.js]
|
||||
[browser_webauthn_telemetry.js]
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const TEST_URL = "https://example.com/";
|
||||
|
||||
function arrivingHereIsBad(aResult) {
|
||||
ok(false, "Bad result! Received a: " + aResult);
|
||||
}
|
||||
|
||||
function expectError(aType) {
|
||||
let expected = `${aType}Error`;
|
||||
return function (aResult) {
|
||||
is(aResult.slice(0, expected.length), expected, `Expecting a ${aType}Error`);
|
||||
};
|
||||
}
|
||||
|
||||
let expectNotSupportedError = expectError("NotSupported");
|
||||
let expectNotAllowedError = expectError("NotAllowed");
|
||||
let expectSecurityError = expectError("Security");
|
||||
|
||||
function promiseU2FRegister(tab, app_id) {
|
||||
let challenge = crypto.getRandomValues(new Uint8Array(16));
|
||||
challenge = bytesToBase64UrlSafe(challenge);
|
||||
|
||||
return ContentTask.spawn(tab.linkedBrowser, [app_id, challenge], function ([app_id, challenge]) {
|
||||
return new Promise(resolve => {
|
||||
content.u2f.register(app_id, [{version: "U2F_V2", challenge}], [], resolve);
|
||||
});
|
||||
}).then(res => {
|
||||
is(res.errorCode, 0, "u2f.register() succeeded");
|
||||
let data = base64ToBytesUrlSafe(res.registrationData);
|
||||
is(data[0], 0x05, "Reserved byte is correct");
|
||||
return data.slice(67, 67 + data[66]);
|
||||
});
|
||||
}
|
||||
|
||||
function promiseWebAuthnRegister(tab, appid) {
|
||||
return ContentTask.spawn(tab.linkedBrowser, [appid], ([appid]) => {
|
||||
const cose_alg_ECDSA_w_SHA256 = -7;
|
||||
|
||||
let challenge = content.crypto.getRandomValues(new Uint8Array(16));
|
||||
|
||||
let pubKeyCredParams = [{
|
||||
type: "public-key",
|
||||
alg: cose_alg_ECDSA_w_SHA256
|
||||
}];
|
||||
|
||||
let publicKey = {
|
||||
rp: {id: content.document.domain, name: "none", icon: "none"},
|
||||
user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
|
||||
pubKeyCredParams,
|
||||
extensions: {appid},
|
||||
challenge
|
||||
};
|
||||
|
||||
return content.navigator.credentials.create({publicKey})
|
||||
.then(res => res.rawId);
|
||||
});
|
||||
}
|
||||
|
||||
function promiseWebAuthnSign(tab, key_handle, extensions = {}) {
|
||||
return ContentTask.spawn(tab.linkedBrowser, [key_handle, extensions],
|
||||
([key_handle, extensions]) => {
|
||||
let challenge = content.crypto.getRandomValues(new Uint8Array(16));
|
||||
|
||||
let credential = {
|
||||
id: key_handle,
|
||||
type: "public-key",
|
||||
transports: ["usb"]
|
||||
};
|
||||
|
||||
let publicKey = {
|
||||
challenge,
|
||||
extensions,
|
||||
rpId: content.document.domain,
|
||||
allowCredentials: [credential],
|
||||
};
|
||||
|
||||
return content.navigator.credentials.get({publicKey})
|
||||
.then(credential => {
|
||||
return {
|
||||
authenticatorData: credential.response.authenticatorData,
|
||||
clientDataJSON: credential.response.clientDataJSON,
|
||||
extensions: credential.getClientExtensionResults()
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function test_setup() {
|
||||
Services.prefs.setBoolPref("security.webauth.u2f", true);
|
||||
Services.prefs.setBoolPref("security.webauth.webauthn", true);
|
||||
Services.prefs.setBoolPref("security.webauth.webauthn_enable_softtoken", true);
|
||||
Services.prefs.setBoolPref("security.webauth.webauthn_enable_usbtoken", false);
|
||||
});
|
||||
|
||||
add_task(async function test_appid() {
|
||||
// Open a new tab.
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
|
||||
|
||||
// Get a keyHandle for a FIDO AppId.
|
||||
let appid = "https://example.com/appId";
|
||||
let keyHandle = await promiseU2FRegister(tab, appid);
|
||||
|
||||
// The FIDO AppId extension can't be used for MakeCredential.
|
||||
await promiseWebAuthnRegister(tab, appid)
|
||||
.then(arrivingHereIsBad)
|
||||
.catch(expectNotSupportedError);
|
||||
|
||||
// Using the keyHandle shouldn't work without the FIDO AppId extension.
|
||||
await promiseWebAuthnSign(tab, keyHandle)
|
||||
.then(arrivingHereIsBad)
|
||||
.catch(expectNotAllowedError);
|
||||
|
||||
// Invalid app IDs (for the current origin) must be rejected.
|
||||
await promiseWebAuthnSign(tab, keyHandle, {appid: "https://bogus.com/appId"})
|
||||
.then(arrivingHereIsBad)
|
||||
.catch(expectSecurityError);
|
||||
|
||||
// Non-matching app IDs must be rejected.
|
||||
await promiseWebAuthnSign(tab, keyHandle, {appid: appid + "2"})
|
||||
.then(arrivingHereIsBad)
|
||||
.catch(expectNotAllowedError);
|
||||
|
||||
let rpId = new TextEncoder("utf-8").encode(appid);
|
||||
let rpIdHash = await crypto.subtle.digest("SHA-256", rpId);
|
||||
|
||||
// Succeed with the right fallback rpId.
|
||||
await promiseWebAuthnSign(tab, keyHandle, {appid})
|
||||
.then(({authenticatorData, clientDataJSON, extensions}) => {
|
||||
is(extensions.appid, true, "appid extension was acted upon");
|
||||
|
||||
// Check that the correct rpIdHash is returned.
|
||||
let rpIdHashSign = authenticatorData.slice(0, 32);
|
||||
ok(memcmp(rpIdHash, rpIdHashSign), "rpIdHash is correct");
|
||||
|
||||
let clientData = JSON.parse(buffer2string(clientDataJSON));
|
||||
is(clientData.clientExtensions.appid, appid, "appid extension sent");
|
||||
});
|
||||
|
||||
// Close tab.
|
||||
await BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(function test_cleanup() {
|
||||
Services.prefs.clearUserPref("security.webauth.u2f");
|
||||
Services.prefs.clearUserPref("security.webauth.webauthn");
|
||||
Services.prefs.clearUserPref("security.webauth.webauthn_enable_softtoken");
|
||||
Services.prefs.clearUserPref("security.webauth.webauthn_enable_usbtoken");
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
function bytesToBase64(u8a){
|
||||
let CHUNK_SZ = 0x8000;
|
||||
let c = [];
|
||||
for (let 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 bytesToBase64UrlSafe(buf) {
|
||||
return bytesToBase64(buf)
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
}
|
||||
|
||||
function base64ToBytes(b64encoded) {
|
||||
return new Uint8Array(window.atob(b64encoded).split("").map(function(c) {
|
||||
return c.charCodeAt(0);
|
||||
}));
|
||||
}
|
||||
|
||||
function base64ToBytesUrlSafe(str) {
|
||||
if (!str || 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 buffer2string(buf) {
|
||||
let str = "";
|
||||
if (!(buf.constructor === Uint8Array)) {
|
||||
buf = new Uint8Array(buf);
|
||||
}
|
||||
buf.map(function(x){ return str += String.fromCharCode(x) });
|
||||
return str;
|
||||
}
|
||||
|
||||
function memcmp(x, y) {
|
||||
let xb = new Uint8Array(x);
|
||||
let yb = new Uint8Array(y);
|
||||
|
||||
if (x.byteLength != y.byteLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < xb.byteLength; ++i) {
|
||||
if (xb[i] != yb[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
|
@ -68,6 +68,10 @@ function() {
|
|||
is(clientData.hashAlgorithm, "SHA-256", "Hash algorithm is correct");
|
||||
is(clientData.type, "webauthn.create", "Type is correct");
|
||||
|
||||
let extensions = aCredInfo.getClientExtensionResults();
|
||||
is(extensions.appid, undefined, "appid extension wasn't used");
|
||||
is(clientData.clientExtensions.appid, undefined, "appid extension wasn't sent");
|
||||
|
||||
return webAuthnDecodeCBORAttestation(aCredInfo.response.attestationObject)
|
||||
.then(function(aAttestationObj) {
|
||||
// Make sure the RP ID hash matches what we calculate.
|
||||
|
|
|
@ -82,7 +82,7 @@ fn main() {
|
|||
flags,
|
||||
15_000,
|
||||
chall_bytes,
|
||||
app_bytes,
|
||||
vec![app_bytes],
|
||||
vec![key_handle],
|
||||
move |rv| { tx.send(rv.unwrap()).unwrap(); },
|
||||
)
|
||||
|
|
|
@ -9,6 +9,7 @@ use std::{ptr, slice};
|
|||
|
||||
use U2FManager;
|
||||
|
||||
type U2FAppIds = Vec<::AppId>;
|
||||
type U2FKeyHandles = Vec<::KeyHandle>;
|
||||
type U2FResult = HashMap<u8, Vec<u8>>;
|
||||
type U2FCallback = extern "C" fn(u64, *mut U2FResult);
|
||||
|
@ -16,6 +17,7 @@ type U2FCallback = extern "C" fn(u64, *mut U2FResult);
|
|||
const RESBUF_ID_REGISTRATION: u8 = 0;
|
||||
const RESBUF_ID_KEYHANDLE: u8 = 1;
|
||||
const RESBUF_ID_SIGNATURE: u8 = 2;
|
||||
const RESBUF_ID_APPID: u8 = 3;
|
||||
|
||||
// Generates a new 64-bit transaction id with collision probability 2^-32.
|
||||
fn new_tid() -> u64 {
|
||||
|
@ -42,6 +44,27 @@ pub unsafe extern "C" fn rust_u2f_mgr_free(mgr: *mut U2FManager) {
|
|||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_u2f_app_ids_new() -> *mut U2FAppIds {
|
||||
Box::into_raw(Box::new(vec![]))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_u2f_app_ids_add(
|
||||
ids: *mut U2FAppIds,
|
||||
id_ptr: *const u8,
|
||||
id_len: usize
|
||||
) {
|
||||
(*ids).push(from_raw(id_ptr, id_len));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_u2f_app_ids_free(ids: *mut U2FAppIds) {
|
||||
if !ids.is_null() {
|
||||
Box::from_raw(ids);
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_u2f_khs_new() -> *mut U2FKeyHandles {
|
||||
Box::into_raw(Box::new(vec![]))
|
||||
|
@ -165,8 +188,7 @@ pub unsafe extern "C" fn rust_u2f_mgr_sign(
|
|||
callback: U2FCallback,
|
||||
challenge_ptr: *const u8,
|
||||
challenge_len: usize,
|
||||
application_ptr: *const u8,
|
||||
application_len: usize,
|
||||
app_ids: *const U2FAppIds,
|
||||
khs: *const U2FKeyHandles,
|
||||
) -> u64 {
|
||||
if mgr.is_null() || khs.is_null() {
|
||||
|
@ -174,13 +196,18 @@ pub unsafe extern "C" fn rust_u2f_mgr_sign(
|
|||
}
|
||||
|
||||
// Check buffers.
|
||||
if challenge_ptr.is_null() || application_ptr.is_null() {
|
||||
if challenge_ptr.is_null() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Need at least one app_id.
|
||||
if (*app_ids).len() < 1 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let flags = ::SignFlags::from_bits_truncate(flags);
|
||||
let challenge = from_raw(challenge_ptr, challenge_len);
|
||||
let application = from_raw(application_ptr, application_len);
|
||||
let app_ids = (*app_ids).clone();
|
||||
let key_handles = (*khs).clone();
|
||||
|
||||
let tid = new_tid();
|
||||
|
@ -188,13 +215,14 @@ pub unsafe extern "C" fn rust_u2f_mgr_sign(
|
|||
flags,
|
||||
timeout,
|
||||
challenge,
|
||||
application,
|
||||
app_ids,
|
||||
key_handles,
|
||||
move |rv| {
|
||||
if let Ok((key_handle, signature)) = rv {
|
||||
if let Ok((app_id, key_handle, signature)) = rv {
|
||||
let mut result = U2FResult::new();
|
||||
result.insert(RESBUF_ID_KEYHANDLE, key_handle);
|
||||
result.insert(RESBUF_ID_SIGNATURE, signature);
|
||||
result.insert(RESBUF_ID_APPID, app_id);
|
||||
callback(tid, Box::into_raw(Box::new(result)));
|
||||
} else {
|
||||
callback(tid, ptr::null_mut());
|
||||
|
|
|
@ -75,6 +75,10 @@ pub struct KeyHandle {
|
|||
pub transports: AuthenticatorTransports,
|
||||
}
|
||||
|
||||
pub type AppId = Vec<u8>;
|
||||
pub type RegisterResult = Vec<u8>;
|
||||
pub type SignResult = (AppId, Vec<u8>, Vec<u8>);
|
||||
|
||||
#[cfg(fuzzing)]
|
||||
pub use u2fprotocol::*;
|
||||
#[cfg(fuzzing)]
|
||||
|
|
|
@ -16,17 +16,17 @@ enum QueueAction {
|
|||
flags: ::RegisterFlags,
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
application: Vec<u8>,
|
||||
application: ::AppId,
|
||||
key_handles: Vec<::KeyHandle>,
|
||||
callback: OnceCallback<Vec<u8>>,
|
||||
callback: OnceCallback<::RegisterResult>,
|
||||
},
|
||||
Sign {
|
||||
flags: ::SignFlags,
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
application: Vec<u8>,
|
||||
app_ids: Vec<::AppId>,
|
||||
key_handles: Vec<::KeyHandle>,
|
||||
callback: OnceCallback<(Vec<u8>, Vec<u8>)>,
|
||||
callback: OnceCallback<::SignResult>,
|
||||
},
|
||||
Cancel,
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ impl U2FManager {
|
|||
flags,
|
||||
timeout,
|
||||
challenge,
|
||||
application,
|
||||
app_ids,
|
||||
key_handles,
|
||||
callback,
|
||||
}) => {
|
||||
|
@ -77,7 +77,7 @@ impl U2FManager {
|
|||
flags,
|
||||
timeout,
|
||||
challenge,
|
||||
application,
|
||||
app_ids,
|
||||
key_handles,
|
||||
callback,
|
||||
);
|
||||
|
@ -109,12 +109,12 @@ impl U2FManager {
|
|||
flags: ::RegisterFlags,
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
application: Vec<u8>,
|
||||
application: ::AppId,
|
||||
key_handles: Vec<::KeyHandle>,
|
||||
callback: F,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(io::Result<Vec<u8>>),
|
||||
F: FnOnce(io::Result<::RegisterResult>),
|
||||
F: Send + 'static,
|
||||
{
|
||||
if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE {
|
||||
|
@ -150,21 +150,37 @@ impl U2FManager {
|
|||
flags: ::SignFlags,
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
application: Vec<u8>,
|
||||
app_ids: Vec<::AppId>,
|
||||
key_handles: Vec<::KeyHandle>,
|
||||
callback: F,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(io::Result<(Vec<u8>, Vec<u8>)>),
|
||||
F: FnOnce(io::Result<::SignResult>),
|
||||
F: Send + 'static,
|
||||
{
|
||||
if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE {
|
||||
if challenge.len() != PARAMETER_SIZE {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Invalid parameter sizes",
|
||||
));
|
||||
}
|
||||
|
||||
if app_ids.len() < 1 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"No app IDs given",
|
||||
));
|
||||
}
|
||||
|
||||
for app_id in &app_ids {
|
||||
if app_id.len() != PARAMETER_SIZE {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Invalid app_id size",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for key_handle in &key_handles {
|
||||
if key_handle.credential.len() > 256 {
|
||||
return Err(io::Error::new(
|
||||
|
@ -179,7 +195,7 @@ impl U2FManager {
|
|||
flags,
|
||||
timeout,
|
||||
challenge,
|
||||
application,
|
||||
app_ids,
|
||||
key_handles,
|
||||
callback,
|
||||
};
|
||||
|
|
|
@ -14,6 +14,31 @@ fn is_valid_transport(transports: ::AuthenticatorTransports) -> bool {
|
|||
transports.is_empty() || transports.contains(::AuthenticatorTransports::USB)
|
||||
}
|
||||
|
||||
fn find_valid_key_handles<'a, F>(
|
||||
app_ids: &'a Vec<::AppId>,
|
||||
key_handles: &'a Vec<::KeyHandle>,
|
||||
mut is_valid: F,
|
||||
) -> (&'a ::AppId, Vec<&'a ::KeyHandle>)
|
||||
where
|
||||
F: FnMut(&Vec<u8>, &::KeyHandle) -> bool,
|
||||
{
|
||||
// Try all given app_ids in order.
|
||||
for app_id in app_ids {
|
||||
// Find all valid key handles for the current app_id.
|
||||
let valid_handles = key_handles
|
||||
.iter()
|
||||
.filter(|key_handle| is_valid(app_id, key_handle))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// If there's at least one, stop.
|
||||
if valid_handles.len() > 0 {
|
||||
return (app_id, valid_handles);
|
||||
}
|
||||
}
|
||||
|
||||
return (&app_ids[0], vec![]);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct StateMachine {
|
||||
transaction: Option<Transaction>,
|
||||
|
@ -29,9 +54,9 @@ impl StateMachine {
|
|||
flags: ::RegisterFlags,
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
application: Vec<u8>,
|
||||
application: ::AppId,
|
||||
key_handles: Vec<::KeyHandle>,
|
||||
callback: OnceCallback<Vec<u8>>,
|
||||
callback: OnceCallback<::RegisterResult>,
|
||||
) {
|
||||
// Abort any prior register/sign calls.
|
||||
self.cancel();
|
||||
|
@ -93,9 +118,9 @@ impl StateMachine {
|
|||
flags: ::SignFlags,
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
application: Vec<u8>,
|
||||
app_ids: Vec<::AppId>,
|
||||
key_handles: Vec<::KeyHandle>,
|
||||
callback: OnceCallback<(Vec<u8>, Vec<u8>)>,
|
||||
callback: OnceCallback<::SignResult>,
|
||||
) {
|
||||
// Abort any prior register/sign calls.
|
||||
self.cancel();
|
||||
|
@ -125,14 +150,15 @@ impl StateMachine {
|
|||
return;
|
||||
}
|
||||
|
||||
// Find all matching key handles.
|
||||
let key_handles = key_handles
|
||||
.iter()
|
||||
.filter(|key_handle| {
|
||||
u2f_is_keyhandle_valid(dev, &challenge, &application, &key_handle.credential)
|
||||
.unwrap_or(false) /* no match on failure */
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// For each appId, try all key handles. If there's at least one
|
||||
// valid key handle for an appId, we'll use that appId below.
|
||||
let (app_id, valid_handles) =
|
||||
find_valid_key_handles(&app_ids, &key_handles,
|
||||
|app_id, key_handle| {
|
||||
u2f_is_keyhandle_valid(dev, &challenge, app_id,
|
||||
&key_handle.credential)
|
||||
.unwrap_or(false) /* no match on failure */
|
||||
});
|
||||
|
||||
// Aggregate distinct transports from all given credentials.
|
||||
let transports = key_handles.iter().fold(
|
||||
|
@ -149,7 +175,7 @@ impl StateMachine {
|
|||
while alive() {
|
||||
// If the device matches none of the given key handles
|
||||
// then just make it blink with bogus data.
|
||||
if key_handles.is_empty() {
|
||||
if valid_handles.is_empty() {
|
||||
let blank = vec![0u8; PARAMETER_SIZE];
|
||||
if let Ok(_) = u2f_register(dev, &blank, &blank) {
|
||||
callback.call(Err(io_err("invalid key")));
|
||||
|
@ -157,15 +183,17 @@ impl StateMachine {
|
|||
}
|
||||
} else {
|
||||
// Otherwise, try to sign.
|
||||
for key_handle in &key_handles {
|
||||
for key_handle in &valid_handles {
|
||||
if let Ok(bytes) = u2f_sign(
|
||||
dev,
|
||||
&challenge,
|
||||
&application,
|
||||
app_id,
|
||||
&key_handle.credential,
|
||||
)
|
||||
{
|
||||
callback.call(Ok((key_handle.credential.clone(), bytes)));
|
||||
callback.call(Ok((app_id.clone(),
|
||||
key_handle.credential.clone(),
|
||||
bytes)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ extern "C" {
|
|||
const uint8_t U2F_RESBUF_ID_REGISTRATION = 0;
|
||||
const uint8_t U2F_RESBUF_ID_KEYHANDLE = 1;
|
||||
const uint8_t U2F_RESBUF_ID_SIGNATURE = 2;
|
||||
const uint8_t U2F_RESBUF_ID_APPID = 3;
|
||||
|
||||
const uint64_t U2F_FLAG_REQUIRE_RESIDENT_KEY = 1;
|
||||
const uint64_t U2F_FLAG_REQUIRE_USER_VERIFICATION = 2;
|
||||
|
@ -34,6 +35,9 @@ const uint8_t U2F_AUTHENTICATOR_TRANSPORT_BLE = 4;
|
|||
// The `rust_u2f_mgr` opaque type is equivalent to the rust type `U2FManager`
|
||||
struct rust_u2f_manager;
|
||||
|
||||
// The `rust_u2f_app_ids` opaque type is equivalent to the rust type `U2FAppIds`
|
||||
struct rust_u2f_app_ids;
|
||||
|
||||
// The `rust_u2f_key_handles` opaque type is equivalent to the rust type `U2FKeyHandles`
|
||||
struct rust_u2f_key_handles;
|
||||
|
||||
|
@ -65,13 +69,21 @@ uint64_t rust_u2f_mgr_sign(rust_u2f_manager* mgr,
|
|||
rust_u2f_callback,
|
||||
const uint8_t* challenge_ptr,
|
||||
size_t challenge_len,
|
||||
const uint8_t* application_ptr,
|
||||
size_t application_len,
|
||||
const rust_u2f_app_ids* app_ids,
|
||||
const rust_u2f_key_handles* khs);
|
||||
|
||||
uint64_t rust_u2f_mgr_cancel(rust_u2f_manager* mgr);
|
||||
|
||||
|
||||
/// U2FAppIds functions.
|
||||
|
||||
rust_u2f_app_ids* rust_u2f_app_ids_new();
|
||||
void rust_u2f_app_ids_add(rust_u2f_app_ids* ids,
|
||||
const uint8_t* id,
|
||||
size_t id_len);
|
||||
/* unsafe */ void rust_u2f_app_ids_free(rust_u2f_app_ids* ids);
|
||||
|
||||
|
||||
/// U2FKeyHandles functions.
|
||||
|
||||
rust_u2f_key_handles* rust_u2f_khs_new();
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
interface PublicKeyCredential : Credential {
|
||||
[SameObject] readonly attribute ArrayBuffer rawId;
|
||||
[SameObject] readonly attribute AuthenticatorResponse response;
|
||||
// Extensions are not supported yet.
|
||||
// [SameObject] readonly attribute AuthenticationExtensions clientExtensionResults; // Add in Bug 1406458
|
||||
AuthenticationExtensionsClientOutputs getClientExtensionResults();
|
||||
};
|
||||
|
||||
[SecureContext]
|
||||
|
@ -104,10 +103,18 @@ dictionary PublicKeyCredentialRequestOptions {
|
|||
AuthenticationExtensionsClientInputs extensions;
|
||||
};
|
||||
|
||||
// TODO - Use partial dictionaries when bug 1436329 is fixed.
|
||||
dictionary AuthenticationExtensionsClientInputs {
|
||||
// FIDO AppID Extension (appid)
|
||||
// <https://w3c.github.io/webauthn/#sctn-appid-extension>
|
||||
USVString appid;
|
||||
};
|
||||
|
||||
// TODO - Use partial dictionaries when bug 1436329 is fixed.
|
||||
dictionary AuthenticationExtensionsClientOutputs {
|
||||
// FIDO AppID Extension (appid)
|
||||
// <https://w3c.github.io/webauthn/#sctn-appid-extension>
|
||||
boolean appid;
|
||||
};
|
||||
|
||||
typedef record<DOMString, DOMString> AuthenticationExtensionsAuthenticatorInputs;
|
||||
|
@ -144,3 +151,16 @@ typedef sequence<AAGUID> AuthenticatorSelectionList;
|
|||
|
||||
typedef BufferSource AAGUID;
|
||||
|
||||
/*
|
||||
// FIDO AppID Extension (appid)
|
||||
// <https://w3c.github.io/webauthn/#sctn-appid-extension>
|
||||
partial dictionary AuthenticationExtensionsClientInputs {
|
||||
USVString appid;
|
||||
};
|
||||
|
||||
// FIDO AppID Extension (appid)
|
||||
// <https://w3c.github.io/webauthn/#sctn-appid-extension>
|
||||
partial dictionary AuthenticationExtensionsClientOutputs {
|
||||
boolean appid;
|
||||
};
|
||||
*/
|
||||
|
|
Загрузка…
Ссылка в новой задаче