Bug 1823782 - implement webauthn serialization methods r=jschanck,webidl,saschanaz,tschuster

Differential Revision: https://phabricator.services.mozilla.com/D187239
This commit is contained in:
Dana Keeler 2023-09-19 20:03:37 +00:00
Родитель 23be0b7524
Коммит a5cfd59207
12 изменённых файлов: 815 добавлений и 29 удалений

Просмотреть файл

@ -4,6 +4,7 @@
* 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/Base64.h"
#include "mozilla/dom/WebAuthenticationBinding.h"
#include "mozilla/dom/AuthenticatorAssertionResponse.h"
#include "mozilla/HoldDropJSObjects.h"
@ -115,4 +116,55 @@ void AuthenticatorAssertionResponse::SetUserHandle(
mUserHandle.Assign(aBuffer);
}
void AuthenticatorAssertionResponse::ToJSON(
AuthenticatorAssertionResponseJSON& aJSON, ErrorResult& aError) {
nsAutoCString clientDataJSONBase64;
nsresult rv = Base64URLEncode(
mClientDataJSON.Length(),
reinterpret_cast<const uint8_t*>(mClientDataJSON.get()),
mozilla::Base64URLEncodePaddingPolicy::Omit, clientDataJSONBase64);
// This will only fail if the length is so long that it overflows 32-bits
// when calculating the encoded size.
if (NS_FAILED(rv)) {
aError.ThrowDataError("clientDataJSON too long");
return;
}
aJSON.mClientDataJSON.Assign(NS_ConvertUTF8toUTF16(clientDataJSONBase64));
nsAutoCString authenticatorDataBase64;
rv = Base64URLEncode(
mAuthenticatorData.Length(), mAuthenticatorData.Elements(),
mozilla::Base64URLEncodePaddingPolicy::Omit, authenticatorDataBase64);
if (NS_FAILED(rv)) {
aError.ThrowDataError("authenticatorData too long");
return;
}
aJSON.mAuthenticatorData.Assign(
NS_ConvertUTF8toUTF16(authenticatorDataBase64));
nsAutoCString signatureBase64;
rv = Base64URLEncode(mSignature.Length(), mSignature.Elements(),
mozilla::Base64URLEncodePaddingPolicy::Omit,
signatureBase64);
if (NS_FAILED(rv)) {
aError.ThrowDataError("signature too long");
return;
}
aJSON.mSignature.Assign(NS_ConvertUTF8toUTF16(signatureBase64));
if (!mUserHandle.IsEmpty()) {
nsAutoCString userHandleBase64;
rv = Base64URLEncode(mUserHandle.Length(), mUserHandle.Elements(),
mozilla::Base64URLEncodePaddingPolicy::Omit,
userHandleBase64);
if (NS_FAILED(rv)) {
aError.ThrowDataError("userHandle too long");
return;
}
aJSON.mUserHandle.Construct(NS_ConvertUTF8toUTF16(userHandleBase64));
}
// attestationObject is not currently supported on assertion responses
}
} // namespace mozilla::dom

Просмотреть файл

@ -11,6 +11,7 @@
#include "mozilla/Attributes.h"
#include "mozilla/dom/AuthenticatorResponse.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/WebAuthenticationBinding.h"
#include "nsCycleCollectionParticipant.h"
#include "nsWrapperCache.h"
@ -46,6 +47,8 @@ class AuthenticatorAssertionResponse final : public AuthenticatorResponse {
void SetUserHandle(const nsTArray<uint8_t>& aBuffer);
void ToJSON(AuthenticatorAssertionResponseJSON& aJSON, ErrorResult& aError);
private:
nsTArray<uint8_t> mAuthenticatorData;
JS::Heap<JSObject*> mAuthenticatorDataCachedObj;

Просмотреть файл

@ -5,10 +5,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "AuthrsBridge_ffi.h"
#include "mozilla/dom/WebAuthenticationBinding.h"
#include "mozilla/dom/AuthenticatorAttestationResponse.h"
#include "mozilla/Base64.h"
#include "mozilla/HoldDropJSObjects.h"
#include "mozilla/dom/AuthenticatorAttestationResponse.h"
#include "mozilla/dom/WebAuthenticationBinding.h"
namespace mozilla::dom {
@ -79,21 +79,28 @@ void AuthenticatorAttestationResponse::SetTransports(
mTransports.Assign(aTransports);
}
void AuthenticatorAttestationResponse::GetAuthenticatorData(
JSContext* aCx, JS::MutableHandle<JSObject*> aValue, ErrorResult& aRv) {
nsresult AuthenticatorAttestationResponse::GetAuthenticatorDataBytes(
nsTArray<uint8_t>& aAuthenticatorData) {
if (!mAttestationObjectParsed) {
nsresult rv = authrs_webauthn_att_obj_constructor(
mAttestationObject, /* anonymize */ false,
getter_AddRefs(mAttestationObjectParsed));
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return;
return rv;
}
}
nsTArray<uint8_t> authenticatorData;
nsresult rv =
mAttestationObjectParsed->GetAuthenticatorData(authenticatorData);
mAttestationObjectParsed->GetAuthenticatorData(aAuthenticatorData);
if (NS_FAILED(rv)) {
return rv;
}
return NS_OK;
}
void AuthenticatorAttestationResponse::GetAuthenticatorData(
JSContext* aCx, JS::MutableHandle<JSObject*> aValue, ErrorResult& aRv) {
nsTArray<uint8_t> authenticatorData;
nsresult rv = GetAuthenticatorDataBytes(authenticatorData);
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return;
@ -108,19 +115,27 @@ void AuthenticatorAttestationResponse::GetAuthenticatorData(
aValue.set(buffer);
}
void AuthenticatorAttestationResponse::GetPublicKey(
JSContext* aCx, JS::MutableHandle<JSObject*> aValue, ErrorResult& aRv) {
nsresult AuthenticatorAttestationResponse::GetPublicKeyBytes(
nsTArray<uint8_t>& aPublicKeyBytes) {
if (!mAttestationObjectParsed) {
nsresult rv = authrs_webauthn_att_obj_constructor(
mAttestationObject, false, getter_AddRefs(mAttestationObjectParsed));
mAttestationObject, /* anonymize */ false,
getter_AddRefs(mAttestationObjectParsed));
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return;
return rv;
}
}
nsresult rv = mAttestationObjectParsed->GetPublicKey(aPublicKeyBytes);
if (NS_FAILED(rv)) {
return rv;
}
return NS_OK;
}
void AuthenticatorAttestationResponse::GetPublicKey(
JSContext* aCx, JS::MutableHandle<JSObject*> aValue, ErrorResult& aRv) {
nsTArray<uint8_t> publicKey;
nsresult rv = mAttestationObjectParsed->GetPublicKey(publicKey);
nsresult rv = GetPublicKeyBytes(publicKey);
if (NS_FAILED(rv)) {
if (rv == NS_ERROR_NOT_AVAILABLE) {
aValue.set(nullptr);
@ -155,4 +170,77 @@ COSEAlgorithmIdentifier AuthenticatorAttestationResponse::GetPublicKeyAlgorithm(
return alg;
}
void AuthenticatorAttestationResponse::ToJSON(
AuthenticatorAttestationResponseJSON& aJSON, ErrorResult& aError) {
nsAutoCString clientDataJSONBase64;
nsresult rv = Base64URLEncode(
mClientDataJSON.Length(),
reinterpret_cast<const uint8_t*>(mClientDataJSON.get()),
mozilla::Base64URLEncodePaddingPolicy::Omit, clientDataJSONBase64);
// This will only fail if the length is so long that it overflows 32-bits
// when calculating the encoded size.
if (NS_FAILED(rv)) {
aError.ThrowDataError("clientDataJSON too long");
return;
}
aJSON.mClientDataJSON.Assign(NS_ConvertUTF8toUTF16(clientDataJSONBase64));
nsTArray<uint8_t> authenticatorData;
rv = GetAuthenticatorDataBytes(authenticatorData);
if (NS_FAILED(rv)) {
aError.ThrowUnknownError("could not get authenticatorData");
return;
}
nsAutoCString authenticatorDataBase64;
rv = Base64URLEncode(authenticatorData.Length(), authenticatorData.Elements(),
mozilla::Base64URLEncodePaddingPolicy::Omit,
authenticatorDataBase64);
if (NS_FAILED(rv)) {
aError.ThrowDataError("authenticatorData too long");
return;
}
aJSON.mAuthenticatorData.Assign(
NS_ConvertUTF8toUTF16(authenticatorDataBase64));
if (!aJSON.mTransports.Assign(mTransports, mozilla::fallible)) {
aError.Throw(NS_ERROR_OUT_OF_MEMORY);
return;
}
nsTArray<uint8_t> publicKeyBytes;
rv = GetPublicKeyBytes(publicKeyBytes);
if (NS_SUCCEEDED(rv)) {
nsAutoCString publicKeyBytesBase64;
rv = Base64URLEncode(publicKeyBytes.Length(), publicKeyBytes.Elements(),
mozilla::Base64URLEncodePaddingPolicy::Omit,
publicKeyBytesBase64);
if (NS_FAILED(rv)) {
aError.ThrowDataError("publicKey too long");
return;
}
aJSON.mPublicKey.Construct(NS_ConvertUTF8toUTF16(publicKeyBytesBase64));
} else if (rv != NS_ERROR_NOT_AVAILABLE) {
aError.ThrowUnknownError("could not get publicKey");
return;
}
COSEAlgorithmIdentifier publicKeyAlgorithm = GetPublicKeyAlgorithm(aError);
if (aError.Failed()) {
aError.ThrowUnknownError("could not get publicKeyAlgorithm");
return;
}
aJSON.mPublicKeyAlgorithm = publicKeyAlgorithm;
nsAutoCString attestationObjectBase64;
rv = Base64URLEncode(
mAttestationObject.Length(), mAttestationObject.Elements(),
mozilla::Base64URLEncodePaddingPolicy::Omit, attestationObjectBase64);
if (NS_FAILED(rv)) {
aError.ThrowDataError("attestationObject too long");
return;
}
aJSON.mAttestationObject.Assign(
NS_ConvertUTF8toUTF16(attestationObjectBase64));
}
} // namespace mozilla::dom

Просмотреть файл

@ -11,6 +11,7 @@
#include "mozilla/Attributes.h"
#include "mozilla/dom/AuthenticatorResponse.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/WebAuthenticationBinding.h"
#include "nsCycleCollectionParticipant.h"
#include "nsIWebAuthnController.h"
#include "nsWrapperCache.h"
@ -49,7 +50,12 @@ class AuthenticatorAttestationResponse final : public AuthenticatorResponse {
COSEAlgorithmIdentifier GetPublicKeyAlgorithm(ErrorResult& aRv);
void ToJSON(AuthenticatorAttestationResponseJSON& aJSON, ErrorResult& aError);
private:
nsresult GetAuthenticatorDataBytes(nsTArray<uint8_t>& aAuthenticatorData);
nsresult GetPublicKeyBytes(nsTArray<uint8_t>& aPublicKeyBytes);
nsTArray<uint8_t> mAttestationObject;
nsCOMPtr<nsIWebAuthnAttObj> mAttestationObjectParsed;
JS::Heap<JSObject*> mAttestationObjectCachedObj;

Просмотреть файл

@ -4,6 +4,7 @@
* 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/Base64.h"
#include "mozilla/dom/AuthenticatorResponse.h"
#include "mozilla/dom/TypedArray.h"
#include "nsPIDOMWindow.h"

Просмотреть файл

@ -38,9 +38,11 @@ class AuthenticatorResponse : public nsISupports, public nsWrapperCache {
void SetClientDataJSON(const nsCString& aBuffer);
protected:
nsCString mClientDataJSON;
private:
nsCOMPtr<nsPIDOMWindowInner> mParent;
nsCString mClientDataJSON;
JS::Heap<JSObject*> mClientDataJSONCachedObj;
};

Просмотреть файл

@ -4,13 +4,16 @@
* 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/Base64.h"
#include "mozilla/HoldDropJSObjects.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_security.h"
#include "mozilla/dom/AuthenticatorResponse.h"
#include "mozilla/dom/ChromeUtils.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PublicKeyCredential.h"
#include "mozilla/dom/WebAuthenticationBinding.h"
#include "nsCycleCollectionParticipant.h"
#include "mozilla/dom/AuthenticatorResponse.h"
#include "mozilla/HoldDropJSObjects.h"
#include "mozilla/StaticPrefs_security.h"
#ifdef XP_WIN
# include "WinWebAuthnManager.h"
@ -36,7 +39,8 @@ NS_IMPL_CYCLE_COLLECTION_TRACE_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PublicKeyCredential,
Credential)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResponse)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAttestationResponse)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAssertionResponse)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_ADDREF_INHERITED(PublicKeyCredential, Credential)
@ -72,16 +76,27 @@ void PublicKeyCredential::GetRawId(JSContext* aCx,
}
already_AddRefed<AuthenticatorResponse> PublicKeyCredential::Response() const {
RefPtr<AuthenticatorResponse> temp(mResponse);
return temp.forget();
if (mAttestationResponse) {
return do_AddRef(mAttestationResponse);
}
if (mAssertionResponse) {
return do_AddRef(mAssertionResponse);
}
return nullptr;
}
void PublicKeyCredential::SetRawId(const nsTArray<uint8_t>& aBuffer) {
mRawId.Assign(aBuffer);
}
void PublicKeyCredential::SetResponse(RefPtr<AuthenticatorResponse> aResponse) {
mResponse = aResponse;
void PublicKeyCredential::SetAttestationResponse(
const RefPtr<AuthenticatorAttestationResponse>& aAttestationResponse) {
mAttestationResponse = aAttestationResponse;
}
void PublicKeyCredential::SetAssertionResponse(
const RefPtr<AuthenticatorAssertionResponse>& aAssertionResponse) {
mAssertionResponse = aAssertionResponse;
}
/* static */
@ -160,6 +175,54 @@ void PublicKeyCredential::GetClientExtensionResults(
aResult = mClientExtensionOutputs;
}
void PublicKeyCredential::ToJSON(JSContext* aCx,
JS::MutableHandle<JSObject*> aRetval,
ErrorResult& aError) {
JS::Rooted<JS::Value> value(aCx);
if (mAttestationResponse) {
RegistrationResponseJSON json;
GetId(json.mId);
GetId(json.mRawId);
mAttestationResponse->ToJSON(json.mResponse, aError);
if (aError.Failed()) {
return;
}
// TODO(bug 1810851): authenticatorAttachment
if (mClientExtensionOutputs.mHmacCreateSecret.WasPassed()) {
json.mClientExtensionResults.mHmacCreateSecret.Construct(
mClientExtensionOutputs.mHmacCreateSecret.Value());
}
json.mType.Assign(u"public-key"_ns);
if (!ToJSValue(aCx, json, &value)) {
aError.StealExceptionFromJSContext(aCx);
return;
}
} else if (mAssertionResponse) {
AuthenticationResponseJSON json;
GetId(json.mId);
GetId(json.mRawId);
mAssertionResponse->ToJSON(json.mResponse, aError);
if (aError.Failed()) {
return;
}
// TODO(bug 1810851): authenticatorAttachment
if (mClientExtensionOutputs.mAppid.WasPassed()) {
json.mClientExtensionResults.mAppid.Construct(
mClientExtensionOutputs.mAppid.Value());
}
json.mType.Assign(u"public-key"_ns);
if (!ToJSValue(aCx, json, &value)) {
aError.StealExceptionFromJSContext(aCx);
return;
}
} else {
MOZ_ASSERT_UNREACHABLE(
"either mAttestationResponse or mAssertionResponse should be set");
}
JS::Rooted<JSObject*> result(aCx, &value.toObject());
aRetval.set(result);
}
void PublicKeyCredential::SetClientExtensionResultAppId(bool aResult) {
mClientExtensionOutputs.mAppid.Construct();
mClientExtensionOutputs.mAppid.Value() = aResult;
@ -171,4 +234,139 @@ void PublicKeyCredential::SetClientExtensionResultHmacSecret(
mClientExtensionOutputs.mHmacCreateSecret.Value() = aHmacCreateSecret;
}
bool Base64DecodeToArrayBuffer(GlobalObject& aGlobal, const nsAString& aString,
ArrayBuffer& aArrayBuffer, ErrorResult& aRv) {
JSContext* cx = aGlobal.Context();
JS::Rooted<JSObject*> result(cx);
Base64URLDecodeOptions options;
options.mPadding = Base64URLDecodePadding::Ignore;
mozilla::dom::ChromeUtils::Base64URLDecode(
aGlobal, NS_ConvertUTF16toUTF8(aString), options, &result, aRv);
if (aRv.Failed()) {
return false;
}
return aArrayBuffer.Init(result);
}
void PublicKeyCredential::ParseCreationOptionsFromJSON(
GlobalObject& aGlobal,
const PublicKeyCredentialCreationOptionsJSON& aOptions,
PublicKeyCredentialCreationOptions& aResult, ErrorResult& aRv) {
if (aOptions.mRp.mId.WasPassed()) {
aResult.mRp.mId.Construct(aOptions.mRp.mId.Value());
}
aResult.mRp.mName.Assign(aOptions.mRp.mName);
aResult.mUser.mName.Assign(aOptions.mUser.mName);
if (!Base64DecodeToArrayBuffer(aGlobal, aOptions.mUser.mId,
aResult.mUser.mId.SetAsArrayBuffer(), aRv)) {
aRv.ThrowEncodingError("could not decode user ID as urlsafe base64");
return;
}
aResult.mUser.mDisplayName.Assign(aOptions.mUser.mDisplayName);
if (!Base64DecodeToArrayBuffer(aGlobal, aOptions.mChallenge,
aResult.mChallenge.SetAsArrayBuffer(), aRv)) {
aRv.ThrowEncodingError("could not decode challenge as urlsafe base64");
return;
}
aResult.mPubKeyCredParams = aOptions.mPubKeyCredParams;
if (aOptions.mTimeout.WasPassed()) {
aResult.mTimeout.Construct(aOptions.mTimeout.Value());
}
for (const auto& excludeCredentialJSON : aOptions.mExcludeCredentials) {
PublicKeyCredentialDescriptor* excludeCredential =
aResult.mExcludeCredentials.AppendElement(fallible);
if (!excludeCredential) {
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
return;
}
excludeCredential->mType = excludeCredentialJSON.mType;
if (!Base64DecodeToArrayBuffer(aGlobal, excludeCredentialJSON.mId,
excludeCredential->mId.SetAsArrayBuffer(),
aRv)) {
aRv.ThrowEncodingError(
"could not decode excluded credential ID as urlsafe base64");
return;
}
if (excludeCredentialJSON.mTransports.WasPassed()) {
excludeCredential->mTransports.Construct(
excludeCredentialJSON.mTransports.Value());
}
}
if (aOptions.mAuthenticatorSelection.WasPassed()) {
aResult.mAuthenticatorSelection = aOptions.mAuthenticatorSelection.Value();
}
aResult.mAttestation = aOptions.mAttestation;
if (aOptions.mExtensions.WasPassed()) {
if (aOptions.mExtensions.Value().mAppid.WasPassed()) {
aResult.mExtensions.mAppid.Construct(
aOptions.mExtensions.Value().mAppid.Value());
}
if (aOptions.mExtensions.Value().mHmacCreateSecret.WasPassed()) {
aResult.mExtensions.mHmacCreateSecret.Construct(
aOptions.mExtensions.Value().mHmacCreateSecret.Value());
}
}
}
void PublicKeyCredential::ParseRequestOptionsFromJSON(
GlobalObject& aGlobal,
const PublicKeyCredentialRequestOptionsJSON& aOptions,
PublicKeyCredentialRequestOptions& aResult, ErrorResult& aRv) {
if (!Base64DecodeToArrayBuffer(aGlobal, aOptions.mChallenge,
aResult.mChallenge.SetAsArrayBuffer(), aRv)) {
aRv.ThrowEncodingError("could not decode challenge as urlsafe base64");
return;
}
if (aOptions.mTimeout.WasPassed()) {
aResult.mTimeout.Construct(aOptions.mTimeout.Value());
}
if (aOptions.mRpId.WasPassed()) {
aResult.mRpId.Construct(aOptions.mRpId.Value());
}
for (const auto& allowCredentialJSON : aOptions.mAllowCredentials) {
PublicKeyCredentialDescriptor* allowCredential =
aResult.mAllowCredentials.AppendElement(fallible);
if (!allowCredential) {
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
return;
}
allowCredential->mType = allowCredentialJSON.mType;
if (!Base64DecodeToArrayBuffer(aGlobal, allowCredentialJSON.mId,
allowCredential->mId.SetAsArrayBuffer(),
aRv)) {
aRv.ThrowEncodingError(
"could not decode allowed credential ID as urlsafe base64");
return;
}
if (allowCredentialJSON.mTransports.WasPassed()) {
allowCredential->mTransports.Construct(
allowCredentialJSON.mTransports.Value());
}
}
aResult.mUserVerification = aOptions.mUserVerification;
if (aOptions.mExtensions.WasPassed()) {
if (aOptions.mExtensions.Value().mAppid.WasPassed()) {
aResult.mExtensions.mAppid.Construct(
aOptions.mExtensions.Value().mAppid.Value());
}
if (aOptions.mExtensions.Value().mHmacCreateSecret.WasPassed()) {
aResult.mExtensions.mHmacCreateSecret.Construct(
aOptions.mExtensions.Value().mHmacCreateSecret.Value());
}
}
}
} // namespace mozilla::dom

Просмотреть файл

@ -9,6 +9,8 @@
#include "js/TypeDecls.h"
#include "mozilla/Attributes.h"
#include "mozilla/dom/AuthenticatorAssertionResponse.h"
#include "mozilla/dom/AuthenticatorAttestationResponse.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/Credential.h"
#include "nsCycleCollectionParticipant.h"
@ -38,7 +40,10 @@ class PublicKeyCredential final : public Credential {
void SetRawId(const nsTArray<uint8_t>& aBuffer);
void SetResponse(RefPtr<AuthenticatorResponse>);
void SetAttestationResponse(
const RefPtr<AuthenticatorAttestationResponse>& aAttestationResponse);
void SetAssertionResponse(
const RefPtr<AuthenticatorAssertionResponse>& aAssertionResponse);
static already_AddRefed<Promise>
IsUserVerifyingPlatformAuthenticatorAvailable(GlobalObject& aGlobal,
@ -50,14 +55,28 @@ class PublicKeyCredential final : public Credential {
void GetClientExtensionResults(
AuthenticationExtensionsClientOutputs& aResult);
void ToJSON(JSContext* aCx, JS::MutableHandle<JSObject*> aRetval,
ErrorResult& aError);
void SetClientExtensionResultAppId(bool aResult);
void SetClientExtensionResultHmacSecret(bool aHmacCreateSecret);
static void ParseCreationOptionsFromJSON(
GlobalObject& aGlobal,
const PublicKeyCredentialCreationOptionsJSON& aOptions,
PublicKeyCredentialCreationOptions& aResult, ErrorResult& aRv);
static void ParseRequestOptionsFromJSON(
GlobalObject& aGlobal,
const PublicKeyCredentialRequestOptionsJSON& aOptions,
PublicKeyCredentialRequestOptions& aResult, ErrorResult& aRv);
private:
nsTArray<uint8_t> mRawId;
JS::Heap<JSObject*> mRawIdCachedObj;
RefPtr<AuthenticatorResponse> mResponse;
RefPtr<AuthenticatorAttestationResponse> mAttestationResponse;
RefPtr<AuthenticatorAssertionResponse> mAssertionResponse;
AuthenticationExtensionsClientOutputs mClientExtensionOutputs;
};

Просмотреть файл

@ -717,7 +717,7 @@ void WebAuthnManager::FinishMakeCredential(
credential->SetId(NS_ConvertASCIItoUTF16(keyHandleBase64Url));
credential->SetType(u"public-key"_ns);
credential->SetRawId(aResult.KeyHandle());
credential->SetResponse(attestation);
credential->SetAttestationResponse(attestation);
// Forward client extension results.
for (const auto& ext : aResult.Extensions()) {
@ -765,7 +765,7 @@ void WebAuthnManager::FinishGetAssertion(
credential->SetId(NS_ConvertASCIItoUTF16(keyHandleBase64Url));
credential->SetType(u"public-key"_ns);
credential->SetRawId(aResult.KeyHandle());
credential->SetResponse(assertion);
credential->SetAssertionResponse(assertion);
// Forward client extension results.
for (const auto& ext : aResult.Extensions()) {

Просмотреть файл

@ -88,6 +88,8 @@ skip-if =
win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
win10_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
[test_webauthn_serialization.html]
fail-if = xorigin
[test_webauthn_store_credential.html]
fail-if = xorigin # NotAllowedError
skip-if =

Просмотреть файл

@ -0,0 +1,297 @@
<!DOCTYPE html>
<meta charset=utf-8>
<head>
<title>Tests W3C Web Authentication Data Types Serialization</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="u2futil.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<h1>Tests W3C Web Authentication Data Types Serialization</h1>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1823782">Mozilla Bug 1823782</a>
<script class="testbody" type="text/javascript">
"use strict";
const { Assert } = SpecialPowers.ChromeUtils.importESModule(
"resource://testing-common/Assert.sys.mjs"
);
function arrayBufferEqualsArray(actual, expected, description) {
ok(actual instanceof ArrayBuffer, `${description} (actual should be array)`);
ok(expected instanceof Array, `${description} (expected should be array)`);
is(actual.byteLength, expected.length, `${description} (actual and expected should have same length)`);
let actualView = new Uint8Array(actual);
for (let i = 0; i < actualView.length; i++) {
if (actualView[i] != expected[i]) {
throw new Error(`actual and expected differ in byte ${i}: ${actualView[i]} vs ${expected[i]}`);
}
}
ok(true, description);
}
function isEmptyArray(arr, description) {
ok(arr instanceof Array, `${description} (expecting Array)`);
is(arr.length, 0, `${description} (array should be empty)`);
}
function shouldThrow(func, expectedError, description) {
let threw = false;
try {
func();
} catch (e) {
is(e.message, expectedError);
threw = true;
}
ok(threw, description);
}
add_task(function test_parseCreationOptionsFromJSON_minimal() {
let creationOptionsJSON = {
rp: { name: "Example" },
user: { id: "-l3qNLTKJng", name: "username", displayName: "display name" },
challenge: "XNJTTB3kfqk",
pubKeyCredParams: [],
};
let creationOptions = PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);
is(Object.getOwnPropertyNames(creationOptions).length, 8, "creation options should have 8 properties");
is(creationOptions.rp.id, undefined, "rp.id should be undefined");
is(creationOptions.rp.name, "Example", "rp.name should be Example");
arrayBufferEqualsArray(creationOptions.user.id, [ 250, 93, 234, 52, 180, 202, 38, 120 ], "user.id should be as expected");
is(creationOptions.user.displayName, "display name", "user.displayName should be 'display name'");
is(creationOptions.user.name, "username", "user.name should be username");
arrayBufferEqualsArray(creationOptions.challenge, [ 92, 210, 83, 76, 29, 228, 126, 169 ], "challenge should be as expected");
isEmptyArray(creationOptions.pubKeyCredParams, "pubKeyCredParams should be an empty array");
is(creationOptions.timeout, undefined, "timeout should be undefined");
isEmptyArray(creationOptions.excludeCredentials, "excludeCredentials should be an empty array");
is(creationOptions.authenticatorSelection.authenticatorAttachment, undefined, "authenticatorSelection.authenticatorAttachment should be undefined");
is(creationOptions.authenticatorSelection.residentKey, undefined, "creationOptions.authenticatorSelection.residentKey should be undefined");
is(creationOptions.authenticatorSelection.requireResidentKey, false, "creationOptions.authenticatorSelection.requireResidentKey should be false");
is(creationOptions.authenticatorSelection.userVerification, "preferred", "creationOptions.authenticatorSelection.userVerification should be preferred");
is(creationOptions.attestation, "none", "attestation should be none");
is(Object.getOwnPropertyNames(creationOptions.extensions).length, 0, "extensions should be an empty object");
});
add_task(function test_parseCreationOptionsFromJSON() {
let creationOptionsJSON = {
rp: { name: "Example", id: "example.com" },
user: { id: "19TVpqBBOAM", name: "username2", displayName: "another display name" },
challenge: "dR82FeUh5q4",
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
timeout: 20000,
excludeCredentials: [{ type: "public-key", id: "TeM2k_di7Dk", transports: [ "usb" ]}],
authenticatorSelection: { authenticatorAttachment: "platform", residentKey: "required", requireResidentKey: true, userVerification: "discouraged" },
hints: ["hybrid"],
attestation: "indirect",
attestationFormats: ["fido-u2f"],
extensions: { appid: "https://www.example.com/appID", hmacCreateSecret: true },
};
let creationOptions = PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);
is(Object.getOwnPropertyNames(creationOptions).length, 9, "creation options should have 9 properties");
is(creationOptions.rp.name, "Example", "rp.name should be Example");
is(creationOptions.rp.id, "example.com", "rp.id should be example.com");
arrayBufferEqualsArray(creationOptions.user.id, [ 215, 212, 213, 166, 160, 65, 56, 3 ], "user.id should be as expected");
is(creationOptions.user.displayName, "another display name", "user.displayName should be 'another display name'");
is(creationOptions.user.name, "username2", "user.name should be username2");
arrayBufferEqualsArray(creationOptions.challenge, [ 117, 31, 54, 21, 229, 33, 230, 174 ], "challenge should be as expected");
is(creationOptions.pubKeyCredParams.length, 1, "pubKeyCredParams should have one element");
is(creationOptions.pubKeyCredParams[0].type, "public-key", "pubKeyCredParams[0].type should be public-key");
is(creationOptions.pubKeyCredParams[0].alg, -7, "pubKeyCredParams[0].alg should be -7");
is(creationOptions.timeout, 20000, "timeout should be 20000");
is(creationOptions.excludeCredentials.length, 1, "excludeCredentials should have one element");
is(creationOptions.excludeCredentials[0].type, "public-key", "excludeCredentials[0].type should be public-key");
arrayBufferEqualsArray(creationOptions.excludeCredentials[0].id, [ 77, 227, 54, 147, 247, 98, 236, 57 ], "excludeCredentials[0].id should be as expected");
is(creationOptions.excludeCredentials[0].transports.length, 1, "excludeCredentials[0].transports should have one element");
is(creationOptions.excludeCredentials[0].transports[0], "usb", "excludeCredentials[0].transports[0] should be usb");
is(creationOptions.authenticatorSelection.authenticatorAttachment, "platform", "authenticatorSelection.authenticatorAttachment should be platform");
is(creationOptions.authenticatorSelection.residentKey, "required", "creationOptions.authenticatorSelection.residentKey should be required");
is(creationOptions.authenticatorSelection.requireResidentKey, true, "creationOptions.authenticatorSelection.requireResidentKey should be true");
is(creationOptions.authenticatorSelection.userVerification, "discouraged", "creationOptions.authenticatorSelection.userVerification should be discouraged");
is(creationOptions.attestation, "indirect", "attestation should be indirect");
is(creationOptions.extensions.appid, "https://www.example.com/appID", "extensions.appid should be https://www.example.com/appID");
is(creationOptions.extensions.hmacCreateSecret, true, "extensions.hmacCreateSecret should be true");
});
add_task(function test_parseCreationOptionsFromJSON_malformed() {
let userIdNotBase64 = {
rp: { name: "Example" },
user: { id: "/not urlsafe base64+", name: "username", displayName: "display name" },
challenge: "XNJTTB3kfqk",
pubKeyCredParams: [],
};
shouldThrow(
() => { PublicKeyCredential.parseCreationOptionsFromJSON(userIdNotBase64); },
"PublicKeyCredential.parseCreationOptionsFromJSON: could not decode user ID as urlsafe base64",
"should get encoding error if user.id is not urlsafe base64"
);
let challengeNotBase64 = {
rp: { name: "Example" },
user: { id: "-l3qNLTKJng", name: "username", displayName: "display name" },
challenge: "this is not urlsafe base64!",
pubKeyCredParams: [],
};
shouldThrow(
() => { PublicKeyCredential.parseCreationOptionsFromJSON(challengeNotBase64); },
"PublicKeyCredential.parseCreationOptionsFromJSON: could not decode challenge as urlsafe base64",
"should get encoding error if challenge is not urlsafe base64"
);
let excludeCredentialsIdNotBase64 = {
rp: { name: "Example", id: "example.com" },
user: { id: "-l3qNLTKJng", name: "username", displayName: "display name" },
challenge: "dR82FeUh5q4",
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
timeout: 20000,
excludeCredentials: [{ type: "public-key", id: "@#$%&^", transports: [ "usb" ]}],
authenticatorselection: { authenticatorattachment: "platform", residentkey: "required", requireresidentkey: true, userverification: "discouraged" },
hints: ["hybrid"],
attestation: "indirect",
attestationformats: ["fido-u2f"],
extensions: { appid: "https://www.example.com/appid", hmaccreatesecret: true },
};
shouldThrow(
() => { PublicKeyCredential.parseCreationOptionsFromJSON(excludeCredentialsIdNotBase64); },
"PublicKeyCredential.parseCreationOptionsFromJSON: could not decode excluded credential ID as urlsafe base64",
"should get encoding error if excludeCredentials[0].id is not urlsafe base64"
);
});
add_task(function test_parseRequestOptionsFromJSON_minimal() {
let requestOptionsJSON = {
challenge: "3yW2WHD_jbU",
};
let requestOptions = PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJSON);
is(Object.getOwnPropertyNames(requestOptions).length, 4, "request options should have 4 properties");
arrayBufferEqualsArray(requestOptions.challenge, [ 223, 37, 182, 88, 112, 255, 141, 181 ], "challenge should be as expected");
is(requestOptions.timeout, undefined, "timeout should be undefined");
is(requestOptions.rpId, undefined, "rpId should be undefined");
isEmptyArray(requestOptions.allowCredentials, "allowCredentials should be an empty array");
is(requestOptions.userVerification, "preferred", "userVerification should be preferred");
is(Object.getOwnPropertyNames(requestOptions.extensions).length, 0, "extensions should be an empty object");
});
add_task(function test_parseRequestOptionsFromJSON() {
let requestOptionsJSON = {
challenge: "QAfaZwEQCkQ",
timeout: 25000,
rpId: "example.com",
allowCredentials: [{type: "public-key", id: "BTBXXGuXRTk", transports: ["smart-card"] }],
userVerification: "discouraged",
hints: ["client-device"],
attestation: "enterprise",
attestationFormats: ["packed"],
extensions: { appid: "https://www.example.com/anotherAppID", hmacCreateSecret: false },
};
let requestOptions = PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJSON);
is(Object.getOwnPropertyNames(requestOptions).length, 6, "request options should have 6 properties");
arrayBufferEqualsArray(requestOptions.challenge, [ 64, 7, 218, 103, 1, 16, 10, 68 ], "challenge should be as expected");
is(requestOptions.timeout, 25000, "timeout should be 25000");
is(requestOptions.rpId, "example.com", "rpId should be example.com");
is(requestOptions.allowCredentials.length, 1, "allowCredentials should have one element");
is(requestOptions.allowCredentials[0].type, "public-key", "allowCredentials[0].type should be public-key");
arrayBufferEqualsArray(requestOptions.allowCredentials[0].id, [ 5, 48, 87, 92, 107, 151, 69, 57 ], "allowCredentials[0].id should be as expected");
is(requestOptions.allowCredentials[0].transports.length, 1, "allowCredentials[0].transports should have one element");
is(requestOptions.allowCredentials[0].transports[0], "smart-card", "allowCredentials[0].transports[0] should be usb");
is(requestOptions.userVerification, "discouraged", "userVerification should be discouraged");
is(requestOptions.extensions.appid, "https://www.example.com/anotherAppID", "extensions.appid should be https://www.example.com/anotherAppID");
is(requestOptions.extensions.hmacCreateSecret, false, "extensions.hmacCreateSecret should be false");
});
add_task(function test_parseRequestOptionsFromJSON_malformed() {
let challengeNotBase64 = {
challenge: "/not+urlsafe+base64/",
};
shouldThrow(
() => { PublicKeyCredential.parseRequestOptionsFromJSON(challengeNotBase64); },
"PublicKeyCredential.parseRequestOptionsFromJSON: could not decode challenge as urlsafe base64",
"should get encoding error if challenge is not urlsafe base64"
);
let allowCredentialsIdNotBase64 = {
challenge: "QAfaZwEQCkQ",
timeout: 25000,
rpId: "example.com",
allowCredentials: [{type: "public-key", id: "not urlsafe base64", transports: ["smart-card"] }],
userVerification: "discouraged",
hints: ["client-device"],
attestation: "enterprise",
attestationFormats: ["packed"],
extensions: { appid: "https://www.example.com/anotherAppID", hmacCreateSecret: false },
};
shouldThrow(
() => { PublicKeyCredential.parseRequestOptionsFromJSON(allowCredentialsIdNotBase64); },
"PublicKeyCredential.parseRequestOptionsFromJSON: could not decode allowed credential ID as urlsafe base64",
"should get encoding error if allowCredentials[0].id is not urlsafe base64"
);
});
add_task(async () => {
await addVirtualAuthenticator();
});
function isUrlsafeBase64(urlsafeBase64) {
try {
atob(urlsafeBase64.replace(/_/g, "/").replace(/-/g, "+"));
return true;
} catch (_) {}
return false;
}
add_task(async function test_registrationResponse_toJSON() {
let publicKey = {
rp: {id: document.domain, name: "none", icon: "none"},
user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
challenge: crypto.getRandomValues(new Uint8Array(16)),
pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
};
let registrationResponse = await navigator.credentials.create({publicKey});
let registrationResponseJSON = registrationResponse.toJSON();
is(Object.keys(registrationResponseJSON).length, 5, "registrationResponseJSON should have 5 properties");
is(registrationResponseJSON.id, registrationResponseJSON.rawId, "registrationResponseJSON.id and rawId should be the same");
ok(isUrlsafeBase64(registrationResponseJSON.id), "registrationResponseJSON.id should be urlsafe base64");
is(Object.keys(registrationResponseJSON.response).length, 6, "registrationResponseJSON.response should have 6 properties");
ok(isUrlsafeBase64(registrationResponseJSON.response.clientDataJSON), "registrationResponseJSON.response.clientDataJSON should be urlsafe base64");
ok(isUrlsafeBase64(registrationResponseJSON.response.authenticatorData), "registrationResponseJSON.response.authenticatorData should be urlsafe base64");
ok(isUrlsafeBase64(registrationResponseJSON.response.publicKey), "registrationResponseJSON.response.publicKey should be urlsafe base64");
ok(isUrlsafeBase64(registrationResponseJSON.response.attestationObject), "registrationResponseJSON.response.attestationObject should be urlsafe base64");
is(registrationResponseJSON.response.publicKeyAlgorithm, cose_alg_ECDSA_w_SHA256, "registrationResponseJSON.response.publicKeyAlgorithm should be ECDSA with SHA256 (COSE)");
is(registrationResponseJSON.response.transports.length, 1, "registrationResponseJSON.response.transports.length should be 1");
is(registrationResponseJSON.response.transports[0], "usb", "registrationResponseJSON.response.transports[0] should be usb");
is(registrationResponseJSON.type, "public-key", "registrationResponseJSON.type should be public-key");
});
add_task(async function test_assertionResponse_toJSON() {
let registrationRequest = {
publicKey: {
rp: {id: document.domain, name: "none", icon: "none"},
user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
challenge: crypto.getRandomValues(new Uint8Array(16)),
pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
},
};
let registrationResponse = await navigator.credentials.create(registrationRequest);
let assertionRequest = {
publicKey: {
challenge: crypto.getRandomValues(new Uint8Array(16)),
allowCredentials: [{ type: "public-key", id: registrationResponse.rawId }],
},
};
let assertionResponse = await navigator.credentials.get(assertionRequest);
let assertionResponseJSON = assertionResponse.toJSON();
is(Object.keys(assertionResponseJSON).length, 5, "assertionResponseJSON should have 5 properties");
is(assertionResponseJSON.id, assertionResponseJSON.rawId, "assertionResponseJSON.id and rawId should be the same");
ok(isUrlsafeBase64(assertionResponseJSON.id), "assertionResponseJSON.id should be urlsafe base64");
is(Object.keys(assertionResponseJSON.response).length, 3, "assertionResponseJSON.response should have 3 properties");
ok(isUrlsafeBase64(assertionResponseJSON.response.clientDataJSON), "assertionResponseJSON.response.clientDataJSON should be urlsafe base64");
ok(isUrlsafeBase64(assertionResponseJSON.response.authenticatorData), "assertionResponseJSON.response.authenticatorData should be urlsafe base64");
ok(isUrlsafeBase64(assertionResponseJSON.response.signature), "assertionResponseJSON.response.signature should be urlsafe base64");
is(Object.keys(assertionResponseJSON.clientExtensionResults).length, 0, "assertionResponseJSON.clientExtensionResults should be an empty dictionary");
is(assertionResponseJSON.type, "public-key", "assertionResponseJSON.type should be public-key");
});
</script>
</body>
</html>

Просмотреть файл

@ -15,6 +15,66 @@ interface PublicKeyCredential : Credential {
[SameObject, Throws] readonly attribute ArrayBuffer rawId;
[SameObject] readonly attribute AuthenticatorResponse response;
AuthenticationExtensionsClientOutputs getClientExtensionResults();
[Throws] object toJSON();
};
typedef DOMString Base64URLString;
[GenerateConversionToJS]
dictionary RegistrationResponseJSON {
required Base64URLString id;
required Base64URLString rawId;
required AuthenticatorAttestationResponseJSON response;
DOMString authenticatorAttachment;
required AuthenticationExtensionsClientOutputsJSON clientExtensionResults;
required DOMString type;
};
[GenerateConversionToJS]
dictionary AuthenticatorAttestationResponseJSON {
required Base64URLString clientDataJSON;
required Base64URLString authenticatorData;
required sequence<DOMString> transports;
// The publicKey field will be missing if pubKeyCredParams was used to
// negotiate a public-key algorithm that the user agent doesnt
// understand. (See section “Easily accessing credential data” for a
// list of which algorithms user agents must support.) If using such an
// algorithm then the public key must be parsed directly from
// attestationObject or authenticatorData.
Base64URLString publicKey;
required long long publicKeyAlgorithm;
// This value contains copies of some of the fields above. See
// section “Easily accessing credential data”.
required Base64URLString attestationObject;
};
[GenerateConversionToJS]
dictionary AuthenticationResponseJSON {
required Base64URLString id;
required Base64URLString rawId;
required AuthenticatorAssertionResponseJSON response;
DOMString authenticatorAttachment;
required AuthenticationExtensionsClientOutputsJSON clientExtensionResults;
required DOMString type;
};
[GenerateConversionToJS]
dictionary AuthenticatorAssertionResponseJSON {
required Base64URLString clientDataJSON;
required Base64URLString authenticatorData;
required Base64URLString signature;
Base64URLString userHandle;
Base64URLString attestationObject;
};
[GenerateConversionToJS]
dictionary AuthenticationExtensionsClientOutputsJSON {
// FIDO AppID Extension (appid)
// <https://w3c.github.io/webauthn/#sctn-appid-extension>
boolean appid;
// <https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#sctn-hmac-secret-extension>
boolean hmacCreateSecret;
};
[SecureContext]
@ -24,6 +84,64 @@ partial interface PublicKeyCredential {
[NewObject] static Promise<boolean> isExternalCTAP2SecurityKeySupported();
};
[SecureContext]
partial interface PublicKeyCredential {
[Throws] static PublicKeyCredentialCreationOptions parseCreationOptionsFromJSON(PublicKeyCredentialCreationOptionsJSON options);
};
dictionary PublicKeyCredentialCreationOptionsJSON {
required PublicKeyCredentialRpEntity rp;
required PublicKeyCredentialUserEntityJSON user;
required Base64URLString challenge;
required sequence<PublicKeyCredentialParameters> pubKeyCredParams;
unsigned long timeout;
sequence<PublicKeyCredentialDescriptorJSON> excludeCredentials = [];
AuthenticatorSelectionCriteria authenticatorSelection;
sequence<DOMString> hints = [];
DOMString attestation = "none";
sequence<DOMString> attestationFormats = [];
AuthenticationExtensionsClientInputsJSON extensions;
};
dictionary PublicKeyCredentialUserEntityJSON {
required Base64URLString id;
required DOMString name;
required DOMString displayName;
};
dictionary PublicKeyCredentialDescriptorJSON {
required Base64URLString id;
required DOMString type;
sequence<DOMString> transports;
};
dictionary AuthenticationExtensionsClientInputsJSON {
// FIDO AppID Extension (appid)
// <https://w3c.github.io/webauthn/#sctn-appid-extension>
USVString appid;
// hmac-secret
// <https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#sctn-hmac-secret-extension>
boolean hmacCreateSecret;
};
[SecureContext]
partial interface PublicKeyCredential {
[Throws] static PublicKeyCredentialRequestOptions parseRequestOptionsFromJSON(PublicKeyCredentialRequestOptionsJSON options);
};
dictionary PublicKeyCredentialRequestOptionsJSON {
required Base64URLString challenge;
unsigned long timeout;
DOMString rpId;
sequence<PublicKeyCredentialDescriptorJSON> allowCredentials = [];
DOMString userVerification = "preferred";
sequence<DOMString> hints = [];
DOMString attestation = "none";
sequence<DOMString> attestationFormats = [];
AuthenticationExtensionsClientInputsJSON extensions;
};
[SecureContext, Pref="security.webauth.webauthn",
Exposed=Window]
interface AuthenticatorResponse {