diff options
Diffstat (limited to 'dom/webauthn')
92 files changed, 33718 insertions, 0 deletions
diff --git a/dom/webauthn/AndroidWebAuthnService.cpp b/dom/webauthn/AndroidWebAuthnService.cpp new file mode 100644 index 0000000000..162cc52033 --- /dev/null +++ b/dom/webauthn/AndroidWebAuthnService.cpp @@ -0,0 +1,428 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/StaticPtr.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/jni/GeckoBundleUtils.h" + +#include "AndroidWebAuthnService.h" +#include "JavaBuiltins.h" +#include "JavaExceptions.h" +#include "WebAuthnPromiseHolder.h" +#include "WebAuthnEnumStrings.h" +#include "WebAuthnResult.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/java/WebAuthnTokenManagerWrappers.h" +#include "mozilla/jni/Conversions.h" + +namespace mozilla { +namespace jni { +template <> +dom::AndroidWebAuthnError Java2Native(mozilla::jni::Object::Param aData, + JNIEnv* aEnv) { + MOZ_ASSERT(aData.IsInstanceOf<jni::Throwable>()); + java::sdk::Throwable::LocalRef throwable(aData); + return dom::AndroidWebAuthnError(throwable->GetMessage()->ToString()); +} +} // namespace jni + +namespace dom { + +NS_IMPL_ISUPPORTS(AndroidWebAuthnService, nsIWebAuthnService) + +NS_IMETHODIMP +AndroidWebAuthnService::GetIsUVPAA(bool* aAvailable) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::MakeCredential(uint64_t aTransactionId, + uint64_t aBrowsingContextId, + nsIWebAuthnRegisterArgs* aArgs, + nsIWebAuthnRegisterPromise* aPromise) { + Reset(); + + GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( + "java::WebAuthnTokenManager::WebAuthnMakeCredential", + [aArgs = RefPtr{aArgs}, aPromise = RefPtr{aPromise}]() { + AssertIsOnMainThread(); + + GECKOBUNDLE_START(credentialBundle); + GECKOBUNDLE_PUT(credentialBundle, "isWebAuthn", + java::sdk::Integer::ValueOf(1)); + + nsString rpId; + Unused << aArgs->GetRpId(rpId); + GECKOBUNDLE_PUT(credentialBundle, "rpId", jni::StringParam(rpId)); + + nsString rpName; + Unused << aArgs->GetRpName(rpName); + GECKOBUNDLE_PUT(credentialBundle, "rpName", jni::StringParam(rpName)); + + nsString userName; + Unused << aArgs->GetUserName(userName); + GECKOBUNDLE_PUT(credentialBundle, "userName", + jni::StringParam(userName)); + + nsString userDisplayName; + Unused << aArgs->GetUserDisplayName(userDisplayName); + GECKOBUNDLE_PUT(credentialBundle, "userDisplayName", + jni::StringParam(userDisplayName)); + + nsString origin; + Unused << aArgs->GetOrigin(origin); + GECKOBUNDLE_PUT(credentialBundle, "origin", jni::StringParam(origin)); + + uint32_t timeout; + Unused << aArgs->GetTimeoutMS(&timeout); + GECKOBUNDLE_PUT(credentialBundle, "timeoutMS", + java::sdk::Double::New(timeout)); + GECKOBUNDLE_FINISH(credentialBundle); + + nsTArray<uint8_t> userId; + Unused << aArgs->GetUserId(userId); + jni::ByteBuffer::LocalRef uid = jni::ByteBuffer::New( + const_cast<void*>(static_cast<const void*>(userId.Elements())), + userId.Length()); + + nsTArray<uint8_t> challBuf; + Unused << aArgs->GetChallenge(challBuf); + jni::ByteBuffer::LocalRef challenge = jni::ByteBuffer::New( + const_cast<void*>(static_cast<const void*>(challBuf.Elements())), + challBuf.Length()); + + nsTArray<nsTArray<uint8_t>> excludeList; + Unused << aArgs->GetExcludeList(excludeList); + jni::ObjectArray::LocalRef idList = + jni::ObjectArray::New(excludeList.Length()); + int ix = 0; + for (const nsTArray<uint8_t>& credId : excludeList) { + jni::ByteBuffer::LocalRef id = jni::ByteBuffer::New( + const_cast<void*>(static_cast<const void*>(credId.Elements())), + credId.Length()); + + idList->SetElement(ix, id); + + ix += 1; + } + + nsTArray<uint8_t> transportBuf; + Unused << aArgs->GetExcludeListTransports(transportBuf); + jni::ByteBuffer::LocalRef transportList = jni::ByteBuffer::New( + const_cast<void*>( + static_cast<const void*>(transportBuf.Elements())), + transportBuf.Length()); + + GECKOBUNDLE_START(authSelBundle); + // Add UI support to consent to attestation, bug 1550164 + GECKOBUNDLE_PUT(authSelBundle, "attestationPreference", + jni::StringParam(u"none"_ns)); + + nsString residentKey; + Unused << aArgs->GetResidentKey(residentKey); + + // Get extensions + bool requestedCredProps; + Unused << aArgs->GetCredProps(&requestedCredProps); + + // Unfortunately, GMS's FIDO2 API has no option for Passkey. If using + // residentKey, credential will be synced with Passkey via Google + // account or credential provider service. So this is experimental. + Maybe<bool> credPropsResponse; + if (requestedCredProps && + StaticPrefs:: + security_webauthn_webauthn_enable_android_fido2_residentkey()) { + GECKOBUNDLE_PUT(authSelBundle, "residentKey", + jni::StringParam(residentKey)); + bool residentKeyRequired = residentKey.EqualsLiteral( + MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED); + credPropsResponse = Some(residentKeyRequired); + } + + nsString userVerification; + Unused << aArgs->GetUserVerification(userVerification); + if (userVerification.EqualsLiteral( + MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED)) { + GECKOBUNDLE_PUT(authSelBundle, "requireUserVerification", + java::sdk::Integer::ValueOf(1)); + } + + nsString authenticatorAttachment; + nsresult rv = + aArgs->GetAuthenticatorAttachment(authenticatorAttachment); + if (rv != NS_ERROR_NOT_AVAILABLE) { + if (NS_FAILED(rv)) { + aPromise->Reject(rv); + return; + } + if (authenticatorAttachment.EqualsLiteral( + MOZ_WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM)) { + GECKOBUNDLE_PUT(authSelBundle, "requirePlatformAttachment", + java::sdk::Integer::ValueOf(1)); + } else if ( + authenticatorAttachment.EqualsLiteral( + MOZ_WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM)) { + GECKOBUNDLE_PUT(authSelBundle, "requireCrossPlatformAttachment", + java::sdk::Integer::ValueOf(1)); + } + } + GECKOBUNDLE_FINISH(authSelBundle); + + GECKOBUNDLE_START(extensionsBundle); + GECKOBUNDLE_FINISH(extensionsBundle); + + auto result = java::WebAuthnTokenManager::WebAuthnMakeCredential( + credentialBundle, uid, challenge, idList, transportList, + authSelBundle, extensionsBundle); + + auto geckoResult = java::GeckoResult::LocalRef(std::move(result)); + + MozPromise<RefPtr<WebAuthnRegisterResult>, AndroidWebAuthnError, + true>::FromGeckoResult(geckoResult) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aPromise, credPropsResponse = std::move(credPropsResponse)]( + RefPtr<WebAuthnRegisterResult>&& aValue) { + // We don't have a way for the user to consent to attestation + // on Android, so always anonymize the result. + nsresult rv = aValue->Anonymize(); + if (NS_FAILED(rv)) { + aPromise->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR); + } + if (credPropsResponse.isSome()) { + Unused << aValue->SetCredPropsRk(credPropsResponse.ref()); + } + aPromise->Resolve(aValue); + }, + [aPromise](AndroidWebAuthnError&& aValue) { + aPromise->Reject(aValue.GetError()); + }); + })); + + return NS_OK; +} + +NS_IMETHODIMP +AndroidWebAuthnService::GetAssertion(uint64_t aTransactionId, + uint64_t aBrowsingContextId, + nsIWebAuthnSignArgs* aArgs, + nsIWebAuthnSignPromise* aPromise) { + Reset(); + + bool conditionallyMediated; + Unused << aArgs->GetConditionallyMediated(&conditionallyMediated); + if (conditionallyMediated) { + aPromise->Reject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return NS_OK; + } + + GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( + "java::WebAuthnTokenManager::WebAuthnGetAssertion", + [self = RefPtr{this}, aArgs = RefPtr{aArgs}, + aPromise = RefPtr{aPromise}]() { + AssertIsOnMainThread(); + + nsTArray<uint8_t> challBuf; + Unused << aArgs->GetChallenge(challBuf); + jni::ByteBuffer::LocalRef challenge = jni::ByteBuffer::New( + const_cast<void*>(static_cast<const void*>(challBuf.Elements())), + challBuf.Length()); + + nsTArray<nsTArray<uint8_t>> allowList; + Unused << aArgs->GetAllowList(allowList); + jni::ObjectArray::LocalRef idList = + jni::ObjectArray::New(allowList.Length()); + int ix = 0; + for (const nsTArray<uint8_t>& credId : allowList) { + jni::ByteBuffer::LocalRef id = jni::ByteBuffer::New( + const_cast<void*>(static_cast<const void*>(credId.Elements())), + credId.Length()); + + idList->SetElement(ix, id); + + ix += 1; + } + + nsTArray<uint8_t> transportBuf; + Unused << aArgs->GetAllowListTransports(transportBuf); + jni::ByteBuffer::LocalRef transportList = jni::ByteBuffer::New( + const_cast<void*>( + static_cast<const void*>(transportBuf.Elements())), + transportBuf.Length()); + + GECKOBUNDLE_START(assertionBundle); + + GECKOBUNDLE_PUT(assertionBundle, "isWebAuthn", + java::sdk::Integer::ValueOf(1)); + + nsString rpId; + Unused << aArgs->GetRpId(rpId); + GECKOBUNDLE_PUT(assertionBundle, "rpId", jni::StringParam(rpId)); + + nsString origin; + Unused << aArgs->GetOrigin(origin); + GECKOBUNDLE_PUT(assertionBundle, "origin", jni::StringParam(origin)); + + uint32_t timeout; + Unused << aArgs->GetTimeoutMS(&timeout); + GECKOBUNDLE_PUT(assertionBundle, "timeoutMS", + java::sdk::Double::New(timeout)); + + // User Verification Requirement is not currently used in the + // Android FIDO API. + + GECKOBUNDLE_FINISH(assertionBundle); + + GECKOBUNDLE_START(extensionsBundle); + + nsString appId; + nsresult rv = aArgs->GetAppId(appId); + if (rv != NS_ERROR_NOT_AVAILABLE) { + if (NS_FAILED(rv)) { + aPromise->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + GECKOBUNDLE_PUT(extensionsBundle, "fidoAppId", + jni::StringParam(appId)); + } + + GECKOBUNDLE_FINISH(extensionsBundle); + + auto result = java::WebAuthnTokenManager::WebAuthnGetAssertion( + challenge, idList, transportList, assertionBundle, + extensionsBundle); + auto geckoResult = java::GeckoResult::LocalRef(std::move(result)); + MozPromise<RefPtr<WebAuthnSignResult>, AndroidWebAuthnError, + true>::FromGeckoResult(geckoResult) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aPromise](RefPtr<WebAuthnSignResult>&& aValue) { + aPromise->Resolve(aValue); + }, + [aPromise](AndroidWebAuthnError&& aValue) { + aPromise->Reject(aValue.GetError()); + }); + })); + + return NS_OK; +} + +NS_IMETHODIMP +AndroidWebAuthnService::Reset() { + mRegisterCredPropsRk.reset(); + + return NS_OK; +} + +NS_IMETHODIMP +AndroidWebAuthnService::Cancel(uint64_t aTransactionId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::HasPendingConditionalGet(uint64_t aBrowsingContextId, + const nsAString& aOrigin, + uint64_t* aRv) { + // Signal that there is no pending conditional get request, so the caller + // will not attempt to call GetAutoFillEntries, SelectAutoFillEntry, or + // ResumeConditionalGet (as these are not implemented). + *aRv = 0; + return NS_OK; +} + +NS_IMETHODIMP +AndroidWebAuthnService::GetAutoFillEntries( + uint64_t aTransactionId, nsTArray<RefPtr<nsIWebAuthnAutoFillEntry>>& aRv) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::SelectAutoFillEntry( + uint64_t aTransactionId, const nsTArray<uint8_t>& aCredentialId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::ResumeConditionalGet(uint64_t aTransactionId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::PinCallback(uint64_t aTransactionId, + const nsACString& aPin) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::ResumeMakeCredential(uint64_t aTransactionId, + bool aForceNoneAttestation) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::SelectionCallback(uint64_t aTransactionId, + uint64_t aIndex) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::AddVirtualAuthenticator( + const nsACString& protocol, const nsACString& transport, + bool hasResidentKey, bool hasUserVerification, bool isUserConsenting, + bool isUserVerified, uint64_t* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::RemoveVirtualAuthenticator(uint64_t authenticatorId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::AddCredential(uint64_t authenticatorId, + const nsACString& credentialId, + bool isResidentCredential, + const nsACString& rpId, + const nsACString& privateKey, + const nsACString& userHandle, + uint32_t signCount) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::GetCredentials( + uint64_t authenticatorId, + nsTArray<RefPtr<nsICredentialParameters>>& _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::RemoveCredential(uint64_t authenticatorId, + const nsACString& credentialId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::RemoveAllCredentials(uint64_t authenticatorId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::SetUserVerified(uint64_t authenticatorId, + bool isUserVerified) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AndroidWebAuthnService::Listen() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +AndroidWebAuthnService::RunCommand(const nsACString& cmd) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/webauthn/AndroidWebAuthnService.h b/dom/webauthn/AndroidWebAuthnService.h new file mode 100644 index 0000000000..ab48be4867 --- /dev/null +++ b/dom/webauthn/AndroidWebAuthnService.h @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_AndroidWebAuthnService_h_ +#define mozilla_dom_AndroidWebAuthnService_h_ + +#include "mozilla/java/WebAuthnTokenManagerNatives.h" +#include "nsIWebAuthnService.h" + +namespace mozilla { + +namespace dom { + +// Collected from +// https://developers.google.com/android/reference/com/google/android/gms/fido/fido2/api/common/ErrorCode +constexpr auto kSecurityError = u"SECURITY_ERR"_ns; +constexpr auto kConstraintError = u"CONSTRAINT_ERR"_ns; +constexpr auto kNotSupportedError = u"NOT_SUPPORTED_ERR"_ns; +constexpr auto kInvalidStateError = u"INVALID_STATE_ERR"_ns; +constexpr auto kNotAllowedError = u"NOT_ALLOWED_ERR"_ns; +constexpr auto kAbortError = u"ABORT_ERR"_ns; +constexpr auto kEncodingError = u"ENCODING_ERR"_ns; +constexpr auto kDataError = u"DATA_ERR"_ns; +constexpr auto kTimeoutError = u"TIMEOUT_ERR"_ns; +constexpr auto kNetworkError = u"NETWORK_ERR"_ns; +constexpr auto kUnknownError = u"UNKNOWN_ERR"_ns; + +class AndroidWebAuthnError { + public: + explicit AndroidWebAuthnError(const nsAString& aErrorCode) + : mErrorCode(aErrorCode) {} + + nsresult GetError() const { + if (mErrorCode.Equals(kSecurityError)) { + return NS_ERROR_DOM_SECURITY_ERR; + } else if (mErrorCode.Equals(kConstraintError)) { + // TODO: The message is right, but it's not about indexeddb. + // See https://heycam.github.io/webidl/#constrainterror + return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR; + } else if (mErrorCode.Equals(kNotSupportedError)) { + return NS_ERROR_DOM_NOT_SUPPORTED_ERR; + } else if (mErrorCode.Equals(kInvalidStateError)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } else if (mErrorCode.Equals(kNotAllowedError)) { + return NS_ERROR_DOM_NOT_ALLOWED_ERR; + } else if (mErrorCode.Equals(kEncodingError)) { + return NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR; + } else if (mErrorCode.Equals(kDataError)) { + return NS_ERROR_DOM_DATA_ERR; + } else if (mErrorCode.Equals(kTimeoutError)) { + return NS_ERROR_DOM_TIMEOUT_ERR; + } else if (mErrorCode.Equals(kNetworkError)) { + return NS_ERROR_DOM_NETWORK_ERR; + } else if (mErrorCode.Equals(kAbortError)) { + return NS_ERROR_DOM_ABORT_ERR; + } else if (mErrorCode.Equals(kUnknownError)) { + return NS_ERROR_DOM_UNKNOWN_ERR; + } else { + __android_log_print(ANDROID_LOG_ERROR, "Gecko", + "RegisterAbort unknown code: %s", + NS_ConvertUTF16toUTF8(mErrorCode).get()); + return NS_ERROR_DOM_UNKNOWN_ERR; + } + } + + private: + const nsString mErrorCode; +}; + +class AndroidWebAuthnService final : public nsIWebAuthnService { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBAUTHNSERVICE + + AndroidWebAuthnService() = default; + + private: + ~AndroidWebAuthnService() = default; + + // The Android FIDO2 API doesn't accept the credProps extension. However, the + // appropriate value for CredentialPropertiesOutput.rk can be determined + // entirely from the input, so we cache it here until mRegisterPromise + // resolves. + Maybe<bool> mRegisterCredPropsRk; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_AndroidWebAuthnService_h_ diff --git a/dom/webauthn/AuthenticatorAssertionResponse.cpp b/dom/webauthn/AuthenticatorAssertionResponse.cpp new file mode 100644 index 0000000000..9824edc30b --- /dev/null +++ b/dom/webauthn/AuthenticatorAssertionResponse.cpp @@ -0,0 +1,165 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Base64.h" +#include "mozilla/dom/WebAuthenticationBinding.h" +#include "mozilla/dom/AuthenticatorAssertionResponse.h" +#include "mozilla/HoldDropJSObjects.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(AuthenticatorAssertionResponse) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(AuthenticatorAssertionResponse, + AuthenticatorResponse) + tmp->mAuthenticatorDataCachedObj = nullptr; + tmp->mSignatureCachedObj = nullptr; + tmp->mUserHandleCachedObj = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(AuthenticatorAssertionResponse, + AuthenticatorResponse) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mAuthenticatorDataCachedObj) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mSignatureCachedObj) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mUserHandleCachedObj) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED( + AuthenticatorAssertionResponse, AuthenticatorResponse) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(AuthenticatorAssertionResponse, AuthenticatorResponse) +NS_IMPL_RELEASE_INHERITED(AuthenticatorAssertionResponse, AuthenticatorResponse) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AuthenticatorAssertionResponse) +NS_INTERFACE_MAP_END_INHERITING(AuthenticatorResponse) + +AuthenticatorAssertionResponse::AuthenticatorAssertionResponse( + nsPIDOMWindowInner* aParent) + : AuthenticatorResponse(aParent), + mAuthenticatorDataCachedObj(nullptr), + mSignatureCachedObj(nullptr), + mUserHandleCachedObj(nullptr) { + mozilla::HoldJSObjects(this); +} + +AuthenticatorAssertionResponse::~AuthenticatorAssertionResponse() { + mozilla::DropJSObjects(this); +} + +JSObject* AuthenticatorAssertionResponse::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return AuthenticatorAssertionResponse_Binding::Wrap(aCx, this, aGivenProto); +} + +void AuthenticatorAssertionResponse::GetAuthenticatorData( + JSContext* aCx, JS::MutableHandle<JSObject*> aValue, ErrorResult& aRv) { + if (!mAuthenticatorDataCachedObj) { + mAuthenticatorDataCachedObj = + ArrayBuffer::Create(aCx, mAuthenticatorData, aRv); + if (aRv.Failed()) { + return; + } + } + aValue.set(mAuthenticatorDataCachedObj); +} + +void AuthenticatorAssertionResponse::SetAuthenticatorData( + const nsTArray<uint8_t>& aBuffer) { + mAuthenticatorData.Assign(aBuffer); +} + +void AuthenticatorAssertionResponse::GetSignature( + JSContext* aCx, JS::MutableHandle<JSObject*> aValue, ErrorResult& aRv) { + if (!mSignatureCachedObj) { + mSignatureCachedObj = ArrayBuffer::Create(aCx, mSignature, aRv); + if (aRv.Failed()) { + return; + } + } + aValue.set(mSignatureCachedObj); +} + +void AuthenticatorAssertionResponse::SetSignature( + const nsTArray<uint8_t>& aBuffer) { + mSignature.Assign(aBuffer); +} + +void AuthenticatorAssertionResponse::GetUserHandle( + JSContext* aCx, JS::MutableHandle<JSObject*> aValue, ErrorResult& aRv) { + // Per + // https://w3c.github.io/webauthn/#ref-for-dom-authenticatorassertionresponse-userhandle%E2%91%A0 + // this should return null if the handle is unset. + if (mUserHandle.IsEmpty()) { + aValue.set(nullptr); + } else { + if (!mUserHandleCachedObj) { + mUserHandleCachedObj = ArrayBuffer::Create(aCx, mUserHandle, aRv); + if (aRv.Failed()) { + return; + } + } + aValue.set(mUserHandleCachedObj); + } +} + +void AuthenticatorAssertionResponse::SetUserHandle( + const nsTArray<uint8_t>& aBuffer) { + 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 diff --git a/dom/webauthn/AuthenticatorAssertionResponse.h b/dom/webauthn/AuthenticatorAssertionResponse.h new file mode 100644 index 0000000000..75982800d1 --- /dev/null +++ b/dom/webauthn/AuthenticatorAssertionResponse.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_AuthenticatorAssertionResponse_h +#define mozilla_dom_AuthenticatorAssertionResponse_h + +#include "js/TypeDecls.h" +#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" + +namespace mozilla::dom { + +class AuthenticatorAssertionResponse final : public AuthenticatorResponse { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + AuthenticatorAssertionResponse, AuthenticatorResponse) + + explicit AuthenticatorAssertionResponse(nsPIDOMWindowInner* aParent); + + protected: + ~AuthenticatorAssertionResponse() override; + + public: + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetAuthenticatorData(JSContext* aCx, JS::MutableHandle<JSObject*> aValue, + ErrorResult& aRv); + + void SetAuthenticatorData(const nsTArray<uint8_t>& aBuffer); + + void GetSignature(JSContext* aCx, JS::MutableHandle<JSObject*> aValue, + ErrorResult& aRv); + + void SetSignature(const nsTArray<uint8_t>& aBuffer); + + void GetUserHandle(JSContext* aCx, JS::MutableHandle<JSObject*> aValue, + ErrorResult& aRv); + + void SetUserHandle(const nsTArray<uint8_t>& aBuffer); + + void ToJSON(AuthenticatorAssertionResponseJSON& aJSON, ErrorResult& aError); + + private: + nsTArray<uint8_t> mAuthenticatorData; + JS::Heap<JSObject*> mAuthenticatorDataCachedObj; + nsTArray<uint8_t> mSignature; + JS::Heap<JSObject*> mSignatureCachedObj; + nsTArray<uint8_t> mUserHandle; + JS::Heap<JSObject*> mUserHandleCachedObj; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_AuthenticatorAssertionResponse_h diff --git a/dom/webauthn/AuthenticatorAttestationResponse.cpp b/dom/webauthn/AuthenticatorAttestationResponse.cpp new file mode 100644 index 0000000000..7fe493dae3 --- /dev/null +++ b/dom/webauthn/AuthenticatorAttestationResponse.cpp @@ -0,0 +1,241 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AuthrsBridge_ffi.h" +#include "mozilla/Base64.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/AuthenticatorAttestationResponse.h" +#include "mozilla/dom/WebAuthenticationBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(AuthenticatorAttestationResponse) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED( + AuthenticatorAttestationResponse, AuthenticatorResponse) + tmp->mAttestationObjectCachedObj = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(AuthenticatorAttestationResponse, + AuthenticatorResponse) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mAttestationObjectCachedObj) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED( + AuthenticatorAttestationResponse, AuthenticatorResponse) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(AuthenticatorAttestationResponse, + AuthenticatorResponse) +NS_IMPL_RELEASE_INHERITED(AuthenticatorAttestationResponse, + AuthenticatorResponse) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AuthenticatorAttestationResponse) +NS_INTERFACE_MAP_END_INHERITING(AuthenticatorResponse) + +AuthenticatorAttestationResponse::AuthenticatorAttestationResponse( + nsPIDOMWindowInner* aParent) + : AuthenticatorResponse(aParent), mAttestationObjectCachedObj(nullptr) { + mozilla::HoldJSObjects(this); +} + +AuthenticatorAttestationResponse::~AuthenticatorAttestationResponse() { + mozilla::DropJSObjects(this); +} + +JSObject* AuthenticatorAttestationResponse::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return AuthenticatorAttestationResponse_Binding::Wrap(aCx, this, aGivenProto); +} + +void AuthenticatorAttestationResponse::GetAttestationObject( + JSContext* aCx, JS::MutableHandle<JSObject*> aValue, ErrorResult& aRv) { + if (!mAttestationObjectCachedObj) { + mAttestationObjectCachedObj = + ArrayBuffer::Create(aCx, mAttestationObject, aRv); + if (aRv.Failed()) { + return; + } + } + aValue.set(mAttestationObjectCachedObj); +} + +void AuthenticatorAttestationResponse::SetAttestationObject( + const nsTArray<uint8_t>& aBuffer) { + mAttestationObject.Assign(aBuffer); +} + +void AuthenticatorAttestationResponse::GetTransports( + nsTArray<nsString>& aTransports) { + aTransports.Assign(mTransports); +} + +void AuthenticatorAttestationResponse::SetTransports( + const nsTArray<nsString>& aTransports) { + mTransports.Assign(aTransports); +} + +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)) { + return rv; + } + } + nsresult rv = + 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; + } + + JS::Heap<JSObject*> buffer(ArrayBuffer::Create(aCx, authenticatorData, aRv)); + if (aRv.Failed()) { + return; + } + aValue.set(buffer); +} + +nsresult AuthenticatorAttestationResponse::GetPublicKeyBytes( + nsTArray<uint8_t>& aPublicKeyBytes) { + if (!mAttestationObjectParsed) { + nsresult rv = authrs_webauthn_att_obj_constructor( + mAttestationObject, /* anonymize */ false, + getter_AddRefs(mAttestationObjectParsed)); + if (NS_FAILED(rv)) { + 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 = GetPublicKeyBytes(publicKey); + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_NOT_AVAILABLE) { + aValue.set(nullptr); + } else { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + } + return; + } + + JS::Heap<JSObject*> buffer(ArrayBuffer::Create(aCx, publicKey, aRv)); + if (aRv.Failed()) { + return; + } + aValue.set(buffer); +} + +COSEAlgorithmIdentifier AuthenticatorAttestationResponse::GetPublicKeyAlgorithm( + ErrorResult& aRv) { + if (!mAttestationObjectParsed) { + nsresult rv = authrs_webauthn_att_obj_constructor( + mAttestationObject, false, getter_AddRefs(mAttestationObjectParsed)); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return 0; + } + } + + COSEAlgorithmIdentifier alg; + mAttestationObjectParsed->GetPublicKeyAlgorithm(&alg); + 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 diff --git a/dom/webauthn/AuthenticatorAttestationResponse.h b/dom/webauthn/AuthenticatorAttestationResponse.h new file mode 100644 index 0000000000..04b28be397 --- /dev/null +++ b/dom/webauthn/AuthenticatorAttestationResponse.h @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_AuthenticatorAttestationResponse_h +#define mozilla_dom_AuthenticatorAttestationResponse_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/AuthenticatorResponse.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/WebAuthenticationBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIWebAuthnAttObj.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class AuthenticatorAttestationResponse final : public AuthenticatorResponse { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + AuthenticatorAttestationResponse, AuthenticatorResponse) + + explicit AuthenticatorAttestationResponse(nsPIDOMWindowInner* aParent); + + protected: + ~AuthenticatorAttestationResponse() override; + + public: + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetAttestationObject(JSContext* aCx, JS::MutableHandle<JSObject*> aValue, + ErrorResult& aRv); + + void SetAttestationObject(const nsTArray<uint8_t>& aBuffer); + + void GetTransports(nsTArray<nsString>& aTransports); + + void SetTransports(const nsTArray<nsString>& aTransports); + + void GetAuthenticatorData(JSContext* aCx, JS::MutableHandle<JSObject*> aValue, + ErrorResult& aRv); + + void GetPublicKey(JSContext* aCx, JS::MutableHandle<JSObject*> aValue, + ErrorResult& aRv); + + 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; + nsTArray<nsString> mTransports; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_AuthenticatorAttestationResponse_h diff --git a/dom/webauthn/AuthenticatorResponse.cpp b/dom/webauthn/AuthenticatorResponse.cpp new file mode 100644 index 0000000000..f0c3888fa8 --- /dev/null +++ b/dom/webauthn/AuthenticatorResponse.cpp @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Base64.h" +#include "mozilla/dom/AuthenticatorResponse.h" +#include "mozilla/dom/TypedArray.h" +#include "nsPIDOMWindow.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WITH_JS_MEMBERS( + AuthenticatorResponse, (mParent), (mClientDataJSONCachedObj)) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AuthenticatorResponse) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AuthenticatorResponse) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AuthenticatorResponse) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +AuthenticatorResponse::AuthenticatorResponse(nsPIDOMWindowInner* aParent) + : mParent(aParent), mClientDataJSONCachedObj(nullptr) { + // Call HoldJSObjects() in subclasses. +} + +AuthenticatorResponse::~AuthenticatorResponse() { + // Call DropJSObjects() in subclasses. +} + +nsISupports* AuthenticatorResponse::GetParentObject() const { return mParent; } + +void AuthenticatorResponse::GetClientDataJSON( + JSContext* aCx, JS::MutableHandle<JSObject*> aValue, ErrorResult& aRv) { + if (!mClientDataJSONCachedObj) { + mClientDataJSONCachedObj = ArrayBuffer::Create(aCx, mClientDataJSON, aRv); + if (aRv.Failed()) { + return; + } + } + aValue.set(mClientDataJSONCachedObj); +} + +void AuthenticatorResponse::SetClientDataJSON(const nsCString& aBuffer) { + mClientDataJSON.Assign(aBuffer); +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/AuthenticatorResponse.h b/dom/webauthn/AuthenticatorResponse.h new file mode 100644 index 0000000000..b0abb3185c --- /dev/null +++ b/dom/webauthn/AuthenticatorResponse.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_AuthenticatorResponse_h +#define mozilla_dom_AuthenticatorResponse_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "nsCOMPtr.h" +#include "nsPIDOMWindow.h" + +namespace mozilla::dom { + +class AuthenticatorResponse : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AuthenticatorResponse) + + explicit AuthenticatorResponse(nsPIDOMWindowInner* aParent); + + protected: + virtual ~AuthenticatorResponse(); + + public: + nsISupports* GetParentObject() const; + + void GetFormat(nsString& aRetVal) const; + + void GetClientDataJSON(JSContext* aCx, JS::MutableHandle<JSObject*> aValue, + ErrorResult& aRv); + + void SetClientDataJSON(const nsCString& aBuffer); + + protected: + nsCString mClientDataJSON; + + private: + nsCOMPtr<nsPIDOMWindowInner> mParent; + JS::Heap<JSObject*> mClientDataJSONCachedObj; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_AuthenticatorResponse_h diff --git a/dom/webauthn/AuthrsBridge_ffi.h b/dom/webauthn/AuthrsBridge_ffi.h new file mode 100644 index 0000000000..72ca7f31ec --- /dev/null +++ b/dom/webauthn/AuthrsBridge_ffi.h @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_AuthrsBridge_ffi_h +#define mozilla_dom_AuthrsBridge_ffi_h + +// TODO(Bug 1850592): Autogenerate this with cbindgen. + +#include "nsTArray.h" + +class nsIWebAuthnAttObj; +class nsIWebAuthnService; + +extern "C" { + +nsresult authrs_service_constructor(nsIWebAuthnService** result); + +nsresult authrs_webauthn_att_obj_constructor( + const nsTArray<uint8_t>& attestation, bool anonymize, + nsIWebAuthnAttObj** result); + +} // extern "C" + +#endif // mozilla_dom_AuthrsBridge_ffi_h diff --git a/dom/webauthn/MacOSWebAuthnService.h b/dom/webauthn/MacOSWebAuthnService.h new file mode 100644 index 0000000000..08c912027e --- /dev/null +++ b/dom/webauthn/MacOSWebAuthnService.h @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_MacOSWebAuthnService_h +#define mozilla_dom_MacOSWebAuthnService_h + +#include <os/availability.h> + +#include "nsIWebAuthnService.h" + +namespace mozilla::dom { + +API_AVAILABLE(macos(13.3)) +already_AddRefed<nsIWebAuthnService> NewMacOSWebAuthnServiceIfAvailable(); + +} // namespace mozilla::dom + +#endif // mozilla_dom_MacOSWebAuthnService_h diff --git a/dom/webauthn/MacOSWebAuthnService.mm b/dom/webauthn/MacOSWebAuthnService.mm new file mode 100644 index 0000000000..79b9030541 --- /dev/null +++ b/dom/webauthn/MacOSWebAuthnService.mm @@ -0,0 +1,1166 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#import <AuthenticationServices/AuthenticationServices.h> + +#include "MacOSWebAuthnService.h" + +#include "CFTypeRefPtr.h" +#include "WebAuthnAutoFillEntry.h" +#include "WebAuthnEnumStrings.h" +#include "WebAuthnResult.h" +#include "WebAuthnTransportIdentifiers.h" +#include "mozilla/Maybe.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "nsCocoaUtils.h" +#include "nsIWebAuthnPromise.h" +#include "nsThreadUtils.h" + +// The documentation for the platform APIs used here can be found at: +// https://developer.apple.com/documentation/authenticationservices/public-private_key_authentication/supporting_passkeys + +namespace { +static mozilla::LazyLogModule gMacOSWebAuthnServiceLog("macoswebauthnservice"); +} // namespace + +namespace mozilla::dom { +class API_AVAILABLE(macos(13.3)) MacOSWebAuthnService; +} // namespace mozilla::dom + +// The following ASC* declarations are from the private framework +// AuthenticationServicesCore. The full definitions can be found in WebKit's +// source at Source/WebKit/Platform/spi/Cocoa/AuthenticationServicesCoreSPI.h. +// Overriding ASAuthorizationController's _requestContextWithRequests is +// currently the only way to provide the right information to the macOS +// WebAuthn API (namely, the clientDataHash for requests made to physical +// tokens). + +NS_ASSUME_NONNULL_BEGIN + +@class ASCPublicKeyCredentialDescriptor; +@interface ASCPublicKeyCredentialDescriptor : NSObject <NSSecureCoding> +- (instancetype)initWithCredentialID:(NSData*)credentialID + transports: + (nullable NSArray<NSString*>*)allowedTransports; +@end + +@protocol ASCPublicKeyCredentialCreationOptions +@property(nonatomic, copy) NSData* clientDataHash; +@property(nonatomic, nullable, copy) NSData* challenge; +@property(nonatomic, copy) + NSArray<ASCPublicKeyCredentialDescriptor*>* excludedCredentials; +@end + +@protocol ASCPublicKeyCredentialAssertionOptions <NSCopying> +@property(nonatomic, copy) NSData* clientDataHash; +@end + +@protocol ASCCredentialRequestContext +@property(nonatomic, nullable, copy) id<ASCPublicKeyCredentialCreationOptions> + platformKeyCredentialCreationOptions; +@property(nonatomic, nullable, copy) id<ASCPublicKeyCredentialCreationOptions> + securityKeyCredentialCreationOptions; +@property(nonatomic, nullable, copy) id<ASCPublicKeyCredentialAssertionOptions> + platformKeyCredentialAssertionOptions; +@property(nonatomic, nullable, copy) id<ASCPublicKeyCredentialAssertionOptions> + securityKeyCredentialAssertionOptions; +@end + +@interface ASAuthorizationController (Secrets) +- (id<ASCCredentialRequestContext>) + _requestContextWithRequests:(NSArray<ASAuthorizationRequest*>*)requests + error:(NSError**)outError; +@end + +NSArray<NSString*>* TransportsByteToTransportsArray(const uint8_t aTransports) + API_AVAILABLE(macos(13.3)) { + NSMutableArray<NSString*>* transportsNS = [[NSMutableArray alloc] init]; + if ((aTransports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB) == + MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB) { + [transportsNS + addObject: + ASAuthorizationSecurityKeyPublicKeyCredentialDescriptorTransportUSB]; + } + if ((aTransports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC) == + MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC) { + [transportsNS + addObject: + ASAuthorizationSecurityKeyPublicKeyCredentialDescriptorTransportNFC]; + } + if ((aTransports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE) == + MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE) { + [transportsNS + addObject: + ASAuthorizationSecurityKeyPublicKeyCredentialDescriptorTransportBluetooth]; + } + // TODO (bug 1859367): the platform doesn't have a definition for "internal" + // transport. When it does, this code should be updated to handle it. + return transportsNS; +} + +NSArray* CredentialListsToCredentialDescriptorArray( + const nsTArray<nsTArray<uint8_t>>& aCredentialList, + const nsTArray<uint8_t>& aCredentialListTransports, + const Class credentialDescriptorClass) API_AVAILABLE(macos(13.3)) { + MOZ_ASSERT(aCredentialList.Length() == aCredentialListTransports.Length()); + NSMutableArray* credentials = [[NSMutableArray alloc] init]; + for (size_t i = 0; i < aCredentialList.Length(); i++) { + const nsTArray<uint8_t>& credentialId = aCredentialList[i]; + const uint8_t& credentialTransports = aCredentialListTransports[i]; + NSData* credentialIdNS = [NSData dataWithBytes:credentialId.Elements() + length:credentialId.Length()]; + NSArray<NSString*>* credentialTransportsNS = + TransportsByteToTransportsArray(credentialTransports); + NSObject* credential = [[credentialDescriptorClass alloc] + initWithCredentialID:credentialIdNS + transports:credentialTransportsNS]; + [credentials addObject:credential]; + } + return credentials; +} + +// MacOSAuthorizationController is an ASAuthorizationController that overrides +// _requestContextWithRequests so that the implementation can set some options +// that aren't directly settable using the public API. +API_AVAILABLE(macos(13.3)) +@interface MacOSAuthorizationController : ASAuthorizationController +@end + +@implementation MacOSAuthorizationController { + nsTArray<uint8_t> mClientDataHash; + nsTArray<nsTArray<uint8_t>> mCredentialList; + nsTArray<uint8_t> mCredentialListTransports; +} + +- (void)setRegistrationOptions: + (id<ASCPublicKeyCredentialCreationOptions>)registrationOptions { + registrationOptions.clientDataHash = + [NSData dataWithBytes:mClientDataHash.Elements() + length:mClientDataHash.Length()]; + // Unset challenge so that the implementation uses clientDataHash (the API + // returns an error otherwise). + registrationOptions.challenge = nil; + const Class publicKeyCredentialDescriptorClass = + NSClassFromString(@"ASCPublicKeyCredentialDescriptor"); + NSArray<ASCPublicKeyCredentialDescriptor*>* excludedCredentials = + CredentialListsToCredentialDescriptorArray( + mCredentialList, mCredentialListTransports, + publicKeyCredentialDescriptorClass); + if ([excludedCredentials count] > 0) { + registrationOptions.excludedCredentials = excludedCredentials; + } +} + +- (void)stashClientDataHash:(nsTArray<uint8_t>&&)clientDataHash + andCredentialList:(nsTArray<nsTArray<uint8_t>>&&)credentialList + andCredentialListTransports:(nsTArray<uint8_t>&&)credentialListTransports { + mClientDataHash = std::move(clientDataHash); + mCredentialList = std::move(credentialList); + mCredentialListTransports = std::move(credentialListTransports); +} + +- (id<ASCCredentialRequestContext>) + _requestContextWithRequests:(NSArray<ASAuthorizationRequest*>*)requests + error:(NSError**)outError { + id<ASCCredentialRequestContext> context = + [super _requestContextWithRequests:requests error:outError]; + + if (context.platformKeyCredentialCreationOptions) { + [self setRegistrationOptions:context.platformKeyCredentialCreationOptions]; + } + if (context.securityKeyCredentialCreationOptions) { + [self setRegistrationOptions:context.securityKeyCredentialCreationOptions]; + } + + if (context.platformKeyCredentialAssertionOptions) { + id<ASCPublicKeyCredentialAssertionOptions> assertionOptions = + context.platformKeyCredentialAssertionOptions; + assertionOptions.clientDataHash = + [NSData dataWithBytes:mClientDataHash.Elements() + length:mClientDataHash.Length()]; + context.platformKeyCredentialAssertionOptions = + [assertionOptions copyWithZone:nil]; + } + if (context.securityKeyCredentialAssertionOptions) { + id<ASCPublicKeyCredentialAssertionOptions> assertionOptions = + context.securityKeyCredentialAssertionOptions; + assertionOptions.clientDataHash = + [NSData dataWithBytes:mClientDataHash.Elements() + length:mClientDataHash.Length()]; + context.securityKeyCredentialAssertionOptions = + [assertionOptions copyWithZone:nil]; + } + + return context; +} +@end + +// MacOSAuthenticatorRequestDelegate is an ASAuthorizationControllerDelegate, +// which can be set as an ASAuthorizationController's delegate to be called +// back when a request for a platform authenticator attestation or assertion +// response completes either with an attestation or assertion +// (didCompleteWithAuthorization) or with an error (didCompleteWithError). +API_AVAILABLE(macos(13.3)) +@interface MacOSAuthenticatorRequestDelegate + : NSObject <ASAuthorizationControllerDelegate> +- (void)setCallback:(mozilla::dom::MacOSWebAuthnService*)callback; +@end + +// MacOSAuthenticatorPresentationContextProvider is an +// ASAuthorizationControllerPresentationContextProviding, which can be set as +// an ASAuthorizationController's presentationContextProvider, and provides a +// presentation anchor for the ASAuthorizationController. Basically, this +// provides the API a handle to the window that made the request. +API_AVAILABLE(macos(13.3)) +@interface MacOSAuthenticatorPresentationContextProvider + : NSObject <ASAuthorizationControllerPresentationContextProviding> +@property(nonatomic, strong) NSWindow* window; +@end + +namespace mozilla::dom { + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullability-completeness" +class API_AVAILABLE(macos(13.3)) MacOSWebAuthnService final + : public nsIWebAuthnService { + public: + MacOSWebAuthnService() + : mTransactionState(Nothing(), + "MacOSWebAuthnService::mTransactionState") {} + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBAUTHNSERVICE + + void FinishMakeCredential(const nsTArray<uint8_t>& aRawAttestationObject, + const nsTArray<uint8_t>& aCredentialId, + const nsTArray<nsString>& aTransports, + const Maybe<nsString>& aAuthenticatorAttachment); + + void FinishGetAssertion(const nsTArray<uint8_t>& aCredentialId, + const nsTArray<uint8_t>& aSignature, + const nsTArray<uint8_t>& aAuthenticatorData, + const nsTArray<uint8_t>& aUserHandle, + const Maybe<nsString>& aAuthenticatorAttachment); + void ReleasePlatformResources(); + void AbortTransaction(nsresult aError); + + private: + ~MacOSWebAuthnService() = default; + + void PerformRequests(NSArray<ASAuthorizationRequest*>* aRequests, + nsTArray<uint8_t>&& aClientDataHash, + nsTArray<nsTArray<uint8_t>>&& aCredentialList, + nsTArray<uint8_t>&& aCredentialListTransports, + uint64_t aBrowsingContextId); + + struct TransactionState { + uint64_t transactionId; + uint64_t browsingContextId; + Maybe<RefPtr<nsIWebAuthnSignArgs>> pendingSignArgs; + Maybe<RefPtr<nsIWebAuthnSignPromise>> pendingSignPromise; + Maybe<nsTArray<RefPtr<nsIWebAuthnAutoFillEntry>>> autoFillEntries; + }; + + using TransactionStateMutex = DataMutex<Maybe<TransactionState>>; + void DoGetAssertion(Maybe<nsTArray<uint8_t>>&& aSelectedCredentialId, + const TransactionStateMutex::AutoLock& aGuard); + + TransactionStateMutex mTransactionState; + + // Main thread only: + ASAuthorizationWebBrowserPublicKeyCredentialManager* mCredentialManager = nil; + nsCOMPtr<nsIWebAuthnRegisterPromise> mRegisterPromise; + nsCOMPtr<nsIWebAuthnSignPromise> mSignPromise; + MacOSAuthorizationController* mAuthorizationController = nil; + MacOSAuthenticatorRequestDelegate* mRequestDelegate = nil; + MacOSAuthenticatorPresentationContextProvider* mPresentationContextProvider = + nil; +}; +#pragma clang diagnostic pop + +} // namespace mozilla::dom + +nsTArray<uint8_t> NSDataToArray(NSData* data) { + nsTArray<uint8_t> array(reinterpret_cast<const uint8_t*>(data.bytes), + data.length); + return array; +} + +@implementation MacOSAuthenticatorRequestDelegate { + RefPtr<mozilla::dom::MacOSWebAuthnService> mCallback; +} + +- (void)setCallback:(mozilla::dom::MacOSWebAuthnService*)callback { + mCallback = callback; +} + +- (void)authorizationController:(ASAuthorizationController*)controller + didCompleteWithAuthorization:(ASAuthorization*)authorization { + if ([authorization.credential + conformsToProtocol: + @protocol(ASAuthorizationPublicKeyCredentialRegistration)]) { + MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug, + ("MacOSAuthenticatorRequestDelegate::didCompleteWithAuthorization: " + "got registration")); + id<ASAuthorizationPublicKeyCredentialRegistration> credential = + (id<ASAuthorizationPublicKeyCredentialRegistration>) + authorization.credential; + nsTArray<uint8_t> rawAttestationObject( + NSDataToArray(credential.rawAttestationObject)); + nsTArray<uint8_t> credentialId(NSDataToArray(credential.credentialID)); + nsTArray<nsString> transports; + mozilla::Maybe<nsString> authenticatorAttachment; + if ([credential isKindOfClass: + [ASAuthorizationPlatformPublicKeyCredentialRegistration + class]]) { + transports.AppendElement(u"internal"_ns); +#if defined(MAC_OS_VERSION_13_5) && \ + MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_13_5 + if (__builtin_available(macos 13.5, *)) { + ASAuthorizationPlatformPublicKeyCredentialRegistration* + platformCredential = + (ASAuthorizationPlatformPublicKeyCredentialRegistration*) + credential; + switch (platformCredential.attachment) { + case ASAuthorizationPublicKeyCredentialAttachmentCrossPlatform: + authenticatorAttachment.emplace(u"cross-platform"_ns); + break; + case ASAuthorizationPublicKeyCredentialAttachmentPlatform: + authenticatorAttachment.emplace(u"platform"_ns); + break; + default: + break; + } + } +#endif + } else { + authenticatorAttachment.emplace(u"cross-platform"_ns); + } + mCallback->FinishMakeCredential(rawAttestationObject, credentialId, + transports, authenticatorAttachment); + } else if ([authorization.credential + conformsToProtocol: + @protocol(ASAuthorizationPublicKeyCredentialAssertion)]) { + MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Debug, + ("MacOSAuthenticatorRequestDelegate::didCompleteWithAuthorization: " + "got assertion")); + id<ASAuthorizationPublicKeyCredentialAssertion> credential = + (id<ASAuthorizationPublicKeyCredentialAssertion>) + authorization.credential; + nsTArray<uint8_t> credentialId(NSDataToArray(credential.credentialID)); + nsTArray<uint8_t> signature(NSDataToArray(credential.signature)); + nsTArray<uint8_t> rawAuthenticatorData( + NSDataToArray(credential.rawAuthenticatorData)); + nsTArray<uint8_t> userHandle(NSDataToArray(credential.userID)); + mozilla::Maybe<nsString> authenticatorAttachment; + if ([credential + isKindOfClass:[ASAuthorizationPlatformPublicKeyCredentialAssertion + class]]) { +#if defined(MAC_OS_VERSION_13_5) && \ + MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_13_5 + if (__builtin_available(macos 13.5, *)) { + ASAuthorizationPlatformPublicKeyCredentialAssertion* + platformCredential = + (ASAuthorizationPlatformPublicKeyCredentialAssertion*) + credential; + switch (platformCredential.attachment) { + case ASAuthorizationPublicKeyCredentialAttachmentCrossPlatform: + authenticatorAttachment.emplace(u"cross-platform"_ns); + break; + case ASAuthorizationPublicKeyCredentialAttachmentPlatform: + authenticatorAttachment.emplace(u"platform"_ns); + break; + default: + break; + } + } +#endif + } else { + authenticatorAttachment.emplace(u"cross-platform"_ns); + } + mCallback->FinishGetAssertion(credentialId, signature, rawAuthenticatorData, + userHandle, authenticatorAttachment); + } else { + MOZ_LOG( + gMacOSWebAuthnServiceLog, mozilla::LogLevel::Error, + ("MacOSAuthenticatorRequestDelegate::didCompleteWithAuthorization: " + "authorization.credential is neither registration nor assertion!")); + MOZ_ASSERT_UNREACHABLE( + "should have ASAuthorizationPublicKeyCredentialRegistration or " + "ASAuthorizationPublicKeyCredentialAssertion"); + } + mCallback->ReleasePlatformResources(); + mCallback = nullptr; +} + +- (void)authorizationController:(ASAuthorizationController*)controller + didCompleteWithError:(NSError*)error { + nsAutoString errorDescription; + nsCocoaUtils::GetStringForNSString(error.localizedDescription, + errorDescription); + nsAutoString errorDomain; + nsCocoaUtils::GetStringForNSString(error.domain, errorDomain); + MOZ_LOG(gMacOSWebAuthnServiceLog, mozilla::LogLevel::Warning, + ("MacOSAuthenticatorRequestDelegate::didCompleteWithError: domain " + "'%s' code %ld (%s)", + NS_ConvertUTF16toUTF8(errorDomain).get(), error.code, + NS_ConvertUTF16toUTF8(errorDescription).get())); + nsresult rv = NS_ERROR_DOM_NOT_ALLOWED_ERR; + // For some reason, the error for "the credential used in a registration was + // on the exclude list" is in the "WKErrorDomain" domain with code 8, which + // is presumably WKErrorDuplicateCredential. + const NSInteger WKErrorDuplicateCredential = 8; + if (errorDomain.EqualsLiteral("WKErrorDomain") && + error.code == WKErrorDuplicateCredential) { + rv = NS_ERROR_DOM_INVALID_STATE_ERR; + } else if (error.domain == ASAuthorizationErrorDomain) { + switch (error.code) { + case ASAuthorizationErrorCanceled: + rv = NS_ERROR_DOM_ABORT_ERR; + break; + case ASAuthorizationErrorFailed: + // The message is right, but it's not about indexeddb. + // See https://webidl.spec.whatwg.org/#constrainterror + rv = NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR; + break; + case ASAuthorizationErrorUnknown: + rv = NS_ERROR_DOM_UNKNOWN_ERR; + break; + default: + // rv already has a default value + break; + } + } + mCallback->AbortTransaction(rv); + mCallback = nullptr; +} +@end + +@implementation MacOSAuthenticatorPresentationContextProvider +@synthesize window = window; + +- (ASPresentationAnchor)presentationAnchorForAuthorizationController: + (ASAuthorizationController*)controller { + return window; +} +@end + +namespace mozilla::dom { + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullability-completeness" +// Given a browsingContextId, attempts to determine the NSWindow associated +// with that browser. +nsresult BrowsingContextIdToNSWindow(uint64_t browsingContextId, + NSWindow** window) { + *window = nullptr; + RefPtr<BrowsingContext> browsingContext( + BrowsingContext::Get(browsingContextId)); + if (!browsingContext) { + return NS_ERROR_NOT_AVAILABLE; + } + CanonicalBrowsingContext* canonicalBrowsingContext = + browsingContext->Canonical(); + if (!canonicalBrowsingContext) { + return NS_ERROR_NOT_AVAILABLE; + } + nsCOMPtr<nsIWidget> widget( + canonicalBrowsingContext->GetParentProcessWidgetContaining()); + if (!widget) { + return NS_ERROR_NOT_AVAILABLE; + } + *window = static_cast<NSWindow*>(widget->GetNativeData(NS_NATIVE_WINDOW)); + return NS_OK; +} +#pragma clang diagnostic pop + +already_AddRefed<nsIWebAuthnService> NewMacOSWebAuthnServiceIfAvailable() { + MOZ_ASSERT(XRE_IsParentProcess()); + if (!StaticPrefs::security_webauthn_enable_macos_passkeys()) { + MOZ_LOG( + gMacOSWebAuthnServiceLog, LogLevel::Debug, + ("macOS platform support for webauthn (passkeys) disabled by pref")); + return nullptr; + } + // This code checks for the entitlement + // 'com.apple.developer.web-browser.public-key-credential', which must be + // true to be able to use the platform APIs. + CFTypeRefPtr<SecTaskRef> entitlementTask( + CFTypeRefPtr<SecTaskRef>::WrapUnderCreateRule( + SecTaskCreateFromSelf(nullptr))); + CFTypeRefPtr<CFBooleanRef> entitlementValue( + CFTypeRefPtr<CFBooleanRef>::WrapUnderCreateRule( + reinterpret_cast<CFBooleanRef>(SecTaskCopyValueForEntitlement( + entitlementTask.get(), + CFSTR("com.apple.developer.web-browser.public-key-credential"), + nullptr)))); + if (!entitlementValue || !CFBooleanGetValue(entitlementValue.get())) { + MOZ_LOG( + gMacOSWebAuthnServiceLog, LogLevel::Warning, + ("entitlement com.apple.developer.web-browser.public-key-credential " + "not present: platform passkey APIs will not be available")); + return nullptr; + } + nsCOMPtr<nsIWebAuthnService> service(new MacOSWebAuthnService()); + return service.forget(); +} + +void MacOSWebAuthnService::AbortTransaction(nsresult aError) { + MOZ_ASSERT(NS_IsMainThread()); + if (mRegisterPromise) { + Unused << mRegisterPromise->Reject(aError); + mRegisterPromise = nullptr; + } + if (mSignPromise) { + Unused << mSignPromise->Reject(aError); + mSignPromise = nullptr; + } + ReleasePlatformResources(); +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullability-completeness" +NS_IMPL_ISUPPORTS(MacOSWebAuthnService, nsIWebAuthnService) +#pragma clang diagnostic pop + +NS_IMETHODIMP +MacOSWebAuthnService::MakeCredential(uint64_t aTransactionId, + uint64_t aBrowsingContextId, + nsIWebAuthnRegisterArgs* aArgs, + nsIWebAuthnRegisterPromise* aPromise) { + Reset(); + auto guard = mTransactionState.Lock(); + *guard = Some(TransactionState{ + aTransactionId, + aBrowsingContextId, + Nothing(), + Nothing(), + Nothing(), + }); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "MacOSWebAuthnService::MakeCredential", + [self = RefPtr{this}, browsingContextId(aBrowsingContextId), + aArgs = nsCOMPtr{aArgs}, aPromise = nsCOMPtr{aPromise}]() { + self->mRegisterPromise = aPromise; + + nsAutoString rpId; + Unused << aArgs->GetRpId(rpId); + NSString* rpIdNS = nsCocoaUtils::ToNSString(rpId); + + nsTArray<uint8_t> challenge; + Unused << aArgs->GetChallenge(challenge); + NSData* challengeNS = [NSData dataWithBytes:challenge.Elements() + length:challenge.Length()]; + + nsTArray<uint8_t> userId; + Unused << aArgs->GetUserId(userId); + NSData* userIdNS = [NSData dataWithBytes:userId.Elements() + length:userId.Length()]; + + nsAutoString userName; + Unused << aArgs->GetUserName(userName); + NSString* userNameNS = nsCocoaUtils::ToNSString(userName); + + nsAutoString userDisplayName; + Unused << aArgs->GetUserName(userDisplayName); + NSString* userDisplayNameNS = nsCocoaUtils::ToNSString(userDisplayName); + + nsTArray<int32_t> coseAlgs; + Unused << aArgs->GetCoseAlgs(coseAlgs); + NSMutableArray* credentialParameters = [[NSMutableArray alloc] init]; + for (const auto& coseAlg : coseAlgs) { + ASAuthorizationPublicKeyCredentialParameters* credentialParameter = + [[ASAuthorizationPublicKeyCredentialParameters alloc] + initWithAlgorithm:coseAlg]; + [credentialParameters addObject:credentialParameter]; + } + + nsTArray<nsTArray<uint8_t>> excludeList; + Unused << aArgs->GetExcludeList(excludeList); + nsTArray<uint8_t> excludeListTransports; + Unused << aArgs->GetExcludeListTransports(excludeListTransports); + if (excludeList.Length() != excludeListTransports.Length()) { + self->mRegisterPromise->Reject(NS_ERROR_INVALID_ARG); + return; + } + + Maybe<ASAuthorizationPublicKeyCredentialUserVerificationPreference> + userVerificationPreference = Nothing(); + nsAutoString userVerification; + Unused << aArgs->GetUserVerification(userVerification); + if (userVerification.EqualsLiteral( + MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED)) { + userVerificationPreference.emplace( + ASAuthorizationPublicKeyCredentialUserVerificationPreferenceRequired); + } else if (userVerification.EqualsLiteral( + MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED)) { + userVerificationPreference.emplace( + ASAuthorizationPublicKeyCredentialUserVerificationPreferencePreferred); + } else if ( + userVerification.EqualsLiteral( + MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED)) { + userVerificationPreference.emplace( + ASAuthorizationPublicKeyCredentialUserVerificationPreferenceDiscouraged); + } + + // The API doesn't support attestation for platform passkeys and shows + // no consent UI for non-none attestation for cross-platform devices, + // so this must always be none. + ASAuthorizationPublicKeyCredentialAttestationKind + attestationPreference = + ASAuthorizationPublicKeyCredentialAttestationKindNone; + + // Initialize the platform provider with the rpId. + ASAuthorizationPlatformPublicKeyCredentialProvider* platformProvider = + [[ASAuthorizationPlatformPublicKeyCredentialProvider alloc] + initWithRelyingPartyIdentifier:rpIdNS]; + // Make a credential registration request with the challenge, userName, + // and userId. + ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest* + platformRegistrationRequest = [platformProvider + createCredentialRegistrationRequestWithChallenge:challengeNS + name:userNameNS + userID:userIdNS]; + [platformProvider release]; + platformRegistrationRequest.attestationPreference = + attestationPreference; + if (userVerificationPreference.isSome()) { + platformRegistrationRequest.userVerificationPreference = + *userVerificationPreference; + } + + // Initialize the cross-platform provider with the rpId. + ASAuthorizationSecurityKeyPublicKeyCredentialProvider* + crossPlatformProvider = + [[ASAuthorizationSecurityKeyPublicKeyCredentialProvider alloc] + initWithRelyingPartyIdentifier:rpIdNS]; + // Make a credential registration request with the challenge, + // userDisplayName, userName, and userId. + ASAuthorizationSecurityKeyPublicKeyCredentialRegistrationRequest* + crossPlatformRegistrationRequest = [crossPlatformProvider + createCredentialRegistrationRequestWithChallenge:challengeNS + displayName: + userDisplayNameNS + name:userNameNS + userID:userIdNS]; + [crossPlatformProvider release]; + crossPlatformRegistrationRequest.attestationPreference = + attestationPreference; + crossPlatformRegistrationRequest.credentialParameters = + credentialParameters; + if (userVerificationPreference.isSome()) { + crossPlatformRegistrationRequest.userVerificationPreference = + *userVerificationPreference; + } + nsTArray<uint8_t> clientDataHash; + nsresult rv = aArgs->GetClientDataHash(clientDataHash); + if (NS_FAILED(rv)) { + self->mRegisterPromise->Reject(rv); + return; + } + nsAutoString authenticatorAttachment; + rv = aArgs->GetAuthenticatorAttachment(authenticatorAttachment); + if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) { + self->mRegisterPromise->Reject(rv); + return; + } + NSMutableArray* requests = [[NSMutableArray alloc] init]; + if (authenticatorAttachment.EqualsLiteral( + MOZ_WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM)) { + [requests addObject:platformRegistrationRequest]; + } else if (authenticatorAttachment.EqualsLiteral( + MOZ_WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM)) { + [requests addObject:crossPlatformRegistrationRequest]; + } else { + // Regarding the value of authenticator attachment, according to the + // specification, "client platforms MUST ignore unknown values, + // treating an unknown value as if the member does not exist". + [requests addObject:platformRegistrationRequest]; + [requests addObject:crossPlatformRegistrationRequest]; + } + self->PerformRequests( + requests, std::move(clientDataHash), std::move(excludeList), + std::move(excludeListTransports), browsingContextId); + })); + return NS_OK; +} + +void MacOSWebAuthnService::PerformRequests( + NSArray<ASAuthorizationRequest*>* aRequests, + nsTArray<uint8_t>&& aClientDataHash, + nsTArray<nsTArray<uint8_t>>&& aCredentialList, + nsTArray<uint8_t>&& aCredentialListTransports, + uint64_t aBrowsingContextId) { + MOZ_ASSERT(NS_IsMainThread()); + // Create a MacOSAuthorizationController and initialize it with the requests. + MOZ_ASSERT(!mAuthorizationController); + mAuthorizationController = [[MacOSAuthorizationController alloc] + initWithAuthorizationRequests:aRequests]; + [mAuthorizationController + stashClientDataHash:std::move(aClientDataHash) + andCredentialList:std::move(aCredentialList) + andCredentialListTransports:std::move(aCredentialListTransports)]; + + // Set up the delegate to run when the operation completes. + MOZ_ASSERT(!mRequestDelegate); + mRequestDelegate = [[MacOSAuthenticatorRequestDelegate alloc] init]; + [mRequestDelegate setCallback:this]; + mAuthorizationController.delegate = mRequestDelegate; + + // Create a presentation context provider so the API knows which window + // made the request. + NSWindow* window = nullptr; + nsresult rv = BrowsingContextIdToNSWindow(aBrowsingContextId, &window); + if (NS_FAILED(rv)) { + AbortTransaction(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + MOZ_ASSERT(!mPresentationContextProvider); + mPresentationContextProvider = + [[MacOSAuthenticatorPresentationContextProvider alloc] init]; + mPresentationContextProvider.window = window; + mAuthorizationController.presentationContextProvider = + mPresentationContextProvider; + + // Finally, perform the request. + [mAuthorizationController performRequests]; +} + +void MacOSWebAuthnService::FinishMakeCredential( + const nsTArray<uint8_t>& aRawAttestationObject, + const nsTArray<uint8_t>& aCredentialId, + const nsTArray<nsString>& aTransports, + const Maybe<nsString>& aAuthenticatorAttachment) { + MOZ_ASSERT(NS_IsMainThread()); + if (!mRegisterPromise) { + return; + } + + RefPtr<WebAuthnRegisterResult> result(new WebAuthnRegisterResult( + aRawAttestationObject, Nothing(), aCredentialId, aTransports, + aAuthenticatorAttachment)); + Unused << mRegisterPromise->Resolve(result); + mRegisterPromise = nullptr; +} + +NS_IMETHODIMP +MacOSWebAuthnService::GetAssertion(uint64_t aTransactionId, + uint64_t aBrowsingContextId, + nsIWebAuthnSignArgs* aArgs, + nsIWebAuthnSignPromise* aPromise) { + Reset(); + + auto guard = mTransactionState.Lock(); + *guard = Some(TransactionState{ + aTransactionId, + aBrowsingContextId, + Some(RefPtr{aArgs}), + Some(RefPtr{aPromise}), + Nothing(), + }); + + bool conditionallyMediated; + Unused << aArgs->GetConditionallyMediated(&conditionallyMediated); + if (!conditionallyMediated) { + DoGetAssertion(Nothing(), guard); + return NS_OK; + } + + // Using conditional mediation, so dispatch a task to collect any available + // passkeys. + NS_DispatchToMainThread(NS_NewRunnableFunction( + "platformCredentialsForRelyingParty", + [self = RefPtr{this}, aTransactionId, aArgs = nsCOMPtr{aArgs}]() { + // This handler is called when platformCredentialsForRelyingParty + // completes. + auto credentialsCompletionHandler = ^( + NSArray<ASAuthorizationWebBrowserPlatformPublicKeyCredential*>* + credentials) { + nsTArray<RefPtr<nsIWebAuthnAutoFillEntry>> autoFillEntries; + for (NSUInteger i = 0; i < credentials.count; i++) { + const auto& credential = credentials[i]; + nsAutoString userName; + nsCocoaUtils::GetStringForNSString(credential.name, userName); + nsAutoString rpId; + nsCocoaUtils::GetStringForNSString(credential.relyingParty, rpId); + autoFillEntries.AppendElement(new WebAuthnAutoFillEntry( + nsIWebAuthnAutoFillEntry::PROVIDER_PLATFORM_MACOS, userName, + rpId, NSDataToArray(credential.credentialID))); + } + auto guard = self->mTransactionState.Lock(); + if (guard->isSome() && guard->ref().transactionId == aTransactionId) { + guard->ref().autoFillEntries.emplace(std::move(autoFillEntries)); + } + }; + // This handler is called when + // requestAuthorizationForPublicKeyCredentials completes. + auto authorizationHandler = ^( + ASAuthorizationWebBrowserPublicKeyCredentialManagerAuthorizationState + authorizationState) { + // If authorized, list any available passkeys. + if (authorizationState == + ASAuthorizationWebBrowserPublicKeyCredentialManagerAuthorizationStateAuthorized) { + nsAutoString rpId; + Unused << aArgs->GetRpId(rpId); + [self->mCredentialManager + platformCredentialsForRelyingParty:nsCocoaUtils::ToNSString( + rpId) + completionHandler: + credentialsCompletionHandler]; + } + }; + if (!self->mCredentialManager) { + self->mCredentialManager = + [[ASAuthorizationWebBrowserPublicKeyCredentialManager alloc] + init]; + } + // Request authorization to examine any available passkeys. This will + // cause a permission prompt to appear once. + [self->mCredentialManager + requestAuthorizationForPublicKeyCredentials:authorizationHandler]; + })); + + return NS_OK; +} + +void MacOSWebAuthnService::DoGetAssertion( + Maybe<nsTArray<uint8_t>>&& aSelectedCredentialId, + const TransactionStateMutex::AutoLock& aGuard) { + if (aGuard->isNothing() || aGuard->ref().pendingSignArgs.isNothing() || + aGuard->ref().pendingSignPromise.isNothing()) { + return; + } + + // Take the pending Args and Promise to prevent repeated calls to + // DoGetAssertion for this transaction. + RefPtr<nsIWebAuthnSignArgs> aArgs = aGuard->ref().pendingSignArgs.extract(); + RefPtr<nsIWebAuthnSignPromise> aPromise = + aGuard->ref().pendingSignPromise.extract(); + uint64_t aBrowsingContextId = aGuard->ref().browsingContextId; + + NS_DispatchToMainThread(NS_NewRunnableFunction( + "MacOSWebAuthnService::MakeCredential", + [self = RefPtr{this}, browsingContextId(aBrowsingContextId), aArgs, + aPromise, + aSelectedCredentialId = std::move(aSelectedCredentialId)]() mutable { + self->mSignPromise = aPromise; + + nsAutoString rpId; + Unused << aArgs->GetRpId(rpId); + NSString* rpIdNS = nsCocoaUtils::ToNSString(rpId); + + nsTArray<uint8_t> challenge; + Unused << aArgs->GetChallenge(challenge); + NSData* challengeNS = [NSData dataWithBytes:challenge.Elements() + length:challenge.Length()]; + + nsTArray<nsTArray<uint8_t>> allowList; + nsTArray<uint8_t> allowListTransports; + if (aSelectedCredentialId.isSome()) { + allowList.AppendElement(aSelectedCredentialId.extract()); + allowListTransports.AppendElement( + MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL); + } else { + Unused << aArgs->GetAllowList(allowList); + Unused << aArgs->GetAllowListTransports(allowListTransports); + } + NSMutableArray* platformAllowedCredentials = + [[NSMutableArray alloc] init]; + for (const auto& allowedCredentialId : allowList) { + NSData* allowedCredentialIdNS = + [NSData dataWithBytes:allowedCredentialId.Elements() + length:allowedCredentialId.Length()]; + ASAuthorizationPlatformPublicKeyCredentialDescriptor* + allowedCredential = + [[ASAuthorizationPlatformPublicKeyCredentialDescriptor alloc] + initWithCredentialID:allowedCredentialIdNS]; + [platformAllowedCredentials addObject:allowedCredential]; + } + const Class securityKeyPublicKeyCredentialDescriptorClass = + NSClassFromString( + @"ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor"); + NSArray<ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor*>* + crossPlatformAllowedCredentials = + CredentialListsToCredentialDescriptorArray( + allowList, allowListTransports, + securityKeyPublicKeyCredentialDescriptorClass); + + Maybe<ASAuthorizationPublicKeyCredentialUserVerificationPreference> + userVerificationPreference = Nothing(); + nsAutoString userVerification; + Unused << aArgs->GetUserVerification(userVerification); + if (userVerification.EqualsLiteral( + MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED)) { + userVerificationPreference.emplace( + ASAuthorizationPublicKeyCredentialUserVerificationPreferenceRequired); + } else if (userVerification.EqualsLiteral( + MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED)) { + userVerificationPreference.emplace( + ASAuthorizationPublicKeyCredentialUserVerificationPreferencePreferred); + } else if ( + userVerification.EqualsLiteral( + MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED)) { + userVerificationPreference.emplace( + ASAuthorizationPublicKeyCredentialUserVerificationPreferenceDiscouraged); + } + + // Initialize the platform provider with the rpId. + ASAuthorizationPlatformPublicKeyCredentialProvider* platformProvider = + [[ASAuthorizationPlatformPublicKeyCredentialProvider alloc] + initWithRelyingPartyIdentifier:rpIdNS]; + // Make a credential assertion request with the challenge. + ASAuthorizationPlatformPublicKeyCredentialAssertionRequest* + platformAssertionRequest = [platformProvider + createCredentialAssertionRequestWithChallenge:challengeNS]; + [platformProvider release]; + platformAssertionRequest.allowedCredentials = + platformAllowedCredentials; + if (userVerificationPreference.isSome()) { + platformAssertionRequest.userVerificationPreference = + *userVerificationPreference; + } + + // Initialize the cross-platform provider with the rpId. + ASAuthorizationSecurityKeyPublicKeyCredentialProvider* + crossPlatformProvider = + [[ASAuthorizationSecurityKeyPublicKeyCredentialProvider alloc] + initWithRelyingPartyIdentifier:rpIdNS]; + // Make a credential assertion request with the challenge. + ASAuthorizationSecurityKeyPublicKeyCredentialAssertionRequest* + crossPlatformAssertionRequest = [crossPlatformProvider + createCredentialAssertionRequestWithChallenge:challengeNS]; + [crossPlatformProvider release]; + crossPlatformAssertionRequest.allowedCredentials = + crossPlatformAllowedCredentials; + if (userVerificationPreference.isSome()) { + crossPlatformAssertionRequest.userVerificationPreference = + *userVerificationPreference; + } + nsTArray<uint8_t> clientDataHash; + nsresult rv = aArgs->GetClientDataHash(clientDataHash); + if (NS_FAILED(rv)) { + self->mSignPromise->Reject(rv); + return; + } + nsTArray<nsTArray<uint8_t>> unusedCredentialIds; + nsTArray<uint8_t> unusedTransports; + // allowList and allowListTransports won't actually get used. + self->PerformRequests( + @[ platformAssertionRequest, crossPlatformAssertionRequest ], + std::move(clientDataHash), std::move(allowList), + std::move(allowListTransports), browsingContextId); + })); +} + +void MacOSWebAuthnService::FinishGetAssertion( + const nsTArray<uint8_t>& aCredentialId, const nsTArray<uint8_t>& aSignature, + const nsTArray<uint8_t>& aAuthenticatorData, + const nsTArray<uint8_t>& aUserHandle, + const Maybe<nsString>& aAuthenticatorAttachment) { + MOZ_ASSERT(NS_IsMainThread()); + if (!mSignPromise) { + return; + } + + RefPtr<WebAuthnSignResult> result(new WebAuthnSignResult( + aAuthenticatorData, Nothing(), aCredentialId, aSignature, aUserHandle, + aAuthenticatorAttachment)); + Unused << mSignPromise->Resolve(result); + mSignPromise = nullptr; +} + +void MacOSWebAuthnService::ReleasePlatformResources() { + MOZ_ASSERT(NS_IsMainThread()); + [mCredentialManager release]; + mCredentialManager = nil; + [mAuthorizationController release]; + mAuthorizationController = nil; + [mRequestDelegate release]; + mRequestDelegate = nil; + [mPresentationContextProvider release]; + mPresentationContextProvider = nil; +} + +NS_IMETHODIMP +MacOSWebAuthnService::Reset() { + auto guard = mTransactionState.Lock(); + if (guard->isSome()) { + if (guard->ref().pendingSignPromise.isSome()) { + // This request was never dispatched to the platform API, so + // we need to reject the promise ourselves. + guard->ref().pendingSignPromise.ref()->Reject( + NS_ERROR_DOM_NOT_ALLOWED_ERR); + } + guard->reset(); + } + NS_DispatchToMainThread(NS_NewRunnableFunction( + "MacOSWebAuthnService::Cancel", [self = RefPtr{this}] { + // cancel results in the delegate's didCompleteWithError method being + // called, which will release all platform resources. + [self->mAuthorizationController cancel]; + })); + return NS_OK; +} + +NS_IMETHODIMP +MacOSWebAuthnService::GetIsUVPAA(bool* aAvailable) { + *aAvailable = true; + return NS_OK; +} + +NS_IMETHODIMP +MacOSWebAuthnService::Cancel(uint64_t aTransactionId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +MacOSWebAuthnService::HasPendingConditionalGet(uint64_t aBrowsingContextId, + const nsAString& aOrigin, + uint64_t* aRv) { + auto guard = mTransactionState.Lock(); + if (guard->isNothing() || + guard->ref().browsingContextId != aBrowsingContextId || + guard->ref().pendingSignArgs.isNothing()) { + *aRv = 0; + return NS_OK; + } + + nsString origin; + Unused << guard->ref().pendingSignArgs.ref()->GetOrigin(origin); + if (origin != aOrigin) { + *aRv = 0; + return NS_OK; + } + + *aRv = guard->ref().transactionId; + return NS_OK; +} + +NS_IMETHODIMP +MacOSWebAuthnService::GetAutoFillEntries( + uint64_t aTransactionId, nsTArray<RefPtr<nsIWebAuthnAutoFillEntry>>& aRv) { + auto guard = mTransactionState.Lock(); + if (guard->isNothing() || guard->ref().transactionId != aTransactionId || + guard->ref().pendingSignArgs.isNothing() || + guard->ref().autoFillEntries.isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + aRv.Assign(guard->ref().autoFillEntries.ref()); + return NS_OK; +} + +NS_IMETHODIMP +MacOSWebAuthnService::SelectAutoFillEntry( + uint64_t aTransactionId, const nsTArray<uint8_t>& aCredentialId) { + auto guard = mTransactionState.Lock(); + if (guard->isNothing() || guard->ref().transactionId != aTransactionId || + guard->ref().pendingSignArgs.isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsTArray<nsTArray<uint8_t>> allowList; + Unused << guard->ref().pendingSignArgs.ref()->GetAllowList(allowList); + if (!allowList.IsEmpty() && !allowList.Contains(aCredentialId)) { + return NS_ERROR_FAILURE; + } + + Maybe<nsTArray<uint8_t>> id; + id.emplace(); + id.ref().Assign(aCredentialId); + DoGetAssertion(std::move(id), guard); + + return NS_OK; +} + +NS_IMETHODIMP +MacOSWebAuthnService::ResumeConditionalGet(uint64_t aTransactionId) { + auto guard = mTransactionState.Lock(); + if (guard->isNothing() || guard->ref().transactionId != aTransactionId || + guard->ref().pendingSignArgs.isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + DoGetAssertion(Nothing(), guard); + return NS_OK; +} + +NS_IMETHODIMP +MacOSWebAuthnService::PinCallback(uint64_t aTransactionId, + const nsACString& aPin) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +MacOSWebAuthnService::ResumeMakeCredential(uint64_t aTransactionId, + bool aForceNoneAttestation) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +MacOSWebAuthnService::SelectionCallback(uint64_t aTransactionId, + uint64_t aIndex) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +MacOSWebAuthnService::AddVirtualAuthenticator( + const nsACString& protocol, const nsACString& transport, + bool hasResidentKey, bool hasUserVerification, bool isUserConsenting, + bool isUserVerified, uint64_t* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +MacOSWebAuthnService::RemoveVirtualAuthenticator(uint64_t authenticatorId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +MacOSWebAuthnService::AddCredential(uint64_t authenticatorId, + const nsACString& credentialId, + bool isResidentCredential, + const nsACString& rpId, + const nsACString& privateKey, + const nsACString& userHandle, + uint32_t signCount) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +MacOSWebAuthnService::GetCredentials( + uint64_t authenticatorId, + nsTArray<RefPtr<nsICredentialParameters>>& _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +MacOSWebAuthnService::RemoveCredential(uint64_t authenticatorId, + const nsACString& credentialId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +MacOSWebAuthnService::RemoveAllCredentials(uint64_t authenticatorId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +MacOSWebAuthnService::SetUserVerified(uint64_t authenticatorId, + bool isUserVerified) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +MacOSWebAuthnService::Listen() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +MacOSWebAuthnService::RunCommand(const nsACString& cmd) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +} // namespace mozilla::dom + +NS_ASSUME_NONNULL_END diff --git a/dom/webauthn/PWebAuthnTransaction.ipdl b/dom/webauthn/PWebAuthnTransaction.ipdl new file mode 100644 index 0000000000..229fb7899d --- /dev/null +++ b/dom/webauthn/PWebAuthnTransaction.ipdl @@ -0,0 +1,157 @@ +/* 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/. */ + +/* + * IPC Transaction protocol for the WebAuthn DOM API. This IPC protocol allows + * the content process to call to the parent to access hardware for + * authentication registration and challenges. All transactions start in the + * child process, and the parent replies with a "Confirm*" message, or a + * "Cancel" message if there was an error (no hardware available, no registered + * keys, etc) or interruption (another transaction was started in another + * content process). Similarly, the content process can also request a cancel, + * either triggered explicitly by the user/script or due to UI events like + * selecting a different tab. + */ + +include protocol PBackground; + +using mozilla::dom::MaybeDiscardedBrowsingContext from "mozilla/dom/BrowsingContext.h"; + +namespace mozilla { +namespace dom { + +struct WebAuthnAuthenticatorSelection { + nsString residentKey; + nsString userVerificationRequirement; + nsString? authenticatorAttachment; +}; + +struct WebAuthnScopedCredential { + uint8_t[] id; + uint8_t transports; +}; + +struct WebAuthnExtensionAppId { + nsString appIdentifier; +}; + +struct WebAuthnExtensionCredProps { + bool credProps; +}; + +struct WebAuthnExtensionHmacSecret { + bool hmacCreateSecret; +}; + +struct WebAuthnExtensionMinPinLength { + bool minPinLength; +}; + +union WebAuthnExtension { + WebAuthnExtensionAppId; + WebAuthnExtensionCredProps; + WebAuthnExtensionHmacSecret; + WebAuthnExtensionMinPinLength; +}; + +struct WebAuthnExtensionResultAppId { + bool AppId; +}; + +struct WebAuthnExtensionResultCredProps { + bool rk; +}; + +struct WebAuthnExtensionResultHmacSecret { + bool hmacCreateSecret; +}; + +union WebAuthnExtensionResult { + WebAuthnExtensionResultAppId; + WebAuthnExtensionResultCredProps; + WebAuthnExtensionResultHmacSecret; +}; + +struct WebAuthnMakeCredentialRpInfo { + nsString Name; +}; + +struct WebAuthnMakeCredentialUserInfo { + uint8_t[] Id; + nsString Name; + nsString DisplayName; +}; + +struct CoseAlg { + long alg; +}; + +struct WebAuthnMakeCredentialInfo { + nsString Origin; + nsString RpId; + uint8_t[] Challenge; + nsCString ClientDataJSON; + uint32_t TimeoutMS; + WebAuthnScopedCredential[] ExcludeList; + WebAuthnMakeCredentialRpInfo Rp; + WebAuthnMakeCredentialUserInfo User; + CoseAlg[] coseAlgs; + WebAuthnExtension[] Extensions; + WebAuthnAuthenticatorSelection AuthenticatorSelection; + nsString attestationConveyancePreference; + uint64_t BrowsingContextId; +}; + +struct WebAuthnMakeCredentialResult { + nsCString ClientDataJSON; + uint8_t[] AttestationObject; + uint8_t[] KeyHandle; + nsString[] Transports; + WebAuthnExtensionResult[] Extensions; + nsString? AuthenticatorAttachment; +}; + +struct WebAuthnGetAssertionInfo { + nsString Origin; + nsString RpId; + uint8_t[] Challenge; + nsCString ClientDataJSON; + uint32_t TimeoutMS; + WebAuthnScopedCredential[] AllowList; + WebAuthnExtension[] Extensions; + nsString userVerificationRequirement; + bool ConditionallyMediated; + uint64_t BrowsingContextId; +}; + +struct WebAuthnGetAssertionResult { + nsCString ClientDataJSON; + uint8_t[] KeyHandle; + uint8_t[] Signature; + uint8_t[] AuthenticatorData; + WebAuthnExtensionResult[] Extensions; + uint8_t[] UserHandle; + nsString? AuthenticatorAttachment; +}; + +[ManualDealloc] +async protocol PWebAuthnTransaction { + manager PBackground; + + parent: + async RequestRegister(uint64_t aTransactionId, WebAuthnMakeCredentialInfo aTransactionInfo); + async RequestSign(uint64_t aTransactionId, WebAuthnGetAssertionInfo aTransactionInfo); + async RequestIsUVPAA() returns (bool available); + [Tainted] async RequestCancel(uint64_t aTransactionId); + async DestroyMe(); + + child: + async __delete__(); + async ConfirmRegister(uint64_t aTransactionId, WebAuthnMakeCredentialResult aResult); + async ConfirmSign(uint64_t aTransactionId, WebAuthnGetAssertionResult aResult); + async Abort(uint64_t aTransactionId, nsresult Error); +}; + +} +} diff --git a/dom/webauthn/PublicKeyCredential.cpp b/dom/webauthn/PublicKeyCredential.cpp new file mode 100644 index 0000000000..444d9742b3 --- /dev/null +++ b/dom/webauthn/PublicKeyCredential.cpp @@ -0,0 +1,377 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Base64.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/dom/AuthenticatorResponse.h" +#include "mozilla/dom/CredentialsContainer.h" +#include "mozilla/dom/ChromeUtils.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PublicKeyCredential.h" +#include "mozilla/dom/WebAuthenticationBinding.h" +#include "mozilla/dom/WebAuthnManager.h" +#include "nsCycleCollectionParticipant.h" + +#ifdef MOZ_WIDGET_ANDROID +# include "mozilla/MozPromise.h" +# include "mozilla/java/GeckoResultNatives.h" +# include "mozilla/java/WebAuthnTokenManagerWrappers.h" +#endif + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(PublicKeyCredential) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PublicKeyCredential, Credential) + tmp->mRawIdCachedObj = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PublicKeyCredential, Credential) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mRawIdCachedObj) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PublicKeyCredential, + Credential) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAttestationResponse) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAssertionResponse) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(PublicKeyCredential, Credential) +NS_IMPL_RELEASE_INHERITED(PublicKeyCredential, Credential) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PublicKeyCredential) +NS_INTERFACE_MAP_END_INHERITING(Credential) + +PublicKeyCredential::PublicKeyCredential(nsPIDOMWindowInner* aParent) + : Credential(aParent), mRawIdCachedObj(nullptr) { + mozilla::HoldJSObjects(this); +} + +PublicKeyCredential::~PublicKeyCredential() { mozilla::DropJSObjects(this); } + +JSObject* PublicKeyCredential::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return PublicKeyCredential_Binding::Wrap(aCx, this, aGivenProto); +} + +void PublicKeyCredential::GetRawId(JSContext* aCx, + JS::MutableHandle<JSObject*> aValue, + ErrorResult& aRv) { + if (!mRawIdCachedObj) { + mRawIdCachedObj = ArrayBuffer::Create(aCx, mRawId, aRv); + if (aRv.Failed()) { + return; + } + } + aValue.set(mRawIdCachedObj); +} + +void PublicKeyCredential::GetAuthenticatorAttachment( + DOMString& aAuthenticatorAttachment) { + if (mAuthenticatorAttachment.isSome()) { + aAuthenticatorAttachment.SetKnownLiveString(mAuthenticatorAttachment.ref()); + } else { + aAuthenticatorAttachment.SetNull(); + } +} + +already_AddRefed<AuthenticatorResponse> PublicKeyCredential::Response() const { + 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::SetAuthenticatorAttachment( + const Maybe<nsString>& aAuthenticatorAttachment) { + mAuthenticatorAttachment = aAuthenticatorAttachment; +} + +void PublicKeyCredential::SetAttestationResponse( + const RefPtr<AuthenticatorAttestationResponse>& aAttestationResponse) { + mAttestationResponse = aAttestationResponse; +} + +void PublicKeyCredential::SetAssertionResponse( + const RefPtr<AuthenticatorAssertionResponse>& aAssertionResponse) { + mAssertionResponse = aAssertionResponse; +} + +/* static */ +already_AddRefed<Promise> +PublicKeyCredential::IsUserVerifyingPlatformAuthenticatorAvailable( + GlobalObject& aGlobal, ErrorResult& aError) { + nsCOMPtr<nsPIDOMWindowInner> window = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!window) { + aError.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<WebAuthnManager> manager = + window->Navigator()->Credentials()->GetWebAuthnManager(); + return manager->IsUVPAA(aGlobal, aError); +} + +/* static */ +already_AddRefed<Promise> PublicKeyCredential::IsConditionalMediationAvailable( + GlobalObject& aGlobal, ErrorResult& aError) { + RefPtr<Promise> promise = + Promise::Create(xpc::CurrentNativeGlobal(aGlobal.Context()), aError); + if (aError.Failed()) { + return nullptr; + } +#if defined(MOZ_WIDGET_ANDROID) + promise->MaybeResolve(false); +#else + promise->MaybeResolve( + StaticPrefs::security_webauthn_enable_conditional_mediation()); +#endif + return promise.forget(); +} + +void PublicKeyCredential::GetClientExtensionResults( + AuthenticationExtensionsClientOutputs& aResult) { + 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; + } + if (mAuthenticatorAttachment.isSome()) { + json.mAuthenticatorAttachment.Construct(); + json.mAuthenticatorAttachment.Value() = mAuthenticatorAttachment.ref(); + } + if (mClientExtensionOutputs.mCredProps.WasPassed()) { + json.mClientExtensionResults.mCredProps.Construct( + mClientExtensionOutputs.mCredProps.Value()); + } + 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; + } + if (mAuthenticatorAttachment.isSome()) { + json.mAuthenticatorAttachment.Construct(); + json.mAuthenticatorAttachment.Value() = mAuthenticatorAttachment.ref(); + } + 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; +} + +void PublicKeyCredential::SetClientExtensionResultCredPropsRk(bool aResult) { + mClientExtensionOutputs.mCredProps.Construct(); + mClientExtensionOutputs.mCredProps.Value().mRk.Construct(); + mClientExtensionOutputs.mCredProps.Value().mRk.Value() = aResult; +} + +void PublicKeyCredential::SetClientExtensionResultHmacSecret( + bool aHmacCreateSecret) { + mClientExtensionOutputs.mHmacCreateSecret.Construct(); + 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().mCredProps.WasPassed()) { + aResult.mExtensions.mCredProps.Construct( + aOptions.mExtensions.Value().mCredProps.Value()); + } + if (aOptions.mExtensions.Value().mHmacCreateSecret.WasPassed()) { + aResult.mExtensions.mHmacCreateSecret.Construct( + aOptions.mExtensions.Value().mHmacCreateSecret.Value()); + } + if (aOptions.mExtensions.Value().mMinPinLength.WasPassed()) { + aResult.mExtensions.mMinPinLength.Construct( + aOptions.mExtensions.Value().mMinPinLength.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().mCredProps.WasPassed()) { + aResult.mExtensions.mCredProps.Construct( + aOptions.mExtensions.Value().mCredProps.Value()); + } + if (aOptions.mExtensions.Value().mHmacCreateSecret.WasPassed()) { + aResult.mExtensions.mHmacCreateSecret.Construct( + aOptions.mExtensions.Value().mHmacCreateSecret.Value()); + } + if (aOptions.mExtensions.Value().mMinPinLength.WasPassed()) { + aResult.mExtensions.mMinPinLength.Construct( + aOptions.mExtensions.Value().mMinPinLength.Value()); + } + } +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/PublicKeyCredential.h b/dom/webauthn/PublicKeyCredential.h new file mode 100644 index 0000000000..67163553c8 --- /dev/null +++ b/dom/webauthn/PublicKeyCredential.h @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PublicKeyCredential_h +#define mozilla_dom_PublicKeyCredential_h + +#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" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class PublicKeyCredential final : public Credential { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(PublicKeyCredential, + Credential) + + explicit PublicKeyCredential(nsPIDOMWindowInner* aParent); + + protected: + ~PublicKeyCredential() override; + + public: + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetRawId(JSContext* aCx, JS::MutableHandle<JSObject*> aValue, + ErrorResult& aRv); + + void GetAuthenticatorAttachment(DOMString& aAuthenticatorAttachment); + + already_AddRefed<AuthenticatorResponse> Response() const; + + void SetRawId(const nsTArray<uint8_t>& aBuffer); + + void SetAuthenticatorAttachment( + const Maybe<nsString>& aAuthenticatorAttachment); + + void SetAttestationResponse( + const RefPtr<AuthenticatorAttestationResponse>& aAttestationResponse); + void SetAssertionResponse( + const RefPtr<AuthenticatorAssertionResponse>& aAssertionResponse); + + static already_AddRefed<Promise> + IsUserVerifyingPlatformAuthenticatorAvailable(GlobalObject& aGlobal, + ErrorResult& aError); + + static already_AddRefed<Promise> IsConditionalMediationAvailable( + GlobalObject& aGlobal, ErrorResult& aError); + + void GetClientExtensionResults( + AuthenticationExtensionsClientOutputs& aResult); + + void ToJSON(JSContext* aCx, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aError); + + void SetClientExtensionResultAppId(bool aResult); + + void SetClientExtensionResultCredPropsRk(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; + Maybe<nsString> mAuthenticatorAttachment; + RefPtr<AuthenticatorAttestationResponse> mAttestationResponse; + RefPtr<AuthenticatorAssertionResponse> mAssertionResponse; + AuthenticationExtensionsClientOutputs mClientExtensionOutputs; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_PublicKeyCredential_h diff --git a/dom/webauthn/WebAuthnArgs.cpp b/dom/webauthn/WebAuthnArgs.cpp new file mode 100644 index 0000000000..7c78d39bef --- /dev/null +++ b/dom/webauthn/WebAuthnArgs.cpp @@ -0,0 +1,257 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WebAuthnArgs.h" +#include "WebAuthnEnumStrings.h" +#include "WebAuthnUtil.h" +#include "mozilla/dom/PWebAuthnTransactionParent.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(WebAuthnRegisterArgs, nsIWebAuthnRegisterArgs) + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetOrigin(nsAString& aOrigin) { + aOrigin = mInfo.Origin(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetChallenge(nsTArray<uint8_t>& aChallenge) { + aChallenge.Assign(mInfo.Challenge()); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetClientDataJSON(nsACString& aClientDataJSON) { + aClientDataJSON = mInfo.ClientDataJSON(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetClientDataHash(nsTArray<uint8_t>& aClientDataHash) { + nsresult rv = HashCString(mInfo.ClientDataJSON(), aClientDataHash); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetRpId(nsAString& aRpId) { + aRpId = mInfo.RpId(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetRpName(nsAString& aRpName) { + aRpName = mInfo.Rp().Name(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetUserId(nsTArray<uint8_t>& aUserId) { + aUserId.Assign(mInfo.User().Id()); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetUserName(nsAString& aUserName) { + aUserName = mInfo.User().Name(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetUserDisplayName(nsAString& aUserDisplayName) { + aUserDisplayName = mInfo.User().DisplayName(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetCoseAlgs(nsTArray<int32_t>& aCoseAlgs) { + aCoseAlgs.Clear(); + for (const CoseAlg& coseAlg : mInfo.coseAlgs()) { + aCoseAlgs.AppendElement(coseAlg.alg()); + } + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetExcludeList( + nsTArray<nsTArray<uint8_t> >& aExcludeList) { + aExcludeList.Clear(); + for (const WebAuthnScopedCredential& cred : mInfo.ExcludeList()) { + aExcludeList.AppendElement(cred.id().Clone()); + } + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetExcludeListTransports( + nsTArray<uint8_t>& aExcludeListTransports) { + aExcludeListTransports.Clear(); + for (const WebAuthnScopedCredential& cred : mInfo.ExcludeList()) { + aExcludeListTransports.AppendElement(cred.transports()); + } + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetCredProps(bool* aCredProps) { + *aCredProps = mCredProps; + + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetHmacCreateSecret(bool* aHmacCreateSecret) { + *aHmacCreateSecret = mHmacCreateSecret; + + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetMinPinLength(bool* aMinPinLength) { + *aMinPinLength = mMinPinLength; + + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetResidentKey(nsAString& aResidentKey) { + aResidentKey = mInfo.AuthenticatorSelection().residentKey(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetUserVerification( + nsAString& aUserVerificationRequirement) { + aUserVerificationRequirement = + mInfo.AuthenticatorSelection().userVerificationRequirement(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetAuthenticatorAttachment( + nsAString& aAuthenticatorAttachment) { + if (mInfo.AuthenticatorSelection().authenticatorAttachment().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + aAuthenticatorAttachment = + *mInfo.AuthenticatorSelection().authenticatorAttachment(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetTimeoutMS(uint32_t* aTimeoutMS) { + *aTimeoutMS = mInfo.TimeoutMS(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterArgs::GetAttestationConveyancePreference( + nsAString& aAttestationConveyancePreference) { + aAttestationConveyancePreference = mInfo.attestationConveyancePreference(); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(WebAuthnSignArgs, nsIWebAuthnSignArgs) + +NS_IMETHODIMP +WebAuthnSignArgs::GetOrigin(nsAString& aOrigin) { + aOrigin = mInfo.Origin(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignArgs::GetRpId(nsAString& aRpId) { + aRpId = mInfo.RpId(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignArgs::GetChallenge(nsTArray<uint8_t>& aChallenge) { + aChallenge.Assign(mInfo.Challenge()); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignArgs::GetClientDataJSON(nsACString& aClientDataJSON) { + aClientDataJSON = mInfo.ClientDataJSON(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignArgs::GetClientDataHash(nsTArray<uint8_t>& aClientDataHash) { + nsresult rv = HashCString(mInfo.ClientDataJSON(), aClientDataHash); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignArgs::GetAllowList(nsTArray<nsTArray<uint8_t> >& aAllowList) { + aAllowList.Clear(); + for (const WebAuthnScopedCredential& cred : mInfo.AllowList()) { + aAllowList.AppendElement(cred.id().Clone()); + } + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignArgs::GetAllowListTransports( + nsTArray<uint8_t>& aAllowListTransports) { + aAllowListTransports.Clear(); + for (const WebAuthnScopedCredential& cred : mInfo.AllowList()) { + aAllowListTransports.AppendElement(cred.transports()); + } + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignArgs::GetHmacCreateSecret(bool* aHmacCreateSecret) { + for (const WebAuthnExtension& ext : mInfo.Extensions()) { + if (ext.type() == WebAuthnExtension::TWebAuthnExtensionHmacSecret) { + *aHmacCreateSecret = + ext.get_WebAuthnExtensionHmacSecret().hmacCreateSecret(); + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +WebAuthnSignArgs::GetAppId(nsAString& aAppId) { + if (mAppId.isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + aAppId = mAppId.ref(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignArgs::GetUserVerification(nsAString& aUserVerificationRequirement) { + aUserVerificationRequirement = mInfo.userVerificationRequirement(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignArgs::GetTimeoutMS(uint32_t* aTimeoutMS) { + *aTimeoutMS = mInfo.TimeoutMS(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignArgs::GetConditionallyMediated(bool* aConditionallyMediated) { + *aConditionallyMediated = mInfo.ConditionallyMediated(); + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/WebAuthnArgs.h b/dom/webauthn/WebAuthnArgs.h new file mode 100644 index 0000000000..1b02646e62 --- /dev/null +++ b/dom/webauthn/WebAuthnArgs.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WebAuthnArgs_H_ +#define mozilla_dom_WebAuthnArgs_H_ + +#include "mozilla/dom/WebAuthnTransactionChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsIWebAuthnArgs.h" + +namespace mozilla::dom { + +class WebAuthnRegisterArgs final : public nsIWebAuthnRegisterArgs { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBAUTHNREGISTERARGS + + explicit WebAuthnRegisterArgs(const WebAuthnMakeCredentialInfo& aInfo) + : mInfo(aInfo), + mCredProps(false), + mHmacCreateSecret(false), + mMinPinLength(false) { + for (const WebAuthnExtension& ext : mInfo.Extensions()) { + switch (ext.type()) { + case WebAuthnExtension::TWebAuthnExtensionCredProps: + mCredProps = ext.get_WebAuthnExtensionCredProps().credProps(); + break; + case WebAuthnExtension::TWebAuthnExtensionHmacSecret: + mHmacCreateSecret = + ext.get_WebAuthnExtensionHmacSecret().hmacCreateSecret(); + break; + case WebAuthnExtension::TWebAuthnExtensionMinPinLength: + mMinPinLength = + ext.get_WebAuthnExtensionMinPinLength().minPinLength(); + break; + case WebAuthnExtension::TWebAuthnExtensionAppId: + break; + case WebAuthnExtension::T__None: + break; + } + } + } + + private: + ~WebAuthnRegisterArgs() = default; + + const WebAuthnMakeCredentialInfo mInfo; + + // Flags to indicate whether an extension is being requested. + bool mCredProps; + bool mHmacCreateSecret; + bool mMinPinLength; +}; + +class WebAuthnSignArgs final : public nsIWebAuthnSignArgs { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBAUTHNSIGNARGS + + explicit WebAuthnSignArgs(const WebAuthnGetAssertionInfo& aInfo) + : mInfo(aInfo) { + for (const WebAuthnExtension& ext : mInfo.Extensions()) { + switch (ext.type()) { + case WebAuthnExtension::TWebAuthnExtensionAppId: + mAppId = Some(ext.get_WebAuthnExtensionAppId().appIdentifier()); + break; + case WebAuthnExtension::TWebAuthnExtensionCredProps: + break; + case WebAuthnExtension::TWebAuthnExtensionHmacSecret: + break; + case WebAuthnExtension::TWebAuthnExtensionMinPinLength: + break; + case WebAuthnExtension::T__None: + break; + } + } + } + + private: + ~WebAuthnSignArgs() = default; + + const WebAuthnGetAssertionInfo mInfo; + Maybe<nsString> mAppId; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WebAuthnArgs_H_ diff --git a/dom/webauthn/WebAuthnAutoFillEntry.cpp b/dom/webauthn/WebAuthnAutoFillEntry.cpp new file mode 100644 index 0000000000..78423d1df2 --- /dev/null +++ b/dom/webauthn/WebAuthnAutoFillEntry.cpp @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WebAuthnAutoFillEntry.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(WebAuthnAutoFillEntry, nsIWebAuthnAutoFillEntry) + +NS_IMETHODIMP +WebAuthnAutoFillEntry::GetProvider(uint8_t* aProvider) { + *aProvider = mProvider; + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnAutoFillEntry::GetUserName(nsAString& aUserName) { + aUserName.Assign(mUserName); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnAutoFillEntry::GetRpId(nsAString& aRpId) { + aRpId.Assign(mRpId); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnAutoFillEntry::GetCredentialId(nsTArray<uint8_t>& aCredentialId) { + aCredentialId.Assign(mCredentialId); + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/WebAuthnAutoFillEntry.h b/dom/webauthn/WebAuthnAutoFillEntry.h new file mode 100644 index 0000000000..53fb040562 --- /dev/null +++ b/dom/webauthn/WebAuthnAutoFillEntry.h @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WebAuthnAutoFillEntry_h_ +#define mozilla_dom_WebAuthnAutoFillEntry_h_ + +#include "nsIWebAuthnService.h" +#include "nsString.h" + +#ifdef XP_WIN +# include <windows.h> +# include "winwebauthn/webauthn.h" +#endif + +namespace mozilla::dom { + +class WebAuthnAutoFillEntry final : public nsIWebAuthnAutoFillEntry { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBAUTHNAUTOFILLENTRY + + WebAuthnAutoFillEntry(uint8_t aProvider, const nsAString& aUserName, + const nsAString& aRpId, + const nsTArray<uint8_t>& aCredentialId) + : mProvider(aProvider), mUserName(aUserName), mRpId(aRpId) { + mCredentialId.Assign(aCredentialId); + } + +#ifdef XP_WIN + explicit WebAuthnAutoFillEntry( + PCWEBAUTHN_CREDENTIAL_DETAILS aCredentialDetails) { + mProvider = nsIWebAuthnAutoFillEntry::PROVIDER_PLATFORM_WINDOWS; + mUserName.Assign(aCredentialDetails->pUserInformation->pwszName); + mRpId.Assign(aCredentialDetails->pRpInformation->pwszId); + mCredentialId.AppendElements(aCredentialDetails->pbCredentialID, + aCredentialDetails->cbCredentialID); + } +#endif + + private: + ~WebAuthnAutoFillEntry() = default; + + uint8_t mProvider; + nsString mUserName; + nsString mRpId; + nsTArray<uint8_t> mCredentialId; +}; + +} // namespace mozilla::dom +#endif // mozilla_dom_WebAuthnAutoFillEntry_h_ diff --git a/dom/webauthn/WebAuthnCoseIdentifiers.h b/dom/webauthn/WebAuthnCoseIdentifiers.h new file mode 100644 index 0000000000..9e98b5623d --- /dev/null +++ b/dom/webauthn/WebAuthnCoseIdentifiers.h @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WebAuthnCoseIdentifiers_h +#define mozilla_dom_WebAuthnCoseIdentifiers_h + +#include "mozilla/dom/WebCryptoCommon.h" + +namespace mozilla::dom { + +// From https://www.iana.org/assignments/cose/cose.xhtml#algorithms +enum class CoseAlgorithmIdentifier : int32_t { + ES256 = -7, + RS256 = -257, +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WebAuthnCoseIdentifiers_h diff --git a/dom/webauthn/WebAuthnEnumStrings.h b/dom/webauthn/WebAuthnEnumStrings.h new file mode 100644 index 0000000000..35cca2ed0c --- /dev/null +++ b/dom/webauthn/WebAuthnEnumStrings.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WebAuthnEnumStrings_h +#define mozilla_dom_WebAuthnEnumStrings_h + +// WARNING: This version number must match the WebAuthn level where the strings +// below are defined. +#define MOZ_WEBAUTHN_ENUM_STRINGS_VERSION 3 + +// https://www.w3.org/TR/webauthn-2/#enum-attestation-convey +#define MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE "none" +#define MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT "indirect" +#define MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT "direct" +#define MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE "enterprise" +// WARNING: Change version number when adding new values! + +// https://www.w3.org/TR/webauthn-2/#enum-attachment +#define MOZ_WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM "platform" +#define MOZ_WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM "cross-platform" +// WARNING: Change version number when adding new values! + +// https://www.w3.org/TR/webauthn-2/#enum-credentialType +#define MOZ_WEBAUTHN_PUBLIC_KEY_CREDENTIAL_TYPE_PUBLIC_KEY "public-key" +// WARNING: Change version number when adding new values! + +// https://www.w3.org/TR/webauthn-2/#enum-residentKeyRequirement +#define MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED "required" +#define MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_PREFERRED "preferred" +#define MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED "discouraged" +// WARNING: Change version number when adding new values! + +// https://www.w3.org/TR/webauthn-2/#enum-userVerificationRequirement +#define MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED "required" +#define MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED "preferred" +#define MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED "discouraged" +// WARNING: Change version number when adding new values! + +// https://www.w3.org/TR/webauthn-2/#enum-transport +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_USB "usb" +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_NFC "nfc" +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_BLE "ble" +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_INTERNAL "internal" +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_HYBRID "hybrid" +// WARNING: Change version number when adding new values! + +#endif // mozilla_dom_WebAuthnEnumStrings_h diff --git a/dom/webauthn/WebAuthnManager.cpp b/dom/webauthn/WebAuthnManager.cpp new file mode 100644 index 0000000000..e4d18ebc45 --- /dev/null +++ b/dom/webauthn/WebAuthnManager.cpp @@ -0,0 +1,838 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "hasht.h" +#include "nsHTMLDocument.h" +#include "nsIURIMutator.h" +#include "nsThreadUtils.h" +#include "WebAuthnCoseIdentifiers.h" +#include "WebAuthnEnumStrings.h" +#include "WebAuthnTransportIdentifiers.h" +#include "mozilla/Base64.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/AuthenticatorAssertionResponse.h" +#include "mozilla/dom/AuthenticatorAttestationResponse.h" +#include "mozilla/dom/PublicKeyCredential.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PWebAuthnTransaction.h" +#include "mozilla/dom/PWebAuthnTransactionChild.h" +#include "mozilla/dom/WebAuthnManager.h" +#include "mozilla/dom/WebAuthnTransactionChild.h" +#include "mozilla/dom/WebAuthnUtil.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" + +#ifdef XP_WIN +# include "WinWebAuthnService.h" +#endif + +using namespace mozilla::ipc; + +namespace mozilla::dom { + +/*********************************************************************** + * Statics + **********************************************************************/ + +namespace { +static mozilla::LazyLogModule gWebAuthnManagerLog("webauthnmanager"); +} + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(WebAuthnManager, + WebAuthnManagerBase) + +NS_IMPL_CYCLE_COLLECTION_CLASS(WebAuthnManager) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WebAuthnManager, + WebAuthnManagerBase) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransaction) + tmp->mTransaction.reset(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WebAuthnManager, + WebAuthnManagerBase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +/*********************************************************************** + * Utility Functions + **********************************************************************/ + +static nsresult AssembleClientData( + const nsAString& aOrigin, const CryptoBuffer& aChallenge, + const nsAString& aType, + const AuthenticationExtensionsClientInputs& aExtensions, + /* out */ nsACString& aJsonOut) { + MOZ_ASSERT(NS_IsMainThread()); + + nsString challengeBase64; + nsresult rv = aChallenge.ToJwkBase64(challengeBase64); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + CollectedClientData clientDataObject; + clientDataObject.mType.Assign(aType); + clientDataObject.mChallenge.Assign(challengeBase64); + clientDataObject.mOrigin.Assign(aOrigin); + + nsAutoString temp; + if (NS_WARN_IF(!clientDataObject.ToJSON(temp))) { + return NS_ERROR_FAILURE; + } + + aJsonOut.Assign(NS_ConvertUTF16toUTF8(temp)); + return NS_OK; +} + +static uint8_t SerializeTransports( + const mozilla::dom::Sequence<nsString>& aTransports) { + uint8_t transports = 0; + + // We ignore unknown transports for forward-compatibility, but this + // needs to be reviewed if values are added to the + // AuthenticatorTransport enum. + static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); + for (const nsAString& str : aTransports) { + if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_USB)) { + transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB; + } else if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_NFC)) { + transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC; + } else if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_BLE)) { + transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE; + } else if (str.EqualsLiteral( + MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_INTERNAL)) { + transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL; + } else if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_HYBRID)) { + transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_HYBRID; + } + } + return transports; +} + +nsresult GetOrigin(nsPIDOMWindowInner* aParent, + /*out*/ nsAString& aOrigin, /*out*/ nsACString& aHost) { + MOZ_ASSERT(aParent); + nsCOMPtr<Document> doc = aParent->GetDoc(); + MOZ_ASSERT(doc); + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + nsresult rv = + nsContentUtils::GetWebExposedOriginSerialization(principal, aOrigin); + if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(aOrigin.IsEmpty())) { + return NS_ERROR_FAILURE; + } + + if (principal->GetIsIpAddress()) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + if (aOrigin.EqualsLiteral("null")) { + // 4.1.1.3 If callerOrigin is an opaque origin, reject promise with a + // DOMException whose name is "NotAllowedError", and terminate this + // algorithm + MOZ_LOG(gWebAuthnManagerLog, LogLevel::Debug, + ("Rejecting due to opaque origin")); + return NS_ERROR_DOM_NOT_ALLOWED_ERR; + } + + nsCOMPtr<nsIURI> originUri; + auto* basePrin = BasePrincipal::Cast(principal); + if (NS_FAILED(basePrin->GetURI(getter_AddRefs(originUri)))) { + return NS_ERROR_FAILURE; + } + if (NS_FAILED(originUri->GetAsciiHost(aHost))) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult RelaxSameOrigin(nsPIDOMWindowInner* aParent, + const nsAString& aInputRpId, + /* out */ nsACString& aRelaxedRpId) { + MOZ_ASSERT(aParent); + nsCOMPtr<Document> doc = aParent->GetDoc(); + MOZ_ASSERT(doc); + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + auto* basePrin = BasePrincipal::Cast(principal); + nsCOMPtr<nsIURI> uri; + + if (NS_FAILED(basePrin->GetURI(getter_AddRefs(uri)))) { + return NS_ERROR_FAILURE; + } + nsAutoCString originHost; + if (NS_FAILED(uri->GetAsciiHost(originHost))) { + return NS_ERROR_FAILURE; + } + nsCOMPtr<Document> document = aParent->GetDoc(); + if (!document || !document->IsHTMLDocument()) { + return NS_ERROR_FAILURE; + } + nsHTMLDocument* html = document->AsHTMLDocument(); + // See if the given RP ID is a valid domain string. + // (We use the document's URI here as a template so we don't have to come up + // with our own scheme, etc. If we can successfully set the host as the given + // RP ID, then it should be a valid domain string.) + nsCOMPtr<nsIURI> inputRpIdURI; + nsresult rv = NS_MutateURI(uri) + .SetHost(NS_ConvertUTF16toUTF8(aInputRpId)) + .Finalize(inputRpIdURI); + if (NS_FAILED(rv)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + nsAutoCString inputRpId; + if (NS_FAILED(inputRpIdURI->GetAsciiHost(inputRpId))) { + return NS_ERROR_FAILURE; + } + if (!html->IsRegistrableDomainSuffixOfOrEqualTo( + NS_ConvertUTF8toUTF16(inputRpId), originHost)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + aRelaxedRpId.Assign(inputRpId); + return NS_OK; +} + +/*********************************************************************** + * WebAuthnManager Implementation + **********************************************************************/ + +void WebAuthnManager::ClearTransaction() { + mTransaction.reset(); + Unfollow(); +} + +void WebAuthnManager::CancelParent() { + if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) { + mChild->SendRequestCancel(mTransaction.ref().mId); + } +} + +WebAuthnManager::~WebAuthnManager() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mTransaction.isSome()) { + ClearTransaction(); + } + + if (mChild) { + RefPtr<WebAuthnTransactionChild> c; + mChild.swap(c); + c->Disconnect(); + } +} + +already_AddRefed<Promise> WebAuthnManager::MakeCredential( + const PublicKeyCredentialCreationOptions& aOptions, + const Optional<OwningNonNull<AbortSignal>>& aSignal, ErrorResult& aError) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent); + + RefPtr<Promise> promise = Promise::Create(global, aError); + if (aError.Failed()) { + return nullptr; + } + + if (mTransaction.isSome()) { + // abort the old transaction and take over control from here. + CancelTransaction(NS_ERROR_DOM_ABORT_ERR); + } + + nsString origin; + nsCString rpId; + nsresult rv = GetOrigin(mParent, origin, rpId); + if (NS_WARN_IF(NS_FAILED(rv))) { + promise->MaybeReject(rv); + return promise.forget(); + } + + // Enforce 5.4.3 User Account Parameters for Credential Generation + // When we add UX, we'll want to do more with this value, but for now + // we just have to verify its correctness. + + CryptoBuffer userId; + userId.Assign(aOptions.mUser.mId); + if (userId.Length() > 64) { + promise->MaybeRejectWithTypeError("user.id is too long"); + return promise.forget(); + } + + // If timeoutSeconds was specified, check if its value lies within a + // reasonable range as defined by the platform and if not, correct it to the + // closest value lying within that range. + + uint32_t adjustedTimeout = 30000; + if (aOptions.mTimeout.WasPassed()) { + adjustedTimeout = aOptions.mTimeout.Value(); + adjustedTimeout = std::max(15000u, adjustedTimeout); + adjustedTimeout = std::min(120000u, adjustedTimeout); + } + + if (aOptions.mRp.mId.WasPassed()) { + // If rpId is specified, then invoke the procedure used for relaxing the + // same-origin restriction by setting the document.domain attribute, using + // rpId as the given value but without changing the current document’s + // domain. If no errors are thrown, set rpId to the value of host as + // computed by this procedure, and rpIdHash to the SHA-256 hash of rpId. + // Otherwise, reject promise with a DOMException whose name is + // "SecurityError", and terminate this algorithm. + + if (NS_FAILED(RelaxSameOrigin(mParent, aOptions.mRp.mId.Value(), rpId))) { + promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return promise.forget(); + } + } + + // <https://w3c.github.io/webauthn/#sctn-appid-extension> + if (aOptions.mExtensions.mAppid.WasPassed()) { + promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return promise.forget(); + } + + // Process each element of mPubKeyCredParams using the following steps, to + // produce a new sequence of coseAlgos. + nsTArray<CoseAlg> coseAlgos; + // If pubKeyCredParams is empty, append ES256 and RS256 + if (aOptions.mPubKeyCredParams.IsEmpty()) { + coseAlgos.AppendElement(static_cast<long>(CoseAlgorithmIdentifier::ES256)); + coseAlgos.AppendElement(static_cast<long>(CoseAlgorithmIdentifier::RS256)); + } else { + for (size_t a = 0; a < aOptions.mPubKeyCredParams.Length(); ++a) { + // If current.type does not contain a PublicKeyCredentialType + // supported by this implementation, then stop processing current and move + // on to the next element in mPubKeyCredParams. + if (!aOptions.mPubKeyCredParams[a].mType.EqualsLiteral( + MOZ_WEBAUTHN_PUBLIC_KEY_CREDENTIAL_TYPE_PUBLIC_KEY)) { + continue; + } + + coseAlgos.AppendElement(aOptions.mPubKeyCredParams[a].mAlg); + } + } + + // If there are algorithms specified, but none are Public_key algorithms, + // reject the promise. + if (coseAlgos.IsEmpty() && !aOptions.mPubKeyCredParams.IsEmpty()) { + promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return promise.forget(); + } + + // If excludeList is undefined, set it to the empty list. + // + // If extensions was specified, process any extensions supported by this + // client platform, to produce the extension data that needs to be sent to the + // authenticator. If an error is encountered while processing an extension, + // skip that extension and do not produce any extension data for it. Call the + // result of this processing clientExtensions. + // + // Currently no extensions are supported + // + // Use attestationChallenge, callerOrigin and rpId, along with the token + // binding key associated with callerOrigin (if any), to create a ClientData + // structure representing this request. Choose a hash algorithm for hashAlg + // and compute the clientDataJSON and clientDataHash. + + CryptoBuffer challenge; + if (!challenge.Assign(aOptions.mChallenge)) { + promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return promise.forget(); + } + + nsAutoCString clientDataJSON; + nsresult srv = AssembleClientData(origin, challenge, u"webauthn.create"_ns, + aOptions.mExtensions, clientDataJSON); + if (NS_WARN_IF(NS_FAILED(srv))) { + promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return promise.forget(); + } + + nsTArray<WebAuthnScopedCredential> excludeList; + for (const auto& s : aOptions.mExcludeCredentials) { + WebAuthnScopedCredential c; + CryptoBuffer cb; + cb.Assign(s.mId); + c.id() = cb; + if (s.mTransports.WasPassed()) { + c.transports() = SerializeTransports(s.mTransports.Value()); + } + excludeList.AppendElement(c); + } + + if (!MaybeCreateBackgroundActor()) { + promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR); + return promise.forget(); + } + + // TODO: Add extension list building + nsTArray<WebAuthnExtension> extensions; + + // <https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#sctn-hmac-secret-extension> + if (aOptions.mExtensions.mHmacCreateSecret.WasPassed()) { + bool hmacCreateSecret = aOptions.mExtensions.mHmacCreateSecret.Value(); + if (hmacCreateSecret) { + extensions.AppendElement(WebAuthnExtensionHmacSecret(hmacCreateSecret)); + } + } + + if (aOptions.mExtensions.mCredProps.WasPassed()) { + bool credProps = aOptions.mExtensions.mCredProps.Value(); + if (credProps) { + extensions.AppendElement(WebAuthnExtensionCredProps(credProps)); + } + } + + if (aOptions.mExtensions.mMinPinLength.WasPassed()) { + bool minPinLength = aOptions.mExtensions.mMinPinLength.Value(); + if (minPinLength) { + extensions.AppendElement(WebAuthnExtensionMinPinLength(minPinLength)); + } + } + + const auto& selection = aOptions.mAuthenticatorSelection; + const auto& attachment = selection.mAuthenticatorAttachment; + const nsString& attestation = aOptions.mAttestation; + + // Attachment + Maybe<nsString> authenticatorAttachment; + if (attachment.WasPassed()) { + authenticatorAttachment.emplace(attachment.Value()); + } + + // The residentKey field was added in WebAuthn level 2. It takes precedent + // over the requireResidentKey field if and only if it is present and it is a + // member of the ResidentKeyRequirement enum. + static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); + bool useResidentKeyValue = + selection.mResidentKey.WasPassed() && + (selection.mResidentKey.Value().EqualsLiteral( + MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED) || + selection.mResidentKey.Value().EqualsLiteral( + MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_PREFERRED) || + selection.mResidentKey.Value().EqualsLiteral( + MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED)); + + nsString residentKey; + if (useResidentKeyValue) { + residentKey = selection.mResidentKey.Value(); + } else { + // "If no value is given then the effective value is required if + // requireResidentKey is true or discouraged if it is false or absent." + if (selection.mRequireResidentKey) { + residentKey.AssignLiteral(MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED); + } else { + residentKey.AssignLiteral( + MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED); + } + } + + // Create and forward authenticator selection criteria. + WebAuthnAuthenticatorSelection authSelection( + residentKey, selection.mUserVerification, authenticatorAttachment); + + WebAuthnMakeCredentialRpInfo rpInfo(aOptions.mRp.mName); + + WebAuthnMakeCredentialUserInfo userInfo(userId, aOptions.mUser.mName, + aOptions.mUser.mDisplayName); + + BrowsingContext* context = mParent->GetBrowsingContext(); + if (!context) { + promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR); + return promise.forget(); + } + + // Abort the request if aborted flag is already set. + if (aSignal.WasPassed() && aSignal.Value().Aborted()) { + AutoJSAPI jsapi; + if (!jsapi.Init(global)) { + promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + return promise.forget(); + } + JSContext* cx = jsapi.cx(); + JS::Rooted<JS::Value> reason(cx); + aSignal.Value().GetReason(cx, &reason); + promise->MaybeReject(reason); + return promise.forget(); + } + + WebAuthnMakeCredentialInfo info( + origin, NS_ConvertUTF8toUTF16(rpId), challenge, clientDataJSON, + adjustedTimeout, excludeList, rpInfo, userInfo, coseAlgos, extensions, + authSelection, attestation, context->Top()->Id()); + + // Set up the transaction state. Fallible operations should not be performed + // below this line, as we must not leave the transaction state partially + // initialized. Once the transaction state is initialized the only valid ways + // to end the transaction are CancelTransaction, RejectTransaction, and + // FinishMakeCredential. + AbortSignal* signal = nullptr; + if (aSignal.WasPassed()) { + signal = &aSignal.Value(); + Follow(signal); + } + + MOZ_ASSERT(mTransaction.isNothing()); + mTransaction = Some(WebAuthnTransaction(promise)); + mChild->SendRequestRegister(mTransaction.ref().mId, info); + + return promise.forget(); +} + +const size_t MAX_ALLOWED_CREDENTIALS = 20; + +already_AddRefed<Promise> WebAuthnManager::GetAssertion( + const PublicKeyCredentialRequestOptions& aOptions, + const bool aConditionallyMediated, + const Optional<OwningNonNull<AbortSignal>>& aSignal, ErrorResult& aError) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent); + + RefPtr<Promise> promise = Promise::Create(global, aError); + if (aError.Failed()) { + return nullptr; + } + + if (mTransaction.isSome()) { + // abort the old transaction and take over control from here. + CancelTransaction(NS_ERROR_DOM_ABORT_ERR); + } + + nsString origin; + nsCString rpId; + nsresult rv = GetOrigin(mParent, origin, rpId); + if (NS_WARN_IF(NS_FAILED(rv))) { + promise->MaybeReject(rv); + return promise.forget(); + } + + // If timeoutSeconds was specified, check if its value lies within a + // reasonable range as defined by the platform and if not, correct it to the + // closest value lying within that range. + + uint32_t adjustedTimeout = 30000; + if (aOptions.mTimeout.WasPassed()) { + adjustedTimeout = aOptions.mTimeout.Value(); + adjustedTimeout = std::max(15000u, adjustedTimeout); + adjustedTimeout = std::min(120000u, adjustedTimeout); + } + + if (aOptions.mRpId.WasPassed()) { + // If rpId is specified, then invoke the procedure used for relaxing the + // same-origin restriction by setting the document.domain attribute, using + // rpId as the given value but without changing the current document’s + // domain. If no errors are thrown, set rpId to the value of host as + // computed by this procedure, and rpIdHash to the SHA-256 hash of rpId. + // Otherwise, reject promise with a DOMException whose name is + // "SecurityError", and terminate this algorithm. + + if (NS_FAILED(RelaxSameOrigin(mParent, aOptions.mRpId.Value(), rpId))) { + promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return promise.forget(); + } + } + + // Abort the request if the allowCredentials set is too large + if (aOptions.mAllowCredentials.Length() > MAX_ALLOWED_CREDENTIALS) { + promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return promise.forget(); + } + + // Use assertionChallenge, callerOrigin and rpId, along with the token binding + // key associated with callerOrigin (if any), to create a ClientData structure + // representing this request. Choose a hash algorithm for hashAlg and compute + // the clientDataJSON and clientDataHash. + CryptoBuffer challenge; + if (!challenge.Assign(aOptions.mChallenge)) { + promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return promise.forget(); + } + + nsAutoCString clientDataJSON; + rv = AssembleClientData(origin, challenge, u"webauthn.get"_ns, + aOptions.mExtensions, clientDataJSON); + if (NS_WARN_IF(NS_FAILED(rv))) { + promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return promise.forget(); + } + + nsTArray<WebAuthnScopedCredential> allowList; + for (const auto& s : aOptions.mAllowCredentials) { + if (s.mType.EqualsLiteral( + MOZ_WEBAUTHN_PUBLIC_KEY_CREDENTIAL_TYPE_PUBLIC_KEY)) { + WebAuthnScopedCredential c; + CryptoBuffer cb; + cb.Assign(s.mId); + c.id() = cb; + if (s.mTransports.WasPassed()) { + c.transports() = SerializeTransports(s.mTransports.Value()); + } + allowList.AppendElement(c); + } + } + if (allowList.Length() == 0 && aOptions.mAllowCredentials.Length() != 0) { + promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR); + return promise.forget(); + } + + if (!MaybeCreateBackgroundActor()) { + promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR); + return promise.forget(); + } + + // 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; + + // credProps is only supported in MakeCredentials + if (aOptions.mExtensions.mCredProps.WasPassed()) { + promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return promise.forget(); + } + + // minPinLength is only supported in MakeCredentials + if (aOptions.mExtensions.mMinPinLength.WasPassed()) { + promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return promise.forget(); + } + + // <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, appId)) { + promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return promise.forget(); + } + + // Append the hash and send it to the backend. + extensions.AppendElement(WebAuthnExtensionAppId(appId)); + } + + BrowsingContext* context = mParent->GetBrowsingContext(); + if (!context) { + promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR); + return promise.forget(); + } + + // Abort the request if aborted flag is already set. + if (aSignal.WasPassed() && aSignal.Value().Aborted()) { + AutoJSAPI jsapi; + if (!jsapi.Init(global)) { + promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + return promise.forget(); + } + JSContext* cx = jsapi.cx(); + JS::Rooted<JS::Value> reason(cx); + aSignal.Value().GetReason(cx, &reason); + promise->MaybeReject(reason); + return promise.forget(); + } + + WebAuthnGetAssertionInfo info(origin, NS_ConvertUTF8toUTF16(rpId), challenge, + clientDataJSON, adjustedTimeout, allowList, + extensions, aOptions.mUserVerification, + aConditionallyMediated, context->Top()->Id()); + + // Set up the transaction state. Fallible operations should not be performed + // below this line, as we must not leave the transaction state partially + // initialized. Once the transaction state is initialized the only valid ways + // to end the transaction are CancelTransaction, RejectTransaction, and + // FinishGetAssertion. + AbortSignal* signal = nullptr; + if (aSignal.WasPassed()) { + signal = &aSignal.Value(); + Follow(signal); + } + + MOZ_ASSERT(mTransaction.isNothing()); + mTransaction = Some(WebAuthnTransaction(promise)); + mChild->SendRequestSign(mTransaction.ref().mId, info); + + return promise.forget(); +} + +already_AddRefed<Promise> WebAuthnManager::Store(const Credential& aCredential, + ErrorResult& aError) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent); + + RefPtr<Promise> promise = Promise::Create(global, aError); + if (aError.Failed()) { + return nullptr; + } + + if (mTransaction.isSome()) { + // abort the old transaction and take over control from here. + CancelTransaction(NS_ERROR_DOM_ABORT_ERR); + } + + promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return promise.forget(); +} + +already_AddRefed<Promise> WebAuthnManager::IsUVPAA(GlobalObject& aGlobal, + ErrorResult& aError) { + RefPtr<Promise> promise = + Promise::Create(xpc::CurrentNativeGlobal(aGlobal.Context()), aError); + if (aError.Failed()) { + return nullptr; + } + + if (!MaybeCreateBackgroundActor()) { + promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR); + return promise.forget(); + } + + mChild->SendRequestIsUVPAA()->Then( + GetCurrentSerialEventTarget(), __func__, + [promise](const PWebAuthnTransactionChild::RequestIsUVPAAPromise:: + ResolveOrRejectValue& aValue) { + if (aValue.IsResolve()) { + promise->MaybeResolve(aValue.ResolveValue()); + } else { + promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR); + } + }); + return promise.forget(); +} + +void WebAuthnManager::FinishMakeCredential( + const uint64_t& aTransactionId, + const WebAuthnMakeCredentialResult& aResult) { + MOZ_ASSERT(NS_IsMainThread()); + + // Check for a valid transaction. + if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) { + return; + } + + nsAutoCString keyHandleBase64Url; + nsresult rv = Base64URLEncode( + aResult.KeyHandle().Length(), aResult.KeyHandle().Elements(), + Base64URLEncodePaddingPolicy::Omit, keyHandleBase64Url); + if (NS_WARN_IF(NS_FAILED(rv))) { + RejectTransaction(rv); + return; + } + + // Create a new PublicKeyCredential object and populate its fields with the + // values returned from the authenticator as well as the clientDataJSON + // computed earlier. + RefPtr<AuthenticatorAttestationResponse> attestation = + new AuthenticatorAttestationResponse(mParent); + attestation->SetClientDataJSON(aResult.ClientDataJSON()); + attestation->SetAttestationObject(aResult.AttestationObject()); + attestation->SetTransports(aResult.Transports()); + + RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent); + credential->SetId(NS_ConvertASCIItoUTF16(keyHandleBase64Url)); + credential->SetType(u"public-key"_ns); + credential->SetRawId(aResult.KeyHandle()); + credential->SetAttestationResponse(attestation); + credential->SetAuthenticatorAttachment(aResult.AuthenticatorAttachment()); + + // Forward client extension results. + for (const auto& ext : aResult.Extensions()) { + if (ext.type() == + WebAuthnExtensionResult::TWebAuthnExtensionResultCredProps) { + bool credPropsRk = ext.get_WebAuthnExtensionResultCredProps().rk(); + credential->SetClientExtensionResultCredPropsRk(credPropsRk); + } + if (ext.type() == + WebAuthnExtensionResult::TWebAuthnExtensionResultHmacSecret) { + bool hmacCreateSecret = + ext.get_WebAuthnExtensionResultHmacSecret().hmacCreateSecret(); + credential->SetClientExtensionResultHmacSecret(hmacCreateSecret); + } + } + + mTransaction.ref().mPromise->MaybeResolve(credential); + ClearTransaction(); +} + +void WebAuthnManager::FinishGetAssertion( + const uint64_t& aTransactionId, const WebAuthnGetAssertionResult& aResult) { + MOZ_ASSERT(NS_IsMainThread()); + + // Check for a valid transaction. + if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) { + return; + } + + nsAutoCString keyHandleBase64Url; + nsresult rv = Base64URLEncode( + aResult.KeyHandle().Length(), aResult.KeyHandle().Elements(), + Base64URLEncodePaddingPolicy::Omit, keyHandleBase64Url); + if (NS_WARN_IF(NS_FAILED(rv))) { + RejectTransaction(rv); + return; + } + + // Create a new PublicKeyCredential object named value and populate its fields + // with the values returned from the authenticator as well as the + // clientDataJSON computed earlier. + RefPtr<AuthenticatorAssertionResponse> assertion = + new AuthenticatorAssertionResponse(mParent); + assertion->SetClientDataJSON(aResult.ClientDataJSON()); + assertion->SetAuthenticatorData(aResult.AuthenticatorData()); + assertion->SetSignature(aResult.Signature()); + assertion->SetUserHandle(aResult.UserHandle()); // may be empty + + RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent); + credential->SetId(NS_ConvertASCIItoUTF16(keyHandleBase64Url)); + credential->SetType(u"public-key"_ns); + credential->SetRawId(aResult.KeyHandle()); + credential->SetAssertionResponse(assertion); + credential->SetAuthenticatorAttachment(aResult.AuthenticatorAttachment()); + + // Forward client extension results. + for (const auto& ext : aResult.Extensions()) { + if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultAppId) { + bool appid = ext.get_WebAuthnExtensionResultAppId().AppId(); + credential->SetClientExtensionResultAppId(appid); + } + } + + mTransaction.ref().mPromise->MaybeResolve(credential); + ClearTransaction(); +} + +void WebAuthnManager::RequestAborted(const uint64_t& aTransactionId, + const nsresult& aError) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) { + RejectTransaction(aError); + } +} + +void WebAuthnManager::RunAbortAlgorithm() { + if (NS_WARN_IF(mTransaction.isNothing())) { + return; + } + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent); + + AutoJSAPI jsapi; + if (!jsapi.Init(global)) { + CancelTransaction(NS_ERROR_DOM_ABORT_ERR); + return; + } + JSContext* cx = jsapi.cx(); + JS::Rooted<JS::Value> reason(cx); + Signal()->GetReason(cx, &reason); + CancelTransaction(reason); +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/WebAuthnManager.h b/dom/webauthn/WebAuthnManager.h new file mode 100644 index 0000000000..f60635ec88 --- /dev/null +++ b/dom/webauthn/WebAuthnManager.h @@ -0,0 +1,160 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WebAuthnManager_h +#define mozilla_dom_WebAuthnManager_h + +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RandomNum.h" +#include "mozilla/dom/AbortSignal.h" +#include "mozilla/dom/PWebAuthnTransaction.h" +#include "mozilla/dom/WebAuthnManagerBase.h" + +/* + * Content process manager for the WebAuthn protocol. Created on calls to the + * WebAuthentication DOM object, this manager handles establishing IPC channels + * for WebAuthn transactions, as well as keeping track of JS Promise objects + * representing transactions in flight. + * + * The WebAuthn spec (https://www.w3.org/TR/webauthn/) allows for two different + * types of transactions: registration and signing. When either of these is + * requested via the DOM API, the following steps are executed in the + * WebAuthnManager: + * + * - Validation of the request. Return a failed promise to js if request does + * not have correct parameters. + * + * - If request is valid, open a new IPC channel for running the transaction. If + * another transaction is already running in this content process, cancel it. + * Return a pending promise to js. + * + * - Send transaction information to parent process (by running the Start* + * functions of WebAuthnManager). Assuming another transaction is currently in + * flight in another content process, parent will handle canceling it. + * + * - On return of successful transaction information from parent process, turn + * information into DOM object format required by spec, and resolve promise + * (by running the Finish* functions of WebAuthnManager). On cancellation + * request from parent, reject promise with corresponding error code. Either + * outcome will also close the IPC channel. + * + */ + +namespace mozilla::dom { + +class Credential; + +class WebAuthnTransaction { + public: + explicit WebAuthnTransaction(const RefPtr<Promise>& aPromise) + : mPromise(aPromise), mId(NextId()) { + MOZ_ASSERT(mId > 0); + } + + // JS Promise representing the transaction status. + RefPtr<Promise> mPromise; + + // Unique transaction id. + uint64_t mId; + + private: + // Generates a probabilistically unique ID for the new transaction. IDs are 53 + // bits, as they are used in javascript. We use a random value if possible, + // otherwise a counter. + static uint64_t NextId() { + static uint64_t counter = 0; + Maybe<uint64_t> rand = mozilla::RandomUint64(); + uint64_t id = + rand.valueOr(++counter) & UINT64_C(0x1fffffffffffff); // 2^53 - 1 + // The transaction ID 0 is reserved. + return id ? id : 1; + } +}; + +class WebAuthnManager final : public WebAuthnManagerBase, public AbortFollower { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(WebAuthnManager, WebAuthnManagerBase) + + explicit WebAuthnManager(nsPIDOMWindowInner* aParent) + : WebAuthnManagerBase(aParent) {} + + already_AddRefed<Promise> MakeCredential( + const PublicKeyCredentialCreationOptions& aOptions, + const Optional<OwningNonNull<AbortSignal>>& aSignal, ErrorResult& aError); + + already_AddRefed<Promise> GetAssertion( + const PublicKeyCredentialRequestOptions& aOptions, + const bool aConditionallyMediated, + const Optional<OwningNonNull<AbortSignal>>& aSignal, ErrorResult& aError); + + already_AddRefed<Promise> Store(const Credential& aCredential, + ErrorResult& aError); + + already_AddRefed<Promise> IsUVPAA(GlobalObject& aGlobal, ErrorResult& aError); + + // WebAuthnManagerBase + + void FinishMakeCredential( + const uint64_t& aTransactionId, + const WebAuthnMakeCredentialResult& aResult) override; + + void FinishGetAssertion(const uint64_t& aTransactionId, + const WebAuthnGetAssertionResult& aResult) override; + + void RequestAborted(const uint64_t& aTransactionId, + const nsresult& aError) override; + + // AbortFollower + + void RunAbortAlgorithm() override; + + private: + virtual ~WebAuthnManager(); + + // Send a Cancel message to the parent, reject the promise with the given + // reason (an nsresult or JS::Handle<JS::Value>), and clear the transaction. + template <typename T> + void CancelTransaction(const T& aReason) { + CancelParent(); + RejectTransaction(aReason); + } + + // Reject the promise with the given reason (an nsresult or JS::Value), and + // clear the transaction. + template <typename T> + void RejectTransaction(const T& aReason) { + if (!NS_WARN_IF(mTransaction.isNothing())) { + mTransaction.ref().mPromise->MaybeReject(aReason); + } + + ClearTransaction(); + } + + // Send a Cancel message to the parent. + void CancelParent(); + + // Clears all information we have about the current transaction. + void ClearTransaction(); + + // The current transaction, if any. + Maybe<WebAuthnTransaction> mTransaction; +}; + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + WebAuthnTransaction& aTransaction, const char* aName, uint32_t aFlags = 0) { + ImplCycleCollectionTraverse(aCallback, aTransaction.mPromise, aName, aFlags); +} + +inline void ImplCycleCollectionUnlink(WebAuthnTransaction& aTransaction) { + ImplCycleCollectionUnlink(aTransaction.mPromise); +} + +} // namespace mozilla::dom + +#endif // mozilla_dom_WebAuthnManager_h diff --git a/dom/webauthn/WebAuthnManagerBase.cpp b/dom/webauthn/WebAuthnManagerBase.cpp new file mode 100644 index 0000000000..392be93076 --- /dev/null +++ b/dom/webauthn/WebAuthnManagerBase.cpp @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/WebAuthnManagerBase.h" +#include "mozilla/dom/WebAuthnTransactionChild.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" + +namespace mozilla::dom { + +WebAuthnManagerBase::WebAuthnManagerBase(nsPIDOMWindowInner* aParent) + : mParent(aParent) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aParent); +} + +WebAuthnManagerBase::~WebAuthnManagerBase() { MOZ_ASSERT(NS_IsMainThread()); } + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebAuthnManagerBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(WebAuthnManagerBase, mParent) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WebAuthnManagerBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WebAuthnManagerBase) + +/*********************************************************************** + * IPC Protocol Implementation + **********************************************************************/ + +bool WebAuthnManagerBase::MaybeCreateBackgroundActor() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mChild) { + return true; + } + + ::mozilla::ipc::PBackgroundChild* actorChild = + ::mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!actorChild)) { + return false; + } + + RefPtr<WebAuthnTransactionChild> mgr(new WebAuthnTransactionChild(this)); + PWebAuthnTransactionChild* constructedMgr = + actorChild->SendPWebAuthnTransactionConstructor(mgr); + + if (NS_WARN_IF(!constructedMgr)) { + return false; + } + + MOZ_ASSERT(constructedMgr == mgr); + mChild = std::move(mgr); + + return true; +} + +void WebAuthnManagerBase::ActorDestroyed() { + MOZ_ASSERT(NS_IsMainThread()); + mChild = nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/WebAuthnManagerBase.h b/dom/webauthn/WebAuthnManagerBase.h new file mode 100644 index 0000000000..0958fb59b3 --- /dev/null +++ b/dom/webauthn/WebAuthnManagerBase.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WebAuthnManagerBase_h +#define mozilla_dom_WebAuthnManagerBase_h + +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" + +/* + * A base class used by WebAuthn and U2F implementations, providing shared + * functionality and requiring an interface used by the IPC child actors. + */ + +class nsPIDOMWindowInner; + +namespace mozilla::dom { + +class WebAuthnTransactionChild; +class WebAuthnMakeCredentialResult; +class WebAuthnGetAssertionResult; + +class WebAuthnManagerBase : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(WebAuthnManagerBase) + + explicit WebAuthnManagerBase(nsPIDOMWindowInner* aParent); + + MOZ_CAN_RUN_SCRIPT + virtual void FinishMakeCredential( + const uint64_t& aTransactionId, + const WebAuthnMakeCredentialResult& aResult) = 0; + + MOZ_CAN_RUN_SCRIPT + virtual void FinishGetAssertion( + const uint64_t& aTransactionId, + const WebAuthnGetAssertionResult& aResult) = 0; + + MOZ_CAN_RUN_SCRIPT + virtual void RequestAborted(const uint64_t& aTransactionId, + const nsresult& aError) = 0; + + void ActorDestroyed(); + + protected: + MOZ_CAN_RUN_SCRIPT virtual ~WebAuthnManagerBase(); + + bool MaybeCreateBackgroundActor(); + + // The parent window. + nsCOMPtr<nsPIDOMWindowInner> mParent; + + // IPC Channel to the parent process. + RefPtr<WebAuthnTransactionChild> mChild; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WebAuthnManagerBase_h diff --git a/dom/webauthn/WebAuthnPromiseHolder.cpp b/dom/webauthn/WebAuthnPromiseHolder.cpp new file mode 100644 index 0000000000..60dff7f1c2 --- /dev/null +++ b/dom/webauthn/WebAuthnPromiseHolder.cpp @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/AppShutdown.h" +#include "WebAuthnPromiseHolder.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(WebAuthnRegisterPromiseHolder, nsIWebAuthnRegisterPromise); + +already_AddRefed<WebAuthnRegisterPromise> +WebAuthnRegisterPromiseHolder::Ensure() { + return mRegisterPromise.Ensure(__func__); +} + +NS_IMETHODIMP +WebAuthnRegisterPromiseHolder::Resolve(nsIWebAuthnRegisterResult* aResult) { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + // Resolve the promise on its owning thread if Disconnect() has not been + // called. + RefPtr<nsIWebAuthnRegisterResult> result(aResult); + mEventTarget->Dispatch(NS_NewRunnableFunction( + "WebAuthnRegisterPromiseHolder::Resolve", + [self = RefPtr{this}, result]() { + self->mRegisterPromise.ResolveIfExists(result, __func__); + })); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterPromiseHolder::Reject(nsresult aResult) { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + // Reject the promise on its owning thread if Disconnect() has not been + // called. + mEventTarget->Dispatch(NS_NewRunnableFunction( + "WebAuthnRegisterPromiseHolder::Reject", + [self = RefPtr{this}, aResult]() { + self->mRegisterPromise.RejectIfExists(aResult, __func__); + })); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(WebAuthnSignPromiseHolder, nsIWebAuthnSignPromise); + +already_AddRefed<WebAuthnSignPromise> WebAuthnSignPromiseHolder::Ensure() { + return mSignPromise.Ensure(__func__); +} + +NS_IMETHODIMP +WebAuthnSignPromiseHolder::Resolve(nsIWebAuthnSignResult* aResult) { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + // Resolve the promise on its owning thread if Disconnect() has not been + // called. + RefPtr<nsIWebAuthnSignResult> result(aResult); + mEventTarget->Dispatch(NS_NewRunnableFunction( + "WebAuthnSignPromiseHolder::Resolve", [self = RefPtr{this}, result]() { + self->mSignPromise.ResolveIfExists(result, __func__); + })); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignPromiseHolder::Reject(nsresult aResult) { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + // Reject the promise on its owning thread if Disconnect() has not been + // called. + mEventTarget->Dispatch(NS_NewRunnableFunction( + "WebAuthnSignPromiseHolder::Reject", [self = RefPtr{this}, aResult]() { + self->mSignPromise.RejectIfExists(aResult, __func__); + })); + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/WebAuthnPromiseHolder.h b/dom/webauthn/WebAuthnPromiseHolder.h new file mode 100644 index 0000000000..ca70fedb14 --- /dev/null +++ b/dom/webauthn/WebAuthnPromiseHolder.h @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WebAuthnPromiseHolder_h +#define mozilla_dom_WebAuthnPromiseHolder_h + +#include "mozilla/MozPromise.h" +#include "nsIWebAuthnResult.h" +#include "nsIWebAuthnPromise.h" +#include "nsIThread.h" + +namespace mozilla::dom { + +/* + * WebAuthnRegisterPromiseHolder and WebAuthnSignPromiseHolder wrap a + * MozPromiseHolder with an XPCOM interface that allows the contained promise + * to be resolved, rejected, or disconnected safely from any thread. + * + * Calls to Resolve(), Reject(), and Disconnect() are dispatched to the serial + * event target with wich the promise holder was initialized. At most one of + * these calls will be processed; the first call to reach the event target + * wins. Once the promise is initialized with Ensure() the program MUST call + * at least one of Resolve(), Reject(), or Disconnect(). + */ + +using WebAuthnRegisterPromise = + MozPromise<RefPtr<nsIWebAuthnRegisterResult>, nsresult, true>; + +using WebAuthnSignPromise = + MozPromise<RefPtr<nsIWebAuthnSignResult>, nsresult, true>; + +class WebAuthnRegisterPromiseHolder final : public nsIWebAuthnRegisterPromise { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBAUTHNREGISTERPROMISE + + explicit WebAuthnRegisterPromiseHolder(nsISerialEventTarget* aEventTarget) + : mEventTarget(aEventTarget) {} + + already_AddRefed<WebAuthnRegisterPromise> Ensure(); + + private: + ~WebAuthnRegisterPromiseHolder() = default; + + nsCOMPtr<nsISerialEventTarget> mEventTarget; + MozPromiseHolder<WebAuthnRegisterPromise> mRegisterPromise; +}; + +class WebAuthnSignPromiseHolder final : public nsIWebAuthnSignPromise { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBAUTHNSIGNPROMISE + + explicit WebAuthnSignPromiseHolder(nsISerialEventTarget* aEventTarget) + : mEventTarget(aEventTarget) {} + + already_AddRefed<WebAuthnSignPromise> Ensure(); + + private: + ~WebAuthnSignPromiseHolder() = default; + + nsCOMPtr<nsISerialEventTarget> mEventTarget; + MozPromiseHolder<WebAuthnSignPromise> mSignPromise; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WebAuthnPromiseHolder_h diff --git a/dom/webauthn/WebAuthnResult.cpp b/dom/webauthn/WebAuthnResult.cpp new file mode 100644 index 0000000000..268dd62f20 --- /dev/null +++ b/dom/webauthn/WebAuthnResult.cpp @@ -0,0 +1,192 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AuthrsBridge_ffi.h" +#include "WebAuthnResult.h" +#include "nsIWebAuthnAttObj.h" +#include "nsCOMPtr.h" +#include "nsString.h" + +#ifdef MOZ_WIDGET_ANDROID +namespace mozilla::jni { + +template <> +RefPtr<dom::WebAuthnRegisterResult> Java2Native( + mozilla::jni::Object::Param aData, JNIEnv* aEnv) { + MOZ_ASSERT( + aData.IsInstanceOf<java::WebAuthnTokenManager::MakeCredentialResponse>()); + java::WebAuthnTokenManager::MakeCredentialResponse::LocalRef response(aData); + RefPtr<dom::WebAuthnRegisterResult> result = + new dom::WebAuthnRegisterResult(response); + return result; +} + +template <> +RefPtr<dom::WebAuthnSignResult> Java2Native(mozilla::jni::Object::Param aData, + JNIEnv* aEnv) { + MOZ_ASSERT( + aData.IsInstanceOf<java::WebAuthnTokenManager::GetAssertionResponse>()); + java::WebAuthnTokenManager::GetAssertionResponse::LocalRef response(aData); + RefPtr<dom::WebAuthnSignResult> result = + new dom::WebAuthnSignResult(response); + return result; +} + +} // namespace mozilla::jni +#endif + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(WebAuthnRegisterResult, nsIWebAuthnRegisterResult) + +NS_IMETHODIMP +WebAuthnRegisterResult::GetClientDataJSON(nsACString& aClientDataJSON) { + if (mClientDataJSON.isSome()) { + aClientDataJSON = *mClientDataJSON; + return NS_OK; + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +WebAuthnRegisterResult::GetAttestationObject( + nsTArray<uint8_t>& aAttestationObject) { + aAttestationObject.Assign(mAttestationObject); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterResult::GetCredentialId(nsTArray<uint8_t>& aCredentialId) { + aCredentialId.Assign(mCredentialId); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterResult::GetTransports(nsTArray<nsString>& aTransports) { + aTransports.Assign(mTransports); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterResult::GetHmacCreateSecret(bool* aHmacCreateSecret) { + if (mHmacCreateSecret.isSome()) { + *aHmacCreateSecret = mHmacCreateSecret.ref(); + return NS_OK; + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +WebAuthnRegisterResult::GetCredPropsRk(bool* aCredPropsRk) { + if (mCredPropsRk.isSome()) { + *aCredPropsRk = mCredPropsRk.ref(); + return NS_OK; + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +WebAuthnRegisterResult::SetCredPropsRk(bool aCredPropsRk) { + mCredPropsRk = Some(aCredPropsRk); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterResult::GetAuthenticatorAttachment( + nsAString& aAuthenticatorAttachment) { + if (mAuthenticatorAttachment.isSome()) { + aAuthenticatorAttachment = mAuthenticatorAttachment.ref(); + return NS_OK; + } + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult WebAuthnRegisterResult::Anonymize() { + // The anonymize flag in the nsIWebAuthnAttObj constructor causes the + // attestation statement to be removed during deserialization. It also + // causes the AAGUID to be zeroed out. If we can't deserialize the + // existing attestation, then we can't ensure that it is anonymized, so we + // act as though the user denied consent and we return NotAllowed. + nsCOMPtr<nsIWebAuthnAttObj> anonymizedAttObj; + nsresult rv = authrs_webauthn_att_obj_constructor( + mAttestationObject, + /* anonymize */ true, getter_AddRefs(anonymizedAttObj)); + if (NS_FAILED(rv)) { + return rv; + } + mAttestationObject.Clear(); + rv = anonymizedAttObj->GetAttestationObject(mAttestationObject); + if (NS_FAILED(rv)) { + return rv; + } + return NS_OK; +} + +NS_IMPL_ISUPPORTS(WebAuthnSignResult, nsIWebAuthnSignResult) + +NS_IMETHODIMP +WebAuthnSignResult::GetClientDataJSON(nsACString& aClientDataJSON) { + if (mClientDataJSON.isSome()) { + aClientDataJSON = *mClientDataJSON; + return NS_OK; + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +WebAuthnSignResult::GetAuthenticatorData( + nsTArray<uint8_t>& aAuthenticatorData) { + aAuthenticatorData.Assign(mAuthenticatorData); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignResult::GetCredentialId(nsTArray<uint8_t>& aCredentialId) { + aCredentialId.Assign(mCredentialId); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignResult::GetSignature(nsTArray<uint8_t>& aSignature) { + aSignature.Assign(mSignature); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignResult::GetUserHandle(nsTArray<uint8_t>& aUserHandle) { + aUserHandle.Assign(mUserHandle); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignResult::GetUserName(nsACString& aUserName) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +WebAuthnSignResult::GetUsedAppId(bool* aUsedAppId) { + if (mUsedAppId.isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + *aUsedAppId = mUsedAppId.ref(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignResult::SetUsedAppId(bool aUsedAppId) { + mUsedAppId = Some(aUsedAppId); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnSignResult::GetAuthenticatorAttachment( + nsAString& aAuthenticatorAttachment) { + if (mAuthenticatorAttachment.isSome()) { + aAuthenticatorAttachment = mAuthenticatorAttachment.ref(); + return NS_OK; + } + return NS_ERROR_NOT_AVAILABLE; +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/WebAuthnResult.h b/dom/webauthn/WebAuthnResult.h new file mode 100644 index 0000000000..f7653fd4b0 --- /dev/null +++ b/dom/webauthn/WebAuthnResult.h @@ -0,0 +1,229 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WebAuthnResult_h_ +#define mozilla_dom_WebAuthnResult_h_ + +#include "nsIWebAuthnResult.h" +#include "nsString.h" +#include "nsTArray.h" + +#include "mozilla/Maybe.h" +#include "nsString.h" + +#ifdef MOZ_WIDGET_ANDROID +# include "mozilla/java/WebAuthnTokenManagerNatives.h" +#endif + +#ifdef XP_WIN +# include <windows.h> +# include "mozilla/dom/PWebAuthnTransactionParent.h" +# include "winwebauthn/webauthn.h" +#endif + +namespace mozilla::dom { + +class WebAuthnRegisterResult final : public nsIWebAuthnRegisterResult { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBAUTHNREGISTERRESULT + + WebAuthnRegisterResult(const nsTArray<uint8_t>& aAttestationObject, + const Maybe<nsCString>& aClientDataJSON, + const nsTArray<uint8_t>& aCredentialId, + const nsTArray<nsString>& aTransports, + const Maybe<nsString>& aAuthenticatorAttachment) + : mClientDataJSON(aClientDataJSON), + mCredPropsRk(Nothing()), + mAuthenticatorAttachment(aAuthenticatorAttachment) { + mAttestationObject.AppendElements(aAttestationObject); + mCredentialId.AppendElements(aCredentialId); + mTransports.AppendElements(aTransports); + } + +#ifdef MOZ_WIDGET_ANDROID + explicit WebAuthnRegisterResult( + const java::WebAuthnTokenManager::MakeCredentialResponse::LocalRef& + aResponse) { + mAttestationObject.AppendElements( + reinterpret_cast<uint8_t*>( + aResponse->AttestationObject()->GetElements().Elements()), + aResponse->AttestationObject()->Length()); + mClientDataJSON = Some(nsAutoCString( + reinterpret_cast<const char*>( + aResponse->ClientDataJson()->GetElements().Elements()), + aResponse->ClientDataJson()->Length())); + mCredentialId.AppendElements( + reinterpret_cast<uint8_t*>( + aResponse->KeyHandle()->GetElements().Elements()), + aResponse->KeyHandle()->Length()); + auto transports = aResponse->Transports(); + for (size_t i = 0; i < transports->Length(); i++) { + mTransports.AppendElement( + jni::String::LocalRef(transports->GetElement(i))->ToString()); + } + // authenticator attachment is not available on Android + mAuthenticatorAttachment = Nothing(); + } +#endif + +#ifdef XP_WIN + WebAuthnRegisterResult(nsCString& aClientDataJSON, + PCWEBAUTHN_CREDENTIAL_ATTESTATION aResponse) + : mClientDataJSON(Some(aClientDataJSON)) { + mCredentialId.AppendElements(aResponse->pbCredentialId, + aResponse->cbCredentialId); + + mAttestationObject.AppendElements(aResponse->pbAttestationObject, + aResponse->cbAttestationObject); + + nsTArray<WebAuthnExtensionResult> extensions; + if (aResponse->dwVersion >= WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_2) { + PCWEBAUTHN_EXTENSIONS pExtensionList = &aResponse->Extensions; + if (pExtensionList->cExtensions != 0 && + pExtensionList->pExtensions != NULL) { + for (DWORD dwIndex = 0; dwIndex < pExtensionList->cExtensions; + dwIndex++) { + PWEBAUTHN_EXTENSION pExtension = + &pExtensionList->pExtensions[dwIndex]; + if (pExtension->pwszExtensionIdentifier && + (0 == _wcsicmp(pExtension->pwszExtensionIdentifier, + WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET)) && + pExtension->cbExtension == sizeof(BOOL)) { + BOOL* pCredentialCreatedWithHmacSecret = + (BOOL*)pExtension->pvExtension; + if (*pCredentialCreatedWithHmacSecret) { + mHmacCreateSecret = Some(true); + } + } + } + } + } + + if (aResponse->dwVersion >= WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_3) { + if (aResponse->dwUsedTransport & WEBAUTHN_CTAP_TRANSPORT_USB) { + mTransports.AppendElement(u"usb"_ns); + } + if (aResponse->dwUsedTransport & WEBAUTHN_CTAP_TRANSPORT_NFC) { + mTransports.AppendElement(u"nfc"_ns); + } + if (aResponse->dwUsedTransport & WEBAUTHN_CTAP_TRANSPORT_BLE) { + mTransports.AppendElement(u"ble"_ns); + } + if (aResponse->dwUsedTransport & WEBAUTHN_CTAP_TRANSPORT_INTERNAL) { + mTransports.AppendElement(u"internal"_ns); + } + } + // WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5 corresponds to + // WEBAUTHN_API_VERSION_6 which is where WEBAUTHN_CTAP_TRANSPORT_HYBRID was + // defined. + if (aResponse->dwVersion >= WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5) { + if (aResponse->dwUsedTransport & WEBAUTHN_CTAP_TRANSPORT_HYBRID) { + mTransports.AppendElement(u"hybrid"_ns); + } + } + + if (aResponse->dwVersion >= WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_3) { + if (aResponse->dwUsedTransport & WEBAUTHN_CTAP_TRANSPORT_INTERNAL) { + mAuthenticatorAttachment = Some(u"platform"_ns); + } else { + mAuthenticatorAttachment = Some(u"cross-platform"_ns); + } + } + } +#endif + + nsresult Anonymize(); + + private: + ~WebAuthnRegisterResult() = default; + + nsTArray<uint8_t> mAttestationObject; + nsTArray<uint8_t> mCredentialId; + nsTArray<nsString> mTransports; + Maybe<nsCString> mClientDataJSON; + Maybe<bool> mCredPropsRk; + Maybe<bool> mHmacCreateSecret; + Maybe<nsString> mAuthenticatorAttachment; +}; + +class WebAuthnSignResult final : public nsIWebAuthnSignResult { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBAUTHNSIGNRESULT + + WebAuthnSignResult(const nsTArray<uint8_t>& aAuthenticatorData, + const Maybe<nsCString>& aClientDataJSON, + const nsTArray<uint8_t>& aCredentialId, + const nsTArray<uint8_t>& aSignature, + const nsTArray<uint8_t>& aUserHandle, + const Maybe<nsString>& aAuthenticatorAttachment) + : mClientDataJSON(aClientDataJSON), + mAuthenticatorAttachment(aAuthenticatorAttachment) { + mAuthenticatorData.AppendElements(aAuthenticatorData); + mCredentialId.AppendElements(aCredentialId); + mSignature.AppendElements(aSignature); + mUserHandle.AppendElements(aUserHandle); + } + +#ifdef MOZ_WIDGET_ANDROID + explicit WebAuthnSignResult( + const java::WebAuthnTokenManager::GetAssertionResponse::LocalRef& + aResponse) { + mAuthenticatorData.AppendElements( + reinterpret_cast<uint8_t*>( + aResponse->AuthData()->GetElements().Elements()), + aResponse->AuthData()->Length()); + mClientDataJSON = Some(nsAutoCString( + reinterpret_cast<const char*>( + aResponse->ClientDataJson()->GetElements().Elements()), + aResponse->ClientDataJson()->Length())); + mCredentialId.AppendElements( + reinterpret_cast<uint8_t*>( + aResponse->KeyHandle()->GetElements().Elements()), + aResponse->KeyHandle()->Length()); + mSignature.AppendElements( + reinterpret_cast<uint8_t*>( + aResponse->Signature()->GetElements().Elements()), + aResponse->Signature()->Length()); + mUserHandle.AppendElements( + reinterpret_cast<uint8_t*>( + aResponse->UserHandle()->GetElements().Elements()), + aResponse->UserHandle()->Length()); + // authenticator attachment is not available on Android + mAuthenticatorAttachment = Nothing(); + } +#endif + +#ifdef XP_WIN + WebAuthnSignResult(nsCString& aClientDataJSON, PCWEBAUTHN_ASSERTION aResponse) + : mClientDataJSON(Some(aClientDataJSON)) { + mSignature.AppendElements(aResponse->pbSignature, aResponse->cbSignature); + + mCredentialId.AppendElements(aResponse->Credential.pbId, + aResponse->Credential.cbId); + + mUserHandle.AppendElements(aResponse->pbUserId, aResponse->cbUserId); + + mAuthenticatorData.AppendElements(aResponse->pbAuthenticatorData, + aResponse->cbAuthenticatorData); + + mAuthenticatorAttachment = Nothing(); // not available + } +#endif + + private: + ~WebAuthnSignResult() = default; + + nsTArray<uint8_t> mAuthenticatorData; + Maybe<nsCString> mClientDataJSON; + nsTArray<uint8_t> mCredentialId; + nsTArray<uint8_t> mSignature; + nsTArray<uint8_t> mUserHandle; + Maybe<nsString> mAuthenticatorAttachment; + Maybe<bool> mUsedAppId; +}; + +} // namespace mozilla::dom +#endif // mozilla_dom_WebAuthnResult_h diff --git a/dom/webauthn/WebAuthnService.cpp b/dom/webauthn/WebAuthnService.cpp new file mode 100644 index 0000000000..3e1557edbc --- /dev/null +++ b/dom/webauthn/WebAuthnService.cpp @@ -0,0 +1,220 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_security.h" +#include "nsIObserverService.h" +#include "nsThreadUtils.h" +#include "WebAuthnService.h" +#include "WebAuthnTransportIdentifiers.h" + +namespace mozilla::dom { + +already_AddRefed<nsIWebAuthnService> NewWebAuthnService() { + nsCOMPtr<nsIWebAuthnService> webauthnService(new WebAuthnService()); + return webauthnService.forget(); +} + +NS_IMPL_ISUPPORTS(WebAuthnService, nsIWebAuthnService) + +NS_IMETHODIMP +WebAuthnService::MakeCredential(uint64_t aTransactionId, + uint64_t browsingContextId, + nsIWebAuthnRegisterArgs* aArgs, + nsIWebAuthnRegisterPromise* aPromise) { + auto guard = mTransactionState.Lock(); + if (guard->isSome()) { + guard->ref().service->Reset(); + *guard = Nothing(); + } + *guard = Some(TransactionState{DefaultService()}); + return guard->ref().service->MakeCredential(aTransactionId, browsingContextId, + aArgs, aPromise); +} + +NS_IMETHODIMP +WebAuthnService::GetAssertion(uint64_t aTransactionId, + uint64_t browsingContextId, + nsIWebAuthnSignArgs* aArgs, + nsIWebAuthnSignPromise* aPromise) { + auto guard = mTransactionState.Lock(); + if (guard->isSome()) { + guard->ref().service->Reset(); + *guard = Nothing(); + } + *guard = Some(TransactionState{DefaultService()}); + nsresult rv; + +#if defined(XP_MACOSX) + // The macOS security key API doesn't handle the AppID extension. So we'll + // use authenticator-rs if it's likely that the request requires AppID. We + // consider it likely if 1) the AppID extension is present, 2) the allow list + // is non-empty, and 3) none of the allowed credentials use the + // "internal" or "hybrid" transport. + nsString appId; + rv = aArgs->GetAppId(appId); + if (rv == NS_OK) { // AppID is set + uint8_t transportSet = 0; + nsTArray<uint8_t> allowListTransports; + Unused << aArgs->GetAllowListTransports(allowListTransports); + for (const uint8_t& transport : allowListTransports) { + transportSet |= transport; + } + uint8_t passkeyTransportMask = + MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL | + MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_HYBRID; + if (allowListTransports.Length() > 0 && + (transportSet & passkeyTransportMask) == 0) { + guard->ref().service = AuthrsService(); + } + } +#endif + + rv = guard->ref().service->GetAssertion(aTransactionId, browsingContextId, + aArgs, aPromise); + if (NS_FAILED(rv)) { + return rv; + } + + // If this is a conditionally mediated request, notify observers that there + // is a pending transaction. This is mainly useful in tests. + bool conditionallyMediated; + Unused << aArgs->GetConditionallyMediated(&conditionallyMediated); + if (conditionallyMediated) { + nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction(__func__, []() { + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + os->NotifyObservers(nullptr, "webauthn:conditional-get-pending", + nullptr); + } + })); + NS_DispatchToMainThread(runnable.forget()); + } + + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnService::GetIsUVPAA(bool* aAvailable) { + return DefaultService()->GetIsUVPAA(aAvailable); +} + +NS_IMETHODIMP +WebAuthnService::HasPendingConditionalGet(uint64_t aBrowsingContextId, + const nsAString& aOrigin, + uint64_t* aRv) { + return SelectedService()->HasPendingConditionalGet(aBrowsingContextId, + aOrigin, aRv); +} + +NS_IMETHODIMP +WebAuthnService::GetAutoFillEntries( + uint64_t aTransactionId, nsTArray<RefPtr<nsIWebAuthnAutoFillEntry>>& aRv) { + return SelectedService()->GetAutoFillEntries(aTransactionId, aRv); +} + +NS_IMETHODIMP +WebAuthnService::SelectAutoFillEntry(uint64_t aTransactionId, + const nsTArray<uint8_t>& aCredentialId) { + return SelectedService()->SelectAutoFillEntry(aTransactionId, aCredentialId); +} + +NS_IMETHODIMP +WebAuthnService::ResumeConditionalGet(uint64_t aTransactionId) { + return SelectedService()->ResumeConditionalGet(aTransactionId); +} + +NS_IMETHODIMP +WebAuthnService::Reset() { + auto guard = mTransactionState.Lock(); + if (guard->isSome()) { + guard->ref().service->Reset(); + } + *guard = Nothing(); + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnService::Cancel(uint64_t aTransactionId) { + return SelectedService()->Cancel(aTransactionId); +} + +NS_IMETHODIMP +WebAuthnService::PinCallback(uint64_t aTransactionId, const nsACString& aPin) { + return SelectedService()->PinCallback(aTransactionId, aPin); +} + +NS_IMETHODIMP +WebAuthnService::ResumeMakeCredential(uint64_t aTransactionId, + bool aForceNoneAttestation) { + return SelectedService()->ResumeMakeCredential(aTransactionId, + aForceNoneAttestation); +} + +NS_IMETHODIMP +WebAuthnService::SelectionCallback(uint64_t aTransactionId, uint64_t aIndex) { + return SelectedService()->SelectionCallback(aTransactionId, aIndex); +} + +NS_IMETHODIMP +WebAuthnService::AddVirtualAuthenticator( + const nsACString& protocol, const nsACString& transport, + bool hasResidentKey, bool hasUserVerification, bool isUserConsenting, + bool isUserVerified, uint64_t* retval) { + return SelectedService()->AddVirtualAuthenticator( + protocol, transport, hasResidentKey, hasUserVerification, + isUserConsenting, isUserVerified, retval); +} + +NS_IMETHODIMP +WebAuthnService::RemoveVirtualAuthenticator(uint64_t authenticatorId) { + return SelectedService()->RemoveVirtualAuthenticator(authenticatorId); +} + +NS_IMETHODIMP +WebAuthnService::AddCredential(uint64_t authenticatorId, + const nsACString& credentialId, + bool isResidentCredential, + const nsACString& rpId, + const nsACString& privateKey, + const nsACString& userHandle, + uint32_t signCount) { + return SelectedService()->AddCredential(authenticatorId, credentialId, + isResidentCredential, rpId, + privateKey, userHandle, signCount); +} + +NS_IMETHODIMP +WebAuthnService::GetCredentials( + uint64_t authenticatorId, + nsTArray<RefPtr<nsICredentialParameters>>& retval) { + return SelectedService()->GetCredentials(authenticatorId, retval); +} + +NS_IMETHODIMP +WebAuthnService::RemoveCredential(uint64_t authenticatorId, + const nsACString& credentialId) { + return SelectedService()->RemoveCredential(authenticatorId, credentialId); +} + +NS_IMETHODIMP +WebAuthnService::RemoveAllCredentials(uint64_t authenticatorId) { + return SelectedService()->RemoveAllCredentials(authenticatorId); +} + +NS_IMETHODIMP +WebAuthnService::SetUserVerified(uint64_t authenticatorId, + bool isUserVerified) { + return SelectedService()->SetUserVerified(authenticatorId, isUserVerified); +} + +NS_IMETHODIMP +WebAuthnService::Listen() { return SelectedService()->Listen(); } + +NS_IMETHODIMP +WebAuthnService::RunCommand(const nsACString& cmd) { + return SelectedService()->RunCommand(cmd); +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/WebAuthnService.h b/dom/webauthn/WebAuthnService.h new file mode 100644 index 0000000000..254b75d251 --- /dev/null +++ b/dom/webauthn/WebAuthnService.h @@ -0,0 +1,87 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WebAuthnService_h_ +#define mozilla_dom_WebAuthnService_h_ + +#include "nsIWebAuthnService.h" +#include "AuthrsBridge_ffi.h" + +#ifdef MOZ_WIDGET_ANDROID +# include "AndroidWebAuthnService.h" +#endif + +#ifdef XP_MACOSX +# include "MacOSWebAuthnService.h" +#endif + +#ifdef XP_WIN +# include "WinWebAuthnService.h" +#endif + +namespace mozilla::dom { + +already_AddRefed<nsIWebAuthnService> NewWebAuthnService(); + +class WebAuthnService final : public nsIWebAuthnService { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBAUTHNSERVICE + + WebAuthnService() + : mTransactionState(Nothing(), "WebAuthnService::mTransactionState") { + Unused << authrs_service_constructor(getter_AddRefs(mAuthrsService)); +#if defined(XP_WIN) + if (WinWebAuthnService::AreWebAuthNApisAvailable()) { + mPlatformService = new WinWebAuthnService(); + } else { + mPlatformService = mAuthrsService; + } +#elif defined(MOZ_WIDGET_ANDROID) + mPlatformService = new AndroidWebAuthnService(); +#elif defined(XP_MACOSX) + if (__builtin_available(macos 13.3, *)) { + mPlatformService = NewMacOSWebAuthnServiceIfAvailable(); + } + if (!mPlatformService) { + mPlatformService = mAuthrsService; + } +#else + mPlatformService = mAuthrsService; +#endif + } + + private: + ~WebAuthnService() = default; + + nsIWebAuthnService* DefaultService() { + if (StaticPrefs::security_webauth_webauthn_enable_softtoken()) { + return mAuthrsService; + } + return mPlatformService; + } + + nsIWebAuthnService* AuthrsService() { return mAuthrsService; } + + nsIWebAuthnService* SelectedService() { + auto guard = mTransactionState.Lock(); + if (guard->isSome()) { + return guard->ref().service; + } + return DefaultService(); + } + + struct TransactionState { + nsCOMPtr<nsIWebAuthnService> service; + }; + using TransactionStateMutex = DataMutex<Maybe<TransactionState>>; + TransactionStateMutex mTransactionState; + + nsCOMPtr<nsIWebAuthnService> mAuthrsService; + nsCOMPtr<nsIWebAuthnService> mPlatformService; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WebAuthnService_h_ diff --git a/dom/webauthn/WebAuthnTransactionChild.cpp b/dom/webauthn/WebAuthnTransactionChild.cpp new file mode 100644 index 0000000000..15022ff981 --- /dev/null +++ b/dom/webauthn/WebAuthnTransactionChild.cpp @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/WebAuthnTransactionChild.h" + +namespace mozilla::dom { + +WebAuthnTransactionChild::WebAuthnTransactionChild( + WebAuthnManagerBase* aManager) + : mManager(aManager) { + MOZ_ASSERT(aManager); + + // Retain a reference so the task object isn't deleted without IPDL's + // knowledge. The reference will be released by + // mozilla::ipc::BackgroundChildImpl::DeallocPWebAuthnTransactionChild. + NS_ADDREF_THIS(); +} + +mozilla::ipc::IPCResult WebAuthnTransactionChild::RecvConfirmRegister( + const uint64_t& aTransactionId, + const WebAuthnMakeCredentialResult& aResult) { + if (NS_WARN_IF(!mManager)) { + return IPC_FAIL_NO_REASON(this); + } + + // We don't own the reference to mManager. We need to prevent its refcount + // going to 0 while we call anything that can reach the call to + // StopListeningForVisibilityEvents in WebAuthnManager::ClearTransaction + // (often via WebAuthnManager::RejectTransaction). + RefPtr<WebAuthnManagerBase> kungFuDeathGrip(mManager); + kungFuDeathGrip->FinishMakeCredential(aTransactionId, aResult); + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebAuthnTransactionChild::RecvConfirmSign( + const uint64_t& aTransactionId, const WebAuthnGetAssertionResult& aResult) { + if (NS_WARN_IF(!mManager)) { + return IPC_FAIL_NO_REASON(this); + } + + // We don't own the reference to mManager. We need to prevent its refcount + // going to 0 while we call anything that can reach the call to + // StopListeningForVisibilityEvents in WebAuthnManager::ClearTransaction + // (often via WebAuthnManager::RejectTransaction). + RefPtr<WebAuthnManagerBase> kungFuDeathGrip(mManager); + kungFuDeathGrip->FinishGetAssertion(aTransactionId, aResult); + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebAuthnTransactionChild::RecvAbort( + const uint64_t& aTransactionId, const nsresult& aError) { + if (NS_WARN_IF(!mManager)) { + return IPC_FAIL_NO_REASON(this); + } + + // We don't own the reference to mManager. We need to prevent its refcount + // going to 0 while we call anything that can reach the call to + // StopListeningForVisibilityEvents in WebAuthnManager::ClearTransaction + // (often via WebAuthnManager::RejectTransaction). + RefPtr<WebAuthnManagerBase> kungFuDeathGrip(mManager); + kungFuDeathGrip->RequestAborted(aTransactionId, aError); + return IPC_OK(); +} + +void WebAuthnTransactionChild::ActorDestroy(ActorDestroyReason why) { + // Called by either a __delete__ message from the parent, or when the + // channel disconnects. Clear out the child actor reference to be sure. + if (mManager) { + mManager->ActorDestroyed(); + mManager = nullptr; + } +} + +void WebAuthnTransactionChild::Disconnect() { + mManager = nullptr; + + // The WebAuthnManager released us, but we're going to be held alive by the + // IPC layer. The parent will explicitly destroy us via Send__delete__(), + // after receiving the DestroyMe message. + + SendDestroyMe(); +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/WebAuthnTransactionChild.h b/dom/webauthn/WebAuthnTransactionChild.h new file mode 100644 index 0000000000..fd2d4db90b --- /dev/null +++ b/dom/webauthn/WebAuthnTransactionChild.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WebAuthnTransactionChild_h +#define mozilla_dom_WebAuthnTransactionChild_h + +#include "mozilla/dom/PWebAuthnTransactionChild.h" +#include "mozilla/dom/WebAuthnManagerBase.h" + +/* + * Child process IPC implementation for WebAuthn API. Receives results of + * WebAuthn transactions from the parent process, and sends them to the + * WebAuthnManager either cancel the transaction, or be formatted and relayed to + * content. + */ + +namespace mozilla::dom { + +class WebAuthnTransactionChild final : public PWebAuthnTransactionChild { + public: + NS_INLINE_DECL_REFCOUNTING(WebAuthnTransactionChild); + explicit WebAuthnTransactionChild(WebAuthnManagerBase* aManager); + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until we can do MOZ_CAN_RUN_SCRIPT in + // IPDL-generated things. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + mozilla::ipc::IPCResult RecvConfirmRegister( + const uint64_t& aTransactionId, + const WebAuthnMakeCredentialResult& aResult); + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until we can do MOZ_CAN_RUN_SCRIPT in + // IPDL-generated things. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + mozilla::ipc::IPCResult RecvConfirmSign( + const uint64_t& aTransactionId, + const WebAuthnGetAssertionResult& aResult); + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until we can do MOZ_CAN_RUN_SCRIPT in + // IPDL-generated things. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + mozilla::ipc::IPCResult RecvAbort(const uint64_t& aTransactionId, + const nsresult& aError); + + void ActorDestroy(ActorDestroyReason why) override; + + void Disconnect(); + + private: + ~WebAuthnTransactionChild() = default; + + // Nulled by ~WebAuthnManager() when disconnecting. + WebAuthnManagerBase* mManager; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WebAuthnTransactionChild_h diff --git a/dom/webauthn/WebAuthnTransactionParent.cpp b/dom/webauthn/WebAuthnTransactionParent.cpp new file mode 100644 index 0000000000..697e8dc970 --- /dev/null +++ b/dom/webauthn/WebAuthnTransactionParent.cpp @@ -0,0 +1,409 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/WebAuthnTransactionParent.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/StaticPrefs_security.h" + +#include "nsThreadUtils.h" +#include "WebAuthnArgs.h" + +namespace mozilla::dom { + +void WebAuthnTransactionParent::CompleteTransaction() { + if (mTransactionId.isSome()) { + if (mRegisterPromiseRequest.Exists()) { + mRegisterPromiseRequest.Complete(); + } + if (mSignPromiseRequest.Exists()) { + mSignPromiseRequest.Complete(); + } + if (mWebAuthnService) { + // We have to do this to work around Bug 1864526. + mWebAuthnService->Cancel(mTransactionId.ref()); + } + mTransactionId.reset(); + } +} + +void WebAuthnTransactionParent::DisconnectTransaction() { + mTransactionId.reset(); + mRegisterPromiseRequest.DisconnectIfExists(); + mSignPromiseRequest.DisconnectIfExists(); + if (mWebAuthnService) { + mWebAuthnService->Reset(); + } +} + +mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestRegister( + const uint64_t& aTransactionId, + const WebAuthnMakeCredentialInfo& aTransactionInfo) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (!mWebAuthnService) { + mWebAuthnService = do_GetService("@mozilla.org/webauthn/service;1"); + if (!mWebAuthnService) { + return IPC_FAIL_NO_REASON(this); + } + } + + // If there's an ongoing transaction, abort it. + if (mTransactionId.isSome()) { + DisconnectTransaction(); + Unused << SendAbort(mTransactionId.ref(), NS_ERROR_DOM_ABORT_ERR); + } + mTransactionId = Some(aTransactionId); + + RefPtr<WebAuthnRegisterPromiseHolder> promiseHolder = + new WebAuthnRegisterPromiseHolder(GetCurrentSerialEventTarget()); + + PWebAuthnTransactionParent* parent = this; + RefPtr<WebAuthnRegisterPromise> promise = promiseHolder->Ensure(); + promise + ->Then( + GetCurrentSerialEventTarget(), __func__, + [this, parent, aTransactionId, + inputClientData = aTransactionInfo.ClientDataJSON()]( + const WebAuthnRegisterPromise::ResolveValueType& aValue) { + CompleteTransaction(); + + nsCString clientData; + nsresult rv = aValue->GetClientDataJSON(clientData); + if (rv == NS_ERROR_NOT_AVAILABLE) { + clientData = inputClientData; + } else if (NS_FAILED(rv)) { + Unused << parent->SendAbort(aTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + + nsTArray<uint8_t> attObj; + rv = aValue->GetAttestationObject(attObj); + if (NS_WARN_IF(NS_FAILED(rv))) { + Unused << parent->SendAbort(aTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + + nsTArray<uint8_t> credentialId; + rv = aValue->GetCredentialId(credentialId); + if (NS_WARN_IF(NS_FAILED(rv))) { + Unused << parent->SendAbort(aTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + + nsTArray<nsString> transports; + rv = aValue->GetTransports(transports); + if (NS_WARN_IF(NS_FAILED(rv))) { + Unused << parent->SendAbort(aTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + + Maybe<nsString> authenticatorAttachment; + nsString maybeAuthenticatorAttachment; + rv = aValue->GetAuthenticatorAttachment( + maybeAuthenticatorAttachment); + if (rv != NS_ERROR_NOT_AVAILABLE) { + if (NS_WARN_IF(NS_FAILED(rv))) { + Unused << parent->SendAbort(aTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + authenticatorAttachment = Some(maybeAuthenticatorAttachment); + } + + nsTArray<WebAuthnExtensionResult> extensions; + bool credPropsRk; + rv = aValue->GetCredPropsRk(&credPropsRk); + if (rv != NS_ERROR_NOT_AVAILABLE) { + if (NS_WARN_IF(NS_FAILED(rv))) { + Unused << parent->SendAbort(aTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + extensions.AppendElement( + WebAuthnExtensionResultCredProps(credPropsRk)); + } + + bool hmacCreateSecret; + rv = aValue->GetHmacCreateSecret(&hmacCreateSecret); + if (rv != NS_ERROR_NOT_AVAILABLE) { + if (NS_WARN_IF(NS_FAILED(rv))) { + Unused << parent->SendAbort(aTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + extensions.AppendElement( + WebAuthnExtensionResultHmacSecret(hmacCreateSecret)); + } + + WebAuthnMakeCredentialResult result( + clientData, attObj, credentialId, transports, extensions, + authenticatorAttachment); + + Unused << parent->SendConfirmRegister(aTransactionId, result); + }, + [this, parent, aTransactionId]( + const WebAuthnRegisterPromise::RejectValueType aValue) { + CompleteTransaction(); + Unused << parent->SendAbort(aTransactionId, aValue); + }) + ->Track(mRegisterPromiseRequest); + + uint64_t browsingContextId = aTransactionInfo.BrowsingContextId(); + RefPtr<WebAuthnRegisterArgs> args(new WebAuthnRegisterArgs(aTransactionInfo)); + + nsresult rv = mWebAuthnService->MakeCredential( + aTransactionId, browsingContextId, args, promiseHolder); + if (NS_FAILED(rv)) { + promiseHolder->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestSign( + const uint64_t& aTransactionId, + const WebAuthnGetAssertionInfo& aTransactionInfo) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (!mWebAuthnService) { + mWebAuthnService = do_GetService("@mozilla.org/webauthn/service;1"); + if (!mWebAuthnService) { + return IPC_FAIL_NO_REASON(this); + } + } + + if (mTransactionId.isSome()) { + DisconnectTransaction(); + Unused << SendAbort(mTransactionId.ref(), NS_ERROR_DOM_ABORT_ERR); + } + mTransactionId = Some(aTransactionId); + + RefPtr<WebAuthnSignPromiseHolder> promiseHolder = + new WebAuthnSignPromiseHolder(GetCurrentSerialEventTarget()); + + PWebAuthnTransactionParent* parent = this; + RefPtr<WebAuthnSignPromise> promise = promiseHolder->Ensure(); + promise + ->Then( + GetCurrentSerialEventTarget(), __func__, + [this, parent, aTransactionId, + inputClientData = aTransactionInfo.ClientDataJSON()]( + const WebAuthnSignPromise::ResolveValueType& aValue) { + CompleteTransaction(); + + nsCString clientData; + nsresult rv = aValue->GetClientDataJSON(clientData); + if (rv == NS_ERROR_NOT_AVAILABLE) { + clientData = inputClientData; + } else if (NS_FAILED(rv)) { + Unused << parent->SendAbort(aTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + + nsTArray<uint8_t> credentialId; + rv = aValue->GetCredentialId(credentialId); + if (NS_WARN_IF(NS_FAILED(rv))) { + Unused << parent->SendAbort(aTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + + nsTArray<uint8_t> signature; + rv = aValue->GetSignature(signature); + if (NS_WARN_IF(NS_FAILED(rv))) { + Unused << parent->SendAbort(aTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + + nsTArray<uint8_t> authenticatorData; + rv = aValue->GetAuthenticatorData(authenticatorData); + if (NS_WARN_IF(NS_FAILED(rv))) { + Unused << parent->SendAbort(aTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + + nsTArray<uint8_t> userHandle; + Unused << aValue->GetUserHandle(userHandle); // optional + + Maybe<nsString> authenticatorAttachment; + nsString maybeAuthenticatorAttachment; + rv = aValue->GetAuthenticatorAttachment( + maybeAuthenticatorAttachment); + if (rv != NS_ERROR_NOT_AVAILABLE) { + if (NS_WARN_IF(NS_FAILED(rv))) { + Unused << parent->SendAbort(aTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + authenticatorAttachment = Some(maybeAuthenticatorAttachment); + } + + nsTArray<WebAuthnExtensionResult> extensions; + bool usedAppId; + rv = aValue->GetUsedAppId(&usedAppId); + if (rv != NS_ERROR_NOT_AVAILABLE) { + if (NS_FAILED(rv)) { + Unused << parent->SendAbort(aTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId)); + } + + WebAuthnGetAssertionResult result( + clientData, credentialId, signature, authenticatorData, + extensions, userHandle, authenticatorAttachment); + + Unused << parent->SendConfirmSign(aTransactionId, result); + }, + [this, parent, + aTransactionId](const WebAuthnSignPromise::RejectValueType aValue) { + CompleteTransaction(); + Unused << parent->SendAbort(aTransactionId, aValue); + }) + ->Track(mSignPromiseRequest); + + RefPtr<WebAuthnSignArgs> args(new WebAuthnSignArgs(aTransactionInfo)); + + nsresult rv = mWebAuthnService->GetAssertion( + aTransactionId, aTransactionInfo.BrowsingContextId(), args, + promiseHolder); + if (NS_FAILED(rv)) { + promiseHolder->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestCancel( + const Tainted<uint64_t>& aTransactionId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (mTransactionId.isNothing() || + !MOZ_IS_VALID(aTransactionId, mTransactionId.ref() == aTransactionId)) { + return IPC_OK(); + } + + DisconnectTransaction(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestIsUVPAA( + RequestIsUVPAAResolver&& aResolver) { +#ifdef MOZ_WIDGET_ANDROID + // Try the nsIWebAuthnService. If we're configured for tests we + // will get a result. Otherwise we expect NS_ERROR_NOT_IMPLEMENTED. + nsCOMPtr<nsIWebAuthnService> service( + do_GetService("@mozilla.org/webauthn/service;1")); + bool available; + nsresult rv = service->GetIsUVPAA(&available); + if (NS_SUCCEEDED(rv)) { + aResolver(available); + return IPC_OK(); + } + + // Don't consult the platform API if resident key support is disabled. + if (!StaticPrefs:: + security_webauthn_webauthn_enable_android_fido2_residentkey()) { + aResolver(false); + return IPC_OK(); + } + + // The GeckoView implementation of + // isUserVerifiyingPlatformAuthenticatorAvailable does not block, but we must + // call it on the main thread. It returns a MozPromise which we can ->Then to + // call aResolver on the IPDL background thread. + // + // Bug 1550788: there is an unnecessary layer of dispatching here: ipdl -> + // main -> a background thread. Other platforms just do ipdl -> a background + // thread. + nsCOMPtr<nsISerialEventTarget> target = GetCurrentSerialEventTarget(); + nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction( + __func__, [target, resolver = std::move(aResolver)]() { + auto result = java::WebAuthnTokenManager:: + WebAuthnIsUserVerifyingPlatformAuthenticatorAvailable(); + auto geckoResult = java::GeckoResult::LocalRef(std::move(result)); + MozPromise<bool, bool, false>::FromGeckoResult(geckoResult) + ->Then( + target, __func__, + [resolver]( + const MozPromise<bool, bool, false>::ResolveOrRejectValue& + aValue) { + if (aValue.IsResolve()) { + resolver(aValue.ResolveValue()); + } else { + resolver(false); + } + }); + })); + NS_DispatchToMainThread(runnable.forget()); + return IPC_OK(); + +#else + + nsCOMPtr<nsISerialEventTarget> target = GetCurrentSerialEventTarget(); + nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction( + __func__, [target, resolver = std::move(aResolver)]() { + bool available; + nsCOMPtr<nsIWebAuthnService> service( + do_GetService("@mozilla.org/webauthn/service;1")); + nsresult rv = service->GetIsUVPAA(&available); + if (NS_FAILED(rv)) { + available = false; + } + BoolPromise::CreateAndResolve(available, __func__) + ->Then(target, __func__, + [resolver](const BoolPromise::ResolveOrRejectValue& value) { + if (value.IsResolve()) { + resolver(value.ResolveValue()); + } else { + resolver(false); + } + }); + })); + NS_DispatchBackgroundTask(runnable.forget(), NS_DISPATCH_EVENT_MAY_BLOCK); + return IPC_OK(); +#endif +} + +mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvDestroyMe() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + // The child was disconnected from the WebAuthnManager instance and will send + // no further messages. It is kept alive until we delete it explicitly. + + // The child should have cancelled any active transaction. This means + // we expect no more messages to the child. We'll crash otherwise. + + // The IPC roundtrip is complete. No more messages, hopefully. + IProtocol* mgr = Manager(); + if (!Send__delete__(this)) { + return IPC_FAIL_NO_REASON(mgr); + } + + return IPC_OK(); +} + +void WebAuthnTransactionParent::ActorDestroy(ActorDestroyReason aWhy) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + // Called either by Send__delete__() in RecvDestroyMe() above, or when + // the channel disconnects. Ensure the token manager forgets about us. + + if (mTransactionId.isSome()) { + DisconnectTransaction(); + } +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/WebAuthnTransactionParent.h b/dom/webauthn/WebAuthnTransactionParent.h new file mode 100644 index 0000000000..c393971dc4 --- /dev/null +++ b/dom/webauthn/WebAuthnTransactionParent.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WebAuthnTransactionParent_h +#define mozilla_dom_WebAuthnTransactionParent_h + +#include "mozilla/dom/PWebAuthnTransactionParent.h" +#include "mozilla/dom/WebAuthnPromiseHolder.h" +#include "nsIWebAuthnService.h" + +/* + * Parent process IPC implementation for WebAuthn. + */ + +namespace mozilla::dom { + +class WebAuthnRegisterPromiseHolder; +class WebAuthnSignPromiseHolder; + +class WebAuthnTransactionParent final : public PWebAuthnTransactionParent { + public: + NS_INLINE_DECL_REFCOUNTING(WebAuthnTransactionParent); + WebAuthnTransactionParent() = default; + + mozilla::ipc::IPCResult RecvRequestRegister( + const uint64_t& aTransactionId, + const WebAuthnMakeCredentialInfo& aTransactionInfo); + + mozilla::ipc::IPCResult RecvRequestSign( + const uint64_t& aTransactionId, + const WebAuthnGetAssertionInfo& aTransactionInfo); + + mozilla::ipc::IPCResult RecvRequestCancel( + const Tainted<uint64_t>& aTransactionId); + + mozilla::ipc::IPCResult RecvRequestIsUVPAA( + RequestIsUVPAAResolver&& aResolver); + + mozilla::ipc::IPCResult RecvDestroyMe(); + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + ~WebAuthnTransactionParent() = default; + + void CompleteTransaction(); + void DisconnectTransaction(); + + nsCOMPtr<nsIWebAuthnService> mWebAuthnService; + Maybe<uint64_t> mTransactionId; + MozPromiseRequestHolder<WebAuthnRegisterPromise> mRegisterPromiseRequest; + MozPromiseRequestHolder<WebAuthnSignPromise> mSignPromiseRequest; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WebAuthnTransactionParent_h diff --git a/dom/webauthn/WebAuthnTransportIdentifiers.h b/dom/webauthn/WebAuthnTransportIdentifiers.h new file mode 100644 index 0000000000..e6a348a377 --- /dev/null +++ b/dom/webauthn/WebAuthnTransportIdentifiers.h @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WebAuthnTransportIdentifiers_h +#define mozilla_dom_WebAuthnTransportIdentifiers_h + +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB 1 +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC 2 +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE 4 +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL 8 +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_HYBRID 16 + +#endif // mozilla_dom_WebAuthnTransportIdentifiers_h diff --git a/dom/webauthn/WebAuthnUtil.cpp b/dom/webauthn/WebAuthnUtil.cpp new file mode 100644 index 0000000000..941ff0a85b --- /dev/null +++ b/dom/webauthn/WebAuthnUtil.cpp @@ -0,0 +1,156 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/WebAuthnUtil.h" +#include "nsComponentManagerUtils.h" +#include "nsICryptoHash.h" +#include "nsIEffectiveTLDService.h" +#include "nsNetUtil.h" +#include "mozpkix/pkixutil.h" +#include "nsHTMLDocument.h" +#include "hasht.h" + +namespace mozilla::dom { + +// Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023. +constexpr auto kGoogleAccountsAppId1 = + u"https://www.gstatic.com/securitykey/origins.json"_ns; +constexpr auto kGoogleAccountsAppId2 = + u"https://www.gstatic.com/securitykey/a/google.com/origins.json"_ns; + +bool EvaluateAppID(nsPIDOMWindowInner* aParent, const nsString& aOrigin, + /* 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 + if (!facetUri->SchemeIs("https")) { + return false; + } + + // If the appId is empty or null, overwrite it with the facetId and accept + if (aAppId.IsEmpty() || aAppId.EqualsLiteral("null")) { + aAppId.Assign(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. + if (!appIdUri->SchemeIs("https")) { + 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<Document> document = aParent->GetDoc(); + if (!document || !document->IsHTMLDocument()) { + return false; + } + + nsHTMLDocument* html = document->AsHTMLDocument(); + // 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 (lowestFacetHost.EqualsLiteral("google.com") && + (aAppId.Equals(kGoogleAccountsAppId1) || + aAppId.Equals(kGoogleAccountsAppId2))) { + return true; + } + + return false; +} + +static nsresult HashCString(nsICryptoHash* aHashService, const nsACString& aIn, + /* out */ nsTArray<uint8_t>& aOut) { + MOZ_ASSERT(aHashService); + + nsresult rv = aHashService->Init(nsICryptoHash::SHA256); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aHashService->Update( + reinterpret_cast<const uint8_t*>(aIn.BeginReading()), aIn.Length()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString fullHash; + // Passing false below means we will get a binary result rather than a + // base64-encoded string. + rv = aHashService->Finish(false, fullHash); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aOut.Clear(); + aOut.AppendElements(reinterpret_cast<uint8_t const*>(fullHash.BeginReading()), + fullHash.Length()); + + return NS_OK; +} + +nsresult HashCString(const nsACString& aIn, /* out */ nsTArray<uint8_t>& aOut) { + nsresult srv; + nsCOMPtr<nsICryptoHash> hashService = + do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv); + if (NS_FAILED(srv)) { + return srv; + } + + srv = HashCString(hashService, aIn, aOut); + if (NS_WARN_IF(NS_FAILED(srv))) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/WebAuthnUtil.h b/dom/webauthn/WebAuthnUtil.h new file mode 100644 index 0000000000..4c5841b347 --- /dev/null +++ b/dom/webauthn/WebAuthnUtil.h @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WebAuthnUtil_h +#define mozilla_dom_WebAuthnUtil_h + +/* + * Utility functions used by both WebAuthnManager and U2FTokenManager. + */ + +#include "mozilla/dom/WebAuthenticationBinding.h" +#include "ipc/IPCMessageUtils.h" + +namespace mozilla::dom { + +bool EvaluateAppID(nsPIDOMWindowInner* aParent, const nsString& aOrigin, + /* in/out */ nsString& aAppId); + +nsresult HashCString(const nsACString& aIn, /* out */ nsTArray<uint8_t>& aOut); + +} // namespace mozilla::dom + +#endif // mozilla_dom_WebAuthnUtil_h diff --git a/dom/webauthn/WinWebAuthnService.cpp b/dom/webauthn/WinWebAuthnService.cpp new file mode 100644 index 0000000000..8667cf5615 --- /dev/null +++ b/dom/webauthn/WinWebAuthnService.cpp @@ -0,0 +1,1047 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/PWebAuthnTransactionParent.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/Assertions.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Preferences.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/Unused.h" + +#include "nsTextFormatter.h" +#include "nsWindowsHelpers.h" +#include "WebAuthnAutoFillEntry.h" +#include "WebAuthnEnumStrings.h" +#include "WebAuthnResult.h" +#include "WebAuthnTransportIdentifiers.h" +#include "winwebauthn/webauthn.h" +#include "WinWebAuthnService.h" + +namespace mozilla::dom { + +namespace { +StaticRWLock gWinWebAuthnModuleLock; + +static bool gWinWebAuthnModuleUnusable = false; +static HMODULE gWinWebAuthnModule = 0; + +static decltype(WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable)* + gWinWebauthnIsUVPAA = nullptr; +static decltype(WebAuthNAuthenticatorMakeCredential)* + gWinWebauthnMakeCredential = nullptr; +static decltype(WebAuthNFreeCredentialAttestation)* + gWinWebauthnFreeCredentialAttestation = nullptr; +static decltype(WebAuthNAuthenticatorGetAssertion)* gWinWebauthnGetAssertion = + nullptr; +static decltype(WebAuthNFreeAssertion)* gWinWebauthnFreeAssertion = nullptr; +static decltype(WebAuthNGetCancellationId)* gWinWebauthnGetCancellationId = + nullptr; +static decltype(WebAuthNCancelCurrentOperation)* + gWinWebauthnCancelCurrentOperation = nullptr; +static decltype(WebAuthNGetErrorName)* gWinWebauthnGetErrorName = nullptr; +static decltype(WebAuthNGetApiVersionNumber)* gWinWebauthnGetApiVersionNumber = + nullptr; +static decltype(WebAuthNGetPlatformCredentialList)* + gWinWebauthnGetPlatformCredentialList = nullptr; +static decltype(WebAuthNFreePlatformCredentialList)* + gWinWebauthnFreePlatformCredentialList = nullptr; + +} // namespace + +/*********************************************************************** + * WinWebAuthnService Implementation + **********************************************************************/ + +constexpr uint32_t kMinWinWebAuthNApiVersion = WEBAUTHN_API_VERSION_1; + +NS_IMPL_ISUPPORTS(WinWebAuthnService, nsIWebAuthnService) + +/* static */ +nsresult WinWebAuthnService::EnsureWinWebAuthnModuleLoaded() { + { + StaticAutoReadLock moduleLock(gWinWebAuthnModuleLock); + if (gWinWebAuthnModule) { + // The module is already loaded. + return NS_OK; + } + if (gWinWebAuthnModuleUnusable) { + // A previous attempt to load the module failed. + return NS_ERROR_NOT_AVAILABLE; + } + } + + StaticAutoWriteLock lock(gWinWebAuthnModuleLock); + if (gWinWebAuthnModule) { + // Another thread successfully loaded the module while we were waiting. + return NS_OK; + } + if (gWinWebAuthnModuleUnusable) { + // Another thread failed to load the module while we were waiting. + return NS_ERROR_NOT_AVAILABLE; + } + + gWinWebAuthnModule = LoadLibrarySystem32(L"webauthn.dll"); + auto markModuleUnusable = MakeScopeExit([]() { + if (gWinWebAuthnModule) { + FreeLibrary(gWinWebAuthnModule); + gWinWebAuthnModule = 0; + } + gWinWebAuthnModuleUnusable = true; + }); + + if (!gWinWebAuthnModule) { + return NS_ERROR_NOT_AVAILABLE; + } + + gWinWebauthnIsUVPAA = reinterpret_cast< + decltype(WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable)*>( + GetProcAddress(gWinWebAuthnModule, + "WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable")); + gWinWebauthnMakeCredential = + reinterpret_cast<decltype(WebAuthNAuthenticatorMakeCredential)*>( + GetProcAddress(gWinWebAuthnModule, + "WebAuthNAuthenticatorMakeCredential")); + gWinWebauthnFreeCredentialAttestation = + reinterpret_cast<decltype(WebAuthNFreeCredentialAttestation)*>( + GetProcAddress(gWinWebAuthnModule, + "WebAuthNFreeCredentialAttestation")); + gWinWebauthnGetAssertion = + reinterpret_cast<decltype(WebAuthNAuthenticatorGetAssertion)*>( + GetProcAddress(gWinWebAuthnModule, + "WebAuthNAuthenticatorGetAssertion")); + gWinWebauthnFreeAssertion = + reinterpret_cast<decltype(WebAuthNFreeAssertion)*>( + GetProcAddress(gWinWebAuthnModule, "WebAuthNFreeAssertion")); + gWinWebauthnGetCancellationId = + reinterpret_cast<decltype(WebAuthNGetCancellationId)*>( + GetProcAddress(gWinWebAuthnModule, "WebAuthNGetCancellationId")); + gWinWebauthnCancelCurrentOperation = + reinterpret_cast<decltype(WebAuthNCancelCurrentOperation)*>( + GetProcAddress(gWinWebAuthnModule, "WebAuthNCancelCurrentOperation")); + gWinWebauthnGetErrorName = reinterpret_cast<decltype(WebAuthNGetErrorName)*>( + GetProcAddress(gWinWebAuthnModule, "WebAuthNGetErrorName")); + gWinWebauthnGetApiVersionNumber = + reinterpret_cast<decltype(WebAuthNGetApiVersionNumber)*>( + GetProcAddress(gWinWebAuthnModule, "WebAuthNGetApiVersionNumber")); + + if (!(gWinWebauthnIsUVPAA && gWinWebauthnMakeCredential && + gWinWebauthnFreeCredentialAttestation && gWinWebauthnGetAssertion && + gWinWebauthnFreeAssertion && gWinWebauthnGetCancellationId && + gWinWebauthnCancelCurrentOperation && gWinWebauthnGetErrorName && + gWinWebauthnGetApiVersionNumber)) { + return NS_ERROR_NOT_AVAILABLE; + } + + DWORD version = gWinWebauthnGetApiVersionNumber(); + + if (version >= WEBAUTHN_API_VERSION_4) { + gWinWebauthnGetPlatformCredentialList = + reinterpret_cast<decltype(WebAuthNGetPlatformCredentialList)*>( + GetProcAddress(gWinWebAuthnModule, + "WebAuthNGetPlatformCredentialList")); + gWinWebauthnFreePlatformCredentialList = + reinterpret_cast<decltype(WebAuthNFreePlatformCredentialList)*>( + GetProcAddress(gWinWebAuthnModule, + "WebAuthNFreePlatformCredentialList")); + if (!(gWinWebauthnGetPlatformCredentialList && + gWinWebauthnFreePlatformCredentialList)) { + return NS_ERROR_NOT_AVAILABLE; + } + } + + // Bug 1869584: In some of our tests, a content process can end up here due to + // a call to WinWebAuthnService::AreWebAuthNApisAvailable. This causes us to + // fail an assertion in Preferences::SetBool, which is parent-process only. + if (XRE_IsParentProcess()) { + NS_DispatchToMainThread(NS_NewRunnableFunction(__func__, [version]() { + Preferences::SetBool("security.webauthn.show_ms_settings_link", + version >= WEBAUTHN_API_VERSION_7); + })); + } + + markModuleUnusable.release(); + return NS_OK; +} + +WinWebAuthnService::~WinWebAuthnService() { + StaticAutoWriteLock lock(gWinWebAuthnModuleLock); + if (gWinWebAuthnModule) { + FreeLibrary(gWinWebAuthnModule); + } + gWinWebAuthnModule = 0; +} + +// static +bool WinWebAuthnService::AreWebAuthNApisAvailable() { + nsresult rv = EnsureWinWebAuthnModuleLoaded(); + NS_ENSURE_SUCCESS(rv, false); + + StaticAutoReadLock moduleLock(gWinWebAuthnModuleLock); + return gWinWebAuthnModule && + gWinWebauthnGetApiVersionNumber() >= kMinWinWebAuthNApiVersion; +} + +NS_IMETHODIMP +WinWebAuthnService::GetIsUVPAA(bool* aAvailable) { + nsresult rv = EnsureWinWebAuthnModuleLoaded(); + NS_ENSURE_SUCCESS(rv, rv); + + if (WinWebAuthnService::AreWebAuthNApisAvailable()) { + BOOL isUVPAA = FALSE; + StaticAutoReadLock moduleLock(gWinWebAuthnModuleLock); + *aAvailable = gWinWebAuthnModule && gWinWebauthnIsUVPAA(&isUVPAA) == S_OK && + isUVPAA == TRUE; + } else { + *aAvailable = false; + } + return NS_OK; +} + +NS_IMETHODIMP +WinWebAuthnService::Cancel(uint64_t aTransactionId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WinWebAuthnService::Reset() { + // Reset will never be the first function to use gWinWebAuthnModule, so + // we shouldn't try to initialize it here. + auto guard = mTransactionState.Lock(); + if (guard->isSome()) { + StaticAutoReadLock moduleLock(gWinWebAuthnModuleLock); + if (gWinWebAuthnModule) { + const GUID cancellationId = guard->ref().cancellationId; + gWinWebauthnCancelCurrentOperation(&cancellationId); + } + if (guard->ref().pendingSignPromise.isSome()) { + // This request was never dispatched to the platform API, so + // we need to reject the promise ourselves. + guard->ref().pendingSignPromise.ref()->Reject( + NS_ERROR_DOM_NOT_ALLOWED_ERR); + } + guard->reset(); + } + + return NS_OK; +} + +NS_IMETHODIMP +WinWebAuthnService::MakeCredential(uint64_t aTransactionId, + uint64_t aBrowsingContextId, + nsIWebAuthnRegisterArgs* aArgs, + nsIWebAuthnRegisterPromise* aPromise) { + nsresult rv = EnsureWinWebAuthnModuleLoaded(); + NS_ENSURE_SUCCESS(rv, rv); + + Reset(); + auto guard = mTransactionState.Lock(); + StaticAutoReadLock moduleLock(gWinWebAuthnModuleLock); + GUID cancellationId; + if (gWinWebauthnGetCancellationId(&cancellationId) != S_OK) { + // caller will reject promise + return NS_ERROR_DOM_UNKNOWN_ERR; + } + *guard = Some(TransactionState{ + aTransactionId, + aBrowsingContextId, + Nothing(), + Nothing(), + cancellationId, + }); + + nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction( + "WinWebAuthnService::MakeCredential", + [self = RefPtr{this}, aArgs = RefPtr{aArgs}, aPromise = RefPtr{aPromise}, + cancellationId]() mutable { + // Take a read lock on gWinWebAuthnModuleLock to prevent the module from + // being unloaded while the operation is in progress. This does not + // prevent the operation from being cancelled, so it does not block a + // clean shutdown. + StaticAutoReadLock moduleLock(gWinWebAuthnModuleLock); + if (!gWinWebAuthnModule) { + aPromise->Reject(NS_ERROR_DOM_UNKNOWN_ERR); + return; + } + + BOOL HmacCreateSecret = FALSE; + BOOL MinPinLength = FALSE; + + // RP Information + nsString rpId; + Unused << aArgs->GetRpId(rpId); + WEBAUTHN_RP_ENTITY_INFORMATION rpInfo = { + WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION, rpId.get(), nullptr, + nullptr}; + + // User Information + WEBAUTHN_USER_ENTITY_INFORMATION userInfo = { + WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION, + 0, + nullptr, + nullptr, + nullptr, + nullptr}; + + // Client Data + nsCString clientDataJSON; + Unused << aArgs->GetClientDataJSON(clientDataJSON); + WEBAUTHN_CLIENT_DATA WebAuthNClientData = { + WEBAUTHN_CLIENT_DATA_CURRENT_VERSION, + (DWORD)clientDataJSON.Length(), (BYTE*)(clientDataJSON.get()), + WEBAUTHN_HASH_ALGORITHM_SHA_256}; + + // User Verification Requirement + DWORD winUserVerificationReq = + WEBAUTHN_USER_VERIFICATION_REQUIREMENT_ANY; + + // Resident Key + BOOL winRequireResidentKey = FALSE; + BOOL winPreferResidentKey = FALSE; + + // AttestationConveyance + DWORD winAttestation = WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_ANY; + + nsString rpName; + Unused << aArgs->GetRpName(rpName); + rpInfo.pwszName = rpName.get(); + rpInfo.pwszIcon = nullptr; + + nsTArray<uint8_t> userId; + Unused << aArgs->GetUserId(userId); + userInfo.cbId = static_cast<DWORD>(userId.Length()); + userInfo.pbId = const_cast<unsigned char*>(userId.Elements()); + + nsString userName; + Unused << aArgs->GetUserName(userName); + userInfo.pwszName = userName.get(); + + userInfo.pwszIcon = nullptr; + + nsString userDisplayName; + Unused << aArgs->GetUserDisplayName(userDisplayName); + userInfo.pwszDisplayName = userDisplayName.get(); + + // Algorithms + nsTArray<WEBAUTHN_COSE_CREDENTIAL_PARAMETER> coseParams; + nsTArray<int32_t> coseAlgs; + Unused << aArgs->GetCoseAlgs(coseAlgs); + for (const int32_t& coseAlg : coseAlgs) { + WEBAUTHN_COSE_CREDENTIAL_PARAMETER coseAlgorithm = { + WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION, + WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY, coseAlg}; + coseParams.AppendElement(coseAlgorithm); + } + + nsString userVerificationReq; + Unused << aArgs->GetUserVerification(userVerificationReq); + // This mapping needs to be reviewed if values are added to the + // UserVerificationRequirement enum. + static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); + if (userVerificationReq.EqualsLiteral( + MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED)) { + winUserVerificationReq = + WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED; + } else if (userVerificationReq.EqualsLiteral( + MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED)) { + winUserVerificationReq = + WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED; + } else if (userVerificationReq.EqualsLiteral( + MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED)) { + winUserVerificationReq = + WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED; + } else { + winUserVerificationReq = WEBAUTHN_USER_VERIFICATION_REQUIREMENT_ANY; + } + + // Attachment + DWORD winAttachment = WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY; + nsString authenticatorAttachment; + nsresult rv = + aArgs->GetAuthenticatorAttachment(authenticatorAttachment); + if (rv != NS_ERROR_NOT_AVAILABLE) { + if (NS_FAILED(rv)) { + aPromise->Reject(rv); + return; + } + // This mapping needs to be reviewed if values are added to the + // AuthenticatorAttachement enum. + static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); + if (authenticatorAttachment.EqualsLiteral( + MOZ_WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM)) { + winAttachment = WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM; + } else if ( + authenticatorAttachment.EqualsLiteral( + MOZ_WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM)) { + winAttachment = WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM; + } else { + winAttachment = WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY; + } + } + + nsString residentKey; + Unused << aArgs->GetResidentKey(residentKey); + // This mapping needs to be reviewed if values are added to the + // ResidentKeyRequirement enum. + static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); + if (residentKey.EqualsLiteral( + MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED)) { + winRequireResidentKey = TRUE; + winPreferResidentKey = TRUE; + } else if (residentKey.EqualsLiteral( + MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_PREFERRED)) { + winRequireResidentKey = FALSE; + winPreferResidentKey = TRUE; + } else if (residentKey.EqualsLiteral( + MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED)) { + winRequireResidentKey = FALSE; + winPreferResidentKey = FALSE; + } else { + // WebAuthnManager::MakeCredential is supposed to assign one of the + // above values, so this shouldn't happen. + MOZ_ASSERT_UNREACHABLE(); + aPromise->Reject(NS_ERROR_DOM_UNKNOWN_ERR); + return; + } + + // AttestationConveyance + nsString attestation; + Unused << aArgs->GetAttestationConveyancePreference(attestation); + bool anonymize = false; + // This mapping needs to be reviewed if values are added to the + // AttestationConveyancePreference enum. + static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); + if (attestation.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE)) { + winAttestation = WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE; + anonymize = true; + } else if ( + attestation.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT)) { + winAttestation = WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT; + } else if (attestation.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT)) { + winAttestation = WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT; + } else { + winAttestation = WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_ANY; + } + + bool requestedCredProps; + Unused << aArgs->GetCredProps(&requestedCredProps); + + bool requestedMinPinLength; + Unused << aArgs->GetMinPinLength(&requestedMinPinLength); + + bool requestedHmacCreateSecret; + Unused << aArgs->GetHmacCreateSecret(&requestedHmacCreateSecret); + + // Extensions that might require an entry: hmac-secret, minPinLength. + WEBAUTHN_EXTENSION rgExtension[2] = {}; + DWORD cExtensions = 0; + if (requestedHmacCreateSecret) { + HmacCreateSecret = TRUE; + rgExtension[cExtensions].pwszExtensionIdentifier = + WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET; + rgExtension[cExtensions].cbExtension = sizeof(BOOL); + rgExtension[cExtensions].pvExtension = &HmacCreateSecret; + cExtensions++; + } + if (requestedMinPinLength) { + MinPinLength = TRUE; + rgExtension[cExtensions].pwszExtensionIdentifier = + WEBAUTHN_EXTENSIONS_IDENTIFIER_MIN_PIN_LENGTH; + rgExtension[cExtensions].cbExtension = sizeof(BOOL); + rgExtension[cExtensions].pvExtension = &MinPinLength; + cExtensions++; + } + + WEBAUTHN_COSE_CREDENTIAL_PARAMETERS WebAuthNCredentialParameters = { + static_cast<DWORD>(coseParams.Length()), coseParams.Elements()}; + + // Exclude Credentials + nsTArray<nsTArray<uint8_t>> excludeList; + Unused << aArgs->GetExcludeList(excludeList); + + nsTArray<uint8_t> excludeListTransports; + Unused << aArgs->GetExcludeListTransports(excludeListTransports); + + if (excludeList.Length() != excludeListTransports.Length()) { + aPromise->Reject(NS_ERROR_DOM_UNKNOWN_ERR); + return; + } + + nsTArray<WEBAUTHN_CREDENTIAL_EX> excludeCredentials; + WEBAUTHN_CREDENTIAL_EX* pExcludeCredentials = nullptr; + nsTArray<WEBAUTHN_CREDENTIAL_EX*> excludeCredentialsPtrs; + WEBAUTHN_CREDENTIAL_LIST excludeCredentialList = {0}; + WEBAUTHN_CREDENTIAL_LIST* pExcludeCredentialList = nullptr; + + for (size_t i = 0; i < excludeList.Length(); i++) { + nsTArray<uint8_t>& cred = excludeList[i]; + uint8_t& transports = excludeListTransports[i]; + DWORD winTransports = 0; + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB) { + winTransports |= WEBAUTHN_CTAP_TRANSPORT_USB; + } + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC) { + winTransports |= WEBAUTHN_CTAP_TRANSPORT_NFC; + } + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE) { + winTransports |= WEBAUTHN_CTAP_TRANSPORT_BLE; + } + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL) { + winTransports |= WEBAUTHN_CTAP_TRANSPORT_INTERNAL; + } + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_HYBRID) { + winTransports |= WEBAUTHN_CTAP_TRANSPORT_HYBRID; + } + + WEBAUTHN_CREDENTIAL_EX credential = { + WEBAUTHN_CREDENTIAL_EX_CURRENT_VERSION, + static_cast<DWORD>(cred.Length()), (PBYTE)(cred.Elements()), + WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY, winTransports}; + excludeCredentials.AppendElement(credential); + } + + if (!excludeCredentials.IsEmpty()) { + pExcludeCredentials = excludeCredentials.Elements(); + for (DWORD i = 0; i < excludeCredentials.Length(); i++) { + excludeCredentialsPtrs.AppendElement(&pExcludeCredentials[i]); + } + excludeCredentialList.cCredentials = excludeCredentials.Length(); + excludeCredentialList.ppCredentials = + excludeCredentialsPtrs.Elements(); + pExcludeCredentialList = &excludeCredentialList; + } + + uint32_t timeout_u32; + Unused << aArgs->GetTimeoutMS(&timeout_u32); + DWORD timeout = timeout_u32; + + // MakeCredentialOptions + WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS + WebAuthNCredentialOptions = { + WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_7, + timeout, + {0, NULL}, + {0, NULL}, + winAttachment, + winRequireResidentKey, + winUserVerificationReq, + winAttestation, + 0, // Flags + &cancellationId, // CancellationId + pExcludeCredentialList, + WEBAUTHN_ENTERPRISE_ATTESTATION_NONE, + WEBAUTHN_LARGE_BLOB_SUPPORT_NONE, + winPreferResidentKey, // PreferResidentKey + FALSE, // BrowserInPrivateMode + FALSE, // EnablePrf + NULL, // LinkedDevice + 0, // size of JsonExt + NULL, // JsonExt + }; + + if (cExtensions != 0) { + WebAuthNCredentialOptions.Extensions.cExtensions = cExtensions; + WebAuthNCredentialOptions.Extensions.pExtensions = rgExtension; + } + + PWEBAUTHN_CREDENTIAL_ATTESTATION pWebAuthNCredentialAttestation = + nullptr; + + // Bug 1518876: Get Window Handle from Content process for Windows + // WebAuthN APIs + HWND hWnd = GetForegroundWindow(); + + HRESULT hr = gWinWebauthnMakeCredential( + hWnd, &rpInfo, &userInfo, &WebAuthNCredentialParameters, + &WebAuthNClientData, &WebAuthNCredentialOptions, + &pWebAuthNCredentialAttestation); + + if (hr == S_OK) { + RefPtr<WebAuthnRegisterResult> result = new WebAuthnRegisterResult( + clientDataJSON, pWebAuthNCredentialAttestation); + + // WEBAUTHN_CREDENTIAL_ATTESTATION structs of version >= 4 always + // include a flag to indicate whether a resident key was created. We + // copy that flag to the credProps extension output only if the RP + // requested the credProps extension. + if (requestedCredProps && + pWebAuthNCredentialAttestation->dwVersion >= + WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_4) { + BOOL rk = pWebAuthNCredentialAttestation->bResidentKey; + Unused << result->SetCredPropsRk(rk == TRUE); + } + gWinWebauthnFreeCredentialAttestation(pWebAuthNCredentialAttestation); + + if (anonymize) { + nsresult rv = result->Anonymize(); + if (NS_FAILED(rv)) { + aPromise->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR); + return; + } + } + aPromise->Resolve(result); + } else { + PCWSTR errorName = gWinWebauthnGetErrorName(hr); + nsresult aError = NS_ERROR_DOM_ABORT_ERR; + + if (_wcsicmp(errorName, L"InvalidStateError") == 0) { + aError = NS_ERROR_DOM_INVALID_STATE_ERR; + } else if (_wcsicmp(errorName, L"ConstraintError") == 0 || + _wcsicmp(errorName, L"UnknownError") == 0) { + aError = NS_ERROR_DOM_UNKNOWN_ERR; + } else if (_wcsicmp(errorName, L"NotSupportedError") == 0) { + aError = NS_ERROR_DOM_INVALID_STATE_ERR; + } else if (_wcsicmp(errorName, L"NotAllowedError") == 0) { + aError = NS_ERROR_DOM_NOT_ALLOWED_ERR; + } + + aPromise->Reject(aError); + } + })); + + NS_DispatchBackgroundTask(runnable, NS_DISPATCH_EVENT_MAY_BLOCK); + return NS_OK; +} + +NS_IMETHODIMP +WinWebAuthnService::GetAssertion(uint64_t aTransactionId, + uint64_t aBrowsingContextId, + nsIWebAuthnSignArgs* aArgs, + nsIWebAuthnSignPromise* aPromise) { + nsresult rv = EnsureWinWebAuthnModuleLoaded(); + NS_ENSURE_SUCCESS(rv, rv); + + Reset(); + + auto guard = mTransactionState.Lock(); + + GUID cancellationId; + { + StaticAutoReadLock moduleLock(gWinWebAuthnModuleLock); + if (gWinWebauthnGetCancellationId(&cancellationId) != S_OK) { + // caller will reject promise + return NS_ERROR_DOM_UNKNOWN_ERR; + } + } + + *guard = Some(TransactionState{ + aTransactionId, + aBrowsingContextId, + Some(RefPtr{aArgs}), + Some(RefPtr{aPromise}), + cancellationId, + }); + + bool conditionallyMediated; + Unused << aArgs->GetConditionallyMediated(&conditionallyMediated); + if (!conditionallyMediated) { + DoGetAssertion(Nothing(), guard); + } + return NS_OK; +} + +void WinWebAuthnService::DoGetAssertion( + Maybe<nsTArray<uint8_t>>&& aSelectedCredentialId, + const TransactionStateMutex::AutoLock& aGuard) { + if (aGuard->isNothing() || aGuard->ref().pendingSignArgs.isNothing() || + aGuard->ref().pendingSignPromise.isNothing()) { + return; + } + + // Take the pending Args and Promise to prevent repeated calls to + // DoGetAssertion for this transaction. + RefPtr<nsIWebAuthnSignArgs> aArgs = aGuard->ref().pendingSignArgs.extract(); + RefPtr<nsIWebAuthnSignPromise> aPromise = + aGuard->ref().pendingSignPromise.extract(); + + nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction( + "WinWebAuthnService::MakeCredential", + [self = RefPtr{this}, aArgs, aPromise, + aSelectedCredentialId = std::move(aSelectedCredentialId), + aCancellationId = aGuard->ref().cancellationId]() mutable { + // Take a read lock on gWinWebAuthnModuleLock to prevent the module from + // being unloaded while the operation is in progress. This does not + // prevent the operation from being cancelled, so it does not block a + // clean shutdown. + StaticAutoReadLock moduleLock(gWinWebAuthnModuleLock); + if (!gWinWebAuthnModule) { + aPromise->Reject(NS_ERROR_DOM_UNKNOWN_ERR); + return; + } + + // Attachment + DWORD winAttachment = WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY; + + // AppId + BOOL bAppIdUsed = FALSE; + BOOL* pbAppIdUsed = nullptr; + PCWSTR winAppIdentifier = nullptr; + + // Client Data + nsCString clientDataJSON; + Unused << aArgs->GetClientDataJSON(clientDataJSON); + WEBAUTHN_CLIENT_DATA WebAuthNClientData = { + WEBAUTHN_CLIENT_DATA_CURRENT_VERSION, + (DWORD)clientDataJSON.Length(), (BYTE*)(clientDataJSON.get()), + WEBAUTHN_HASH_ALGORITHM_SHA_256}; + + nsString appId; + nsresult rv = aArgs->GetAppId(appId); + if (rv != NS_ERROR_NOT_AVAILABLE) { + if (NS_FAILED(rv)) { + aPromise->Reject(rv); + return; + } + winAppIdentifier = appId.get(); + pbAppIdUsed = &bAppIdUsed; + } + + // RPID + nsString rpId; + Unused << aArgs->GetRpId(rpId); + + // User Verification Requirement + nsString userVerificationReq; + Unused << aArgs->GetUserVerification(userVerificationReq); + DWORD winUserVerificationReq; + // This mapping needs to be reviewed if values are added to the + // UserVerificationRequirement enum. + static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); + if (userVerificationReq.EqualsLiteral( + MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED)) { + winUserVerificationReq = + WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED; + } else if (userVerificationReq.EqualsLiteral( + MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED)) { + winUserVerificationReq = + WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED; + } else if (userVerificationReq.EqualsLiteral( + MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED)) { + winUserVerificationReq = + WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED; + } else { + winUserVerificationReq = WEBAUTHN_USER_VERIFICATION_REQUIREMENT_ANY; + } + + // allow Credentials + nsTArray<nsTArray<uint8_t>> allowList; + nsTArray<uint8_t> allowListTransports; + if (aSelectedCredentialId.isSome()) { + allowList.AppendElement(aSelectedCredentialId.extract()); + allowListTransports.AppendElement( + MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL); + } else { + Unused << aArgs->GetAllowList(allowList); + Unused << aArgs->GetAllowListTransports(allowListTransports); + } + + if (allowList.Length() != allowListTransports.Length()) { + aPromise->Reject(NS_ERROR_DOM_UNKNOWN_ERR); + return; + } + + nsTArray<WEBAUTHN_CREDENTIAL_EX> allowCredentials; + WEBAUTHN_CREDENTIAL_EX* pAllowCredentials = nullptr; + nsTArray<WEBAUTHN_CREDENTIAL_EX*> allowCredentialsPtrs; + WEBAUTHN_CREDENTIAL_LIST allowCredentialList = {0}; + WEBAUTHN_CREDENTIAL_LIST* pAllowCredentialList = nullptr; + + for (size_t i = 0; i < allowList.Length(); i++) { + nsTArray<uint8_t>& cred = allowList[i]; + uint8_t& transports = allowListTransports[i]; + DWORD winTransports = 0; + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB) { + winTransports |= WEBAUTHN_CTAP_TRANSPORT_USB; + } + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC) { + winTransports |= WEBAUTHN_CTAP_TRANSPORT_NFC; + } + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE) { + winTransports |= WEBAUTHN_CTAP_TRANSPORT_BLE; + } + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL) { + winTransports |= WEBAUTHN_CTAP_TRANSPORT_INTERNAL; + } + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_HYBRID) { + winTransports |= WEBAUTHN_CTAP_TRANSPORT_HYBRID; + } + + WEBAUTHN_CREDENTIAL_EX credential = { + WEBAUTHN_CREDENTIAL_EX_CURRENT_VERSION, + static_cast<DWORD>(cred.Length()), (PBYTE)(cred.Elements()), + WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY, winTransports}; + allowCredentials.AppendElement(credential); + } + + if (allowCredentials.Length()) { + pAllowCredentials = allowCredentials.Elements(); + for (DWORD i = 0; i < allowCredentials.Length(); i++) { + allowCredentialsPtrs.AppendElement(&pAllowCredentials[i]); + } + allowCredentialList.cCredentials = allowCredentials.Length(); + allowCredentialList.ppCredentials = allowCredentialsPtrs.Elements(); + pAllowCredentialList = &allowCredentialList; + } + + uint32_t timeout_u32; + Unused << aArgs->GetTimeoutMS(&timeout_u32); + DWORD timeout = timeout_u32; + + WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS WebAuthNAssertionOptions = + { + WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_7, + timeout, + {0, NULL}, + {0, NULL}, + winAttachment, + winUserVerificationReq, + 0, // dwFlags + winAppIdentifier, + pbAppIdUsed, + &aCancellationId, // CancellationId + pAllowCredentialList, + WEBAUTHN_CRED_LARGE_BLOB_OPERATION_NONE, + 0, // Size of CredLargeBlob + NULL, // CredLargeBlob + NULL, // HmacSecretSaltValues + FALSE, // BrowserInPrivateMode + NULL, // LinkedDevice + FALSE, // AutoFill + 0, // Size of JsonExt + NULL, // JsonExt + }; + + PWEBAUTHN_ASSERTION pWebAuthNAssertion = nullptr; + + // Bug 1518876: Get Window Handle from Content process for Windows + // WebAuthN APIs + HWND hWnd = GetForegroundWindow(); + + HRESULT hr = gWinWebauthnGetAssertion( + hWnd, rpId.get(), &WebAuthNClientData, &WebAuthNAssertionOptions, + &pWebAuthNAssertion); + + if (hr == S_OK) { + RefPtr<WebAuthnSignResult> result = + new WebAuthnSignResult(clientDataJSON, pWebAuthNAssertion); + gWinWebauthnFreeAssertion(pWebAuthNAssertion); + if (winAppIdentifier != nullptr) { + // The gWinWebauthnGetAssertion call modified bAppIdUsed through + // a pointer provided in WebAuthNAssertionOptions. + Unused << result->SetUsedAppId(bAppIdUsed == TRUE); + } + aPromise->Resolve(result); + } else { + PCWSTR errorName = gWinWebauthnGetErrorName(hr); + nsresult aError = NS_ERROR_DOM_ABORT_ERR; + + if (_wcsicmp(errorName, L"InvalidStateError") == 0) { + aError = NS_ERROR_DOM_INVALID_STATE_ERR; + } else if (_wcsicmp(errorName, L"ConstraintError") == 0 || + _wcsicmp(errorName, L"UnknownError") == 0) { + aError = NS_ERROR_DOM_UNKNOWN_ERR; + } else if (_wcsicmp(errorName, L"NotSupportedError") == 0) { + aError = NS_ERROR_DOM_INVALID_STATE_ERR; + } else if (_wcsicmp(errorName, L"NotAllowedError") == 0) { + aError = NS_ERROR_DOM_NOT_ALLOWED_ERR; + } + + aPromise->Reject(aError); + } + })); + + NS_DispatchBackgroundTask(runnable, NS_DISPATCH_EVENT_MAY_BLOCK); +} + +NS_IMETHODIMP +WinWebAuthnService::HasPendingConditionalGet(uint64_t aBrowsingContextId, + const nsAString& aOrigin, + uint64_t* aRv) { + auto guard = mTransactionState.Lock(); + if (guard->isNothing() || + guard->ref().browsingContextId != aBrowsingContextId || + guard->ref().pendingSignArgs.isNothing()) { + *aRv = 0; + return NS_OK; + } + + nsString origin; + Unused << guard->ref().pendingSignArgs.ref()->GetOrigin(origin); + if (origin != aOrigin) { + *aRv = 0; + return NS_OK; + } + + *aRv = guard->ref().transactionId; + return NS_OK; +} + +NS_IMETHODIMP +WinWebAuthnService::GetAutoFillEntries( + uint64_t aTransactionId, nsTArray<RefPtr<nsIWebAuthnAutoFillEntry>>& aRv) { + auto guard = mTransactionState.Lock(); + if (guard->isNothing() || guard->ref().transactionId != aTransactionId || + guard->ref().pendingSignArgs.isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + StaticAutoReadLock moduleLock(gWinWebAuthnModuleLock); + if (!gWinWebAuthnModule) { + return NS_ERROR_NOT_AVAILABLE; + } + + aRv.Clear(); + + if (gWinWebauthnGetApiVersionNumber() < WEBAUTHN_API_VERSION_4) { + // GetPlatformCredentialList was added in version 4. Earlier versions + // can still present a generic "Use a Passkey" autofill entry, so + // this isn't an error. + return NS_OK; + } + + nsString rpId; + Unused << guard->ref().pendingSignArgs.ref()->GetRpId(rpId); + + WEBAUTHN_GET_CREDENTIALS_OPTIONS getCredentialsOptions{ + WEBAUTHN_GET_CREDENTIALS_OPTIONS_VERSION_1, + rpId.get(), // pwszRpId + FALSE, // bBrowserInPrivateMode + }; + PWEBAUTHN_CREDENTIAL_DETAILS_LIST pCredentialList = nullptr; + HRESULT hr = gWinWebauthnGetPlatformCredentialList(&getCredentialsOptions, + &pCredentialList); + // WebAuthNGetPlatformCredentialList has an _Outptr_result_maybenull_ + // annotation and a comment "Returns NTE_NOT_FOUND when credentials are + // not found." + if (pCredentialList == nullptr) { + if (hr != NTE_NOT_FOUND) { + return NS_ERROR_FAILURE; + } + return NS_OK; + } + MOZ_ASSERT(hr == S_OK); + for (size_t i = 0; i < pCredentialList->cCredentialDetails; i++) { + RefPtr<nsIWebAuthnAutoFillEntry> entry( + new WebAuthnAutoFillEntry(pCredentialList->ppCredentialDetails[i])); + aRv.AppendElement(entry); + } + gWinWebauthnFreePlatformCredentialList(pCredentialList); + return NS_OK; +} + +NS_IMETHODIMP +WinWebAuthnService::SelectAutoFillEntry( + uint64_t aTransactionId, const nsTArray<uint8_t>& aCredentialId) { + auto guard = mTransactionState.Lock(); + if (guard->isNothing() || guard->ref().transactionId != aTransactionId || + guard->ref().pendingSignArgs.isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsTArray<nsTArray<uint8_t>> allowList; + Unused << guard->ref().pendingSignArgs.ref()->GetAllowList(allowList); + if (!allowList.IsEmpty() && !allowList.Contains(aCredentialId)) { + return NS_ERROR_FAILURE; + } + + Maybe<nsTArray<uint8_t>> id; + id.emplace(); + id.ref().Assign(aCredentialId); + DoGetAssertion(std::move(id), guard); + + return NS_OK; +} + +NS_IMETHODIMP +WinWebAuthnService::ResumeConditionalGet(uint64_t aTransactionId) { + auto guard = mTransactionState.Lock(); + if (guard->isNothing() || guard->ref().transactionId != aTransactionId || + guard->ref().pendingSignArgs.isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + DoGetAssertion(Nothing(), guard); + return NS_OK; +} + +NS_IMETHODIMP +WinWebAuthnService::PinCallback(uint64_t aTransactionId, + const nsACString& aPin) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WinWebAuthnService::ResumeMakeCredential(uint64_t aTransactionId, + bool aForceNoneAttestation) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WinWebAuthnService::SelectionCallback(uint64_t aTransactionId, + uint64_t aIndex) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WinWebAuthnService::AddVirtualAuthenticator( + const nsACString& protocol, const nsACString& transport, + bool hasResidentKey, bool hasUserVerification, bool isUserConsenting, + bool isUserVerified, uint64_t* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WinWebAuthnService::RemoveVirtualAuthenticator(uint64_t authenticatorId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WinWebAuthnService::AddCredential(uint64_t authenticatorId, + const nsACString& credentialId, + bool isResidentCredential, + const nsACString& rpId, + const nsACString& privateKey, + const nsACString& userHandle, + uint32_t signCount) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WinWebAuthnService::GetCredentials( + uint64_t authenticatorId, + nsTArray<RefPtr<nsICredentialParameters>>& _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WinWebAuthnService::RemoveCredential(uint64_t authenticatorId, + const nsACString& credentialId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WinWebAuthnService::RemoveAllCredentials(uint64_t authenticatorId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WinWebAuthnService::SetUserVerified(uint64_t authenticatorId, + bool isUserVerified) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WinWebAuthnService::Listen() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +WinWebAuthnService::RunCommand(const nsACString& cmd) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/WinWebAuthnService.h b/dom/webauthn/WinWebAuthnService.h new file mode 100644 index 0000000000..8312ae83e1 --- /dev/null +++ b/dom/webauthn/WinWebAuthnService.h @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WinWebAuthnService_h +#define mozilla_dom_WinWebAuthnService_h + +#include "mozilla/dom/PWebAuthnTransaction.h" +#include "mozilla/Tainting.h" +#include "nsIWebAuthnService.h" + +namespace mozilla::dom { + +class WinWebAuthnService final : public nsIWebAuthnService { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBAUTHNSERVICE + + static bool IsUserVerifyingPlatformAuthenticatorAvailable(); + static bool AreWebAuthNApisAvailable(); + static nsresult EnsureWinWebAuthnModuleLoaded(); + + WinWebAuthnService() + : mTransactionState(Nothing(), "WinWebAuthnService::mTransactionState"){}; + + private: + ~WinWebAuthnService(); + + uint32_t GetWebAuthNApiVersion(); + + struct TransactionState { + uint64_t transactionId; + uint64_t browsingContextId; + Maybe<RefPtr<nsIWebAuthnSignArgs>> pendingSignArgs; + Maybe<RefPtr<nsIWebAuthnSignPromise>> pendingSignPromise; + GUID cancellationId; + }; + + using TransactionStateMutex = DataMutex<Maybe<TransactionState>>; + TransactionStateMutex mTransactionState; + void DoGetAssertion(Maybe<nsTArray<uint8_t>>&& aSelectedCredentialId, + const TransactionStateMutex::AutoLock& aGuard); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WinWebAuthnService_h diff --git a/dom/webauthn/authrs_bridge/Cargo.toml b/dom/webauthn/authrs_bridge/Cargo.toml new file mode 100644 index 0000000000..44c690a2b1 --- /dev/null +++ b/dom/webauthn/authrs_bridge/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "authrs_bridge" +version = "0.1.0" +edition = "2021" +authors = ["Martin Sirringhaus", "John Schanck"] + +[dependencies] +authenticator = { version = "0.4.0-alpha.24", features = ["gecko"] } +base64 = "^0.21" +cstr = "0.2" +log = "0.4" +moz_task = { path = "../../../xpcom/rust/moz_task" } +nserror = { path = "../../../xpcom/rust/nserror" } +nsstring = { path = "../../../xpcom/rust/nsstring" } +rand = "0.8" +serde = { version = "1", features = ["derive"] } +serde_cbor = "0.11" +serde_json = "1.0" +static_prefs = { path = "../../../modules/libpref/init/static_prefs" } +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +xpcom = { path = "../../../xpcom/rust/xpcom" } + +[features] +fuzzing = [] diff --git a/dom/webauthn/authrs_bridge/src/about_webauthn_controller.rs b/dom/webauthn/authrs_bridge/src/about_webauthn_controller.rs new file mode 100644 index 0000000000..8d77a62df4 --- /dev/null +++ b/dom/webauthn/authrs_bridge/src/about_webauthn_controller.rs @@ -0,0 +1,166 @@ +/* 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 super::*; +use authenticator::{ + ctap2::commands::{PinUvAuthResult, StatusCode}, + errors::{CommandError, HIDError}, + BioEnrollmentCmd, CredManagementCmd, InteractiveRequest, InteractiveUpdate, PinError, +}; +use serde::{Deserialize, Serialize}; + +pub(crate) type InteractiveManagementReceiver = Option<Sender<InteractiveRequest>>; +pub(crate) fn send_about_prompt(prompt: &BrowserPromptType) -> Result<(), nsresult> { + let json = nsString::from(&serde_json::to_string(&prompt).unwrap_or_default()); + notify_observers(PromptTarget::AboutPage, json) +} + +// A wrapper around InteractiveRequest, that leaves out the PUAT +// so that we can easily de/serialize it to/from JSON for the JS-side +// and then add our cached PUAT, if we have one. +#[derive(Debug, Serialize, Deserialize)] +pub enum RequestWrapper { + Quit, + ChangePIN(Pin, Pin), + SetPIN(Pin), + CredentialManagement(CredManagementCmd), + BioEnrollment(BioEnrollmentCmd), +} + +pub(crate) fn authrs_to_prompt<'a>(e: AuthenticatorError) -> BrowserPromptType<'a> { + match e { + AuthenticatorError::PinError(PinError::PinIsTooShort) => BrowserPromptType::PinIsTooShort, + AuthenticatorError::PinError(PinError::PinNotSet) => BrowserPromptType::PinNotSet, + AuthenticatorError::PinError(PinError::PinRequired) => BrowserPromptType::PinRequired, + AuthenticatorError::PinError(PinError::PinIsTooLong(_)) => BrowserPromptType::PinIsTooLong, + AuthenticatorError::PinError(PinError::InvalidPin(r)) => { + BrowserPromptType::PinInvalid { retries: r } + } + AuthenticatorError::PinError(PinError::PinAuthBlocked) => BrowserPromptType::PinAuthBlocked, + AuthenticatorError::PinError(PinError::PinBlocked) => BrowserPromptType::DeviceBlocked, + AuthenticatorError::PinError(PinError::UvBlocked) => BrowserPromptType::UvBlocked, + AuthenticatorError::PinError(PinError::InvalidUv(r)) => { + BrowserPromptType::UvInvalid { retries: r } + } + AuthenticatorError::CancelledByUser + | AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode( + StatusCode::KeepaliveCancel, + _, + ))) => BrowserPromptType::Cancel, + _ => BrowserPromptType::UnknownError, + } +} + +pub(crate) fn cache_puat( + transaction: Arc<Mutex<Option<TransactionState>>>, + puat: Option<PinUvAuthResult>, +) { + let mut guard = transaction.lock().unwrap(); + if let Some(transaction) = guard.as_mut() { + transaction.puat_cache = puat; + }; +} + +pub(crate) fn interactive_status_callback( + status_rx: Receiver<StatusUpdate>, + transaction: Arc<Mutex<Option<TransactionState>>>, /* Shared with an AuthrsTransport */ + upcoming_error: Arc<Mutex<Option<AuthenticatorError>>>, +) -> Result<(), nsresult> { + loop { + match status_rx.recv() { + Ok(StatusUpdate::InteractiveManagement(InteractiveUpdate::StartManagement(( + tx, + auth_info, + )))) => { + let mut guard = transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + warn!("STATUS: received status update after end of transaction."); + break; + }; + transaction.interactive_receiver.replace(tx); + let prompt = BrowserPromptType::SelectedDevice { auth_info }; + send_about_prompt(&prompt)?; + } + Ok(StatusUpdate::InteractiveManagement( + InteractiveUpdate::CredentialManagementUpdate((cfg_result, puat_res)), + )) => { + cache_puat(transaction.clone(), puat_res); // We don't care if we fail here. Worst-case: User has to enter PIN more often. + let prompt = BrowserPromptType::CredentialManagementUpdate { result: cfg_result }; + send_about_prompt(&prompt)?; + continue; + } + Ok(StatusUpdate::InteractiveManagement(InteractiveUpdate::BioEnrollmentUpdate(( + bio_res, + puat_res, + )))) => { + cache_puat(transaction.clone(), puat_res); // We don't care if we fail here. Worst-case: User has to enter PIN more often. + let prompt = BrowserPromptType::BioEnrollmentUpdate { result: bio_res }; + send_about_prompt(&prompt)?; + continue; + } + Ok(StatusUpdate::SelectDeviceNotice) => { + info!("STATUS: Please select a device by touching one of them."); + let prompt = BrowserPromptType::SelectDevice; + send_about_prompt(&prompt)?; + } + Ok(StatusUpdate::PinUvError(e)) => { + let mut guard = transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + warn!("STATUS: received status update after end of transaction."); + break; + }; + let autherr = match e { + StatusPinUv::PinRequired(pin_sender) => { + transaction.pin_receiver.replace((0, pin_sender)); + send_about_prompt(&BrowserPromptType::PinRequired)?; + continue; + } + StatusPinUv::InvalidPin(pin_sender, r) => { + transaction.pin_receiver.replace((0, pin_sender)); + send_about_prompt(&BrowserPromptType::PinInvalid { retries: r })?; + continue; + } + StatusPinUv::PinIsTooShort => { + AuthenticatorError::PinError(PinError::PinIsTooShort) + } + StatusPinUv::PinIsTooLong(s) => { + AuthenticatorError::PinError(PinError::PinIsTooLong(s)) + } + StatusPinUv::InvalidUv(r) => { + send_about_prompt(&BrowserPromptType::UvInvalid { retries: r })?; + continue; + } + StatusPinUv::PinAuthBlocked => { + AuthenticatorError::PinError(PinError::PinAuthBlocked) + } + StatusPinUv::PinBlocked => AuthenticatorError::PinError(PinError::PinBlocked), + StatusPinUv::PinNotSet => AuthenticatorError::PinError(PinError::PinNotSet), + StatusPinUv::UvBlocked => AuthenticatorError::PinError(PinError::UvBlocked), + }; + // We will cause auth-rs to return an error, once we leave this block + // due to us 'hanging up'. Before we do that, we will safe the actual + // error that caused this, so our callback-function can return the true + // error to JS, instead of "cancelled by user". + let guard = upcoming_error.lock(); + if let Ok(mut entry) = guard { + entry.replace(autherr); + } else { + return Err(NS_ERROR_DOM_INVALID_STATE_ERR); + } + warn!("STATUS: Pin Error {:?}", e); + break; + } + + Ok(_) => { + // currently not handled + continue; + } + Err(RecvError) => { + info!("STATUS: end"); + break; + } + } + } + Ok(()) +} diff --git a/dom/webauthn/authrs_bridge/src/lib.rs b/dom/webauthn/authrs_bridge/src/lib.rs new file mode 100644 index 0000000000..353e1e89a4 --- /dev/null +++ b/dom/webauthn/authrs_bridge/src/lib.rs @@ -0,0 +1,1534 @@ +/* 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/. */ + +#[macro_use] +extern crate log; + +#[macro_use] +extern crate xpcom; + +use authenticator::{ + authenticatorservice::{RegisterArgs, SignArgs}, + ctap2::attestation::AttestationObject, + ctap2::commands::{get_info::AuthenticatorVersion, PinUvAuthResult}, + ctap2::server::{ + AuthenticationExtensionsClientInputs, AuthenticatorAttachment, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, + UserVerificationRequirement, + }, + errors::AuthenticatorError, + statecallback::StateCallback, + AuthenticatorInfo, BioEnrollmentResult, CredentialManagementResult, InteractiveRequest, + ManageResult, Pin, RegisterResult, SignResult, StateMachine, StatusPinUv, StatusUpdate, +}; +use base64::Engine; +use cstr::cstr; +use moz_task::{get_main_thread, RunnableBuilder}; +use nserror::{ + nsresult, NS_ERROR_DOM_ABORT_ERR, NS_ERROR_DOM_INVALID_STATE_ERR, NS_ERROR_DOM_NOT_ALLOWED_ERR, + NS_ERROR_DOM_OPERATION_ERR, NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_ERROR_NOT_AVAILABLE, + NS_ERROR_NOT_IMPLEMENTED, NS_ERROR_NULL_POINTER, NS_OK, +}; +use nsstring::{nsACString, nsAString, nsCString, nsString}; +use serde::Serialize; +use serde_cbor; +use serde_json::json; +use std::fmt::Write; +use std::sync::mpsc::{channel, Receiver, RecvError, Sender}; +use std::sync::{Arc, Mutex, MutexGuard}; +use thin_vec::{thin_vec, ThinVec}; +use xpcom::interfaces::{ + nsICredentialParameters, nsIObserverService, nsIWebAuthnAttObj, nsIWebAuthnAutoFillEntry, + nsIWebAuthnRegisterArgs, nsIWebAuthnRegisterPromise, nsIWebAuthnRegisterResult, + nsIWebAuthnService, nsIWebAuthnSignArgs, nsIWebAuthnSignPromise, nsIWebAuthnSignResult, +}; +use xpcom::{xpcom_method, RefPtr}; +mod about_webauthn_controller; +use about_webauthn_controller::*; +mod test_token; +use test_token::TestTokenManager; + +fn authrs_to_nserror(e: AuthenticatorError) -> nsresult { + match e { + AuthenticatorError::CredentialExcluded => NS_ERROR_DOM_INVALID_STATE_ERR, + _ => NS_ERROR_DOM_NOT_ALLOWED_ERR, + } +} + +fn should_cancel_prompts<T>(result: &Result<T, AuthenticatorError>) -> bool { + match result { + Err(AuthenticatorError::CredentialExcluded) | Err(AuthenticatorError::PinError(_)) => false, + _ => true, + } +} + +// Using serde(tag="type") makes it so that, for example, BrowserPromptType::Cancel is serialized +// as '{ type: "cancel" }', and BrowserPromptType::PinInvalid { retries: 5 } is serialized as +// '{type: "pin-invalid", retries: 5}'. +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +enum BrowserPromptType<'a> { + AlreadyRegistered, + Cancel, + DeviceBlocked, + PinAuthBlocked, + PinNotSet, + Presence, + SelectDevice, + UvBlocked, + PinRequired, + SelectedDevice { + auth_info: Option<AuthenticatorInfo>, + }, + PinInvalid { + retries: Option<u8>, + }, + PinIsTooLong, + PinIsTooShort, + RegisterDirect, + UvInvalid { + retries: Option<u8>, + }, + SelectSignResult { + entities: &'a [PublicKeyCredentialUserEntity], + }, + ListenSuccess, + ListenError { + error: Box<BrowserPromptType<'a>>, + }, + CredentialManagementUpdate { + result: CredentialManagementResult, + }, + BioEnrollmentUpdate { + result: BioEnrollmentResult, + }, + UnknownError, +} + +#[derive(Debug)] +enum PromptTarget { + Browser, + AboutPage, +} + +#[derive(Serialize)] +struct BrowserPromptMessage<'a> { + prompt: BrowserPromptType<'a>, + tid: u64, + origin: Option<&'a str>, + #[serde(rename = "browsingContextId")] + browsing_context_id: Option<u64>, +} + +fn notify_observers(prompt_target: PromptTarget, json: nsString) -> Result<(), nsresult> { + let main_thread = get_main_thread()?; + let target = match prompt_target { + PromptTarget::Browser => cstr!("webauthn-prompt"), + PromptTarget::AboutPage => cstr!("about-webauthn-prompt"), + }; + + RunnableBuilder::new("AuthrsService::send_prompt", move || { + if let Ok(obs_svc) = xpcom::components::Observer::service::<nsIObserverService>() { + unsafe { + obs_svc.NotifyObservers(std::ptr::null(), target.as_ptr(), json.as_ptr()); + } + } + }) + .dispatch(main_thread.coerce()) +} + +fn send_prompt( + prompt: BrowserPromptType, + tid: u64, + origin: Option<&str>, + browsing_context_id: Option<u64>, +) -> Result<(), nsresult> { + let mut json = nsString::new(); + write!( + json, + "{}", + json!(&BrowserPromptMessage { + prompt, + tid, + origin, + browsing_context_id + }) + ) + .or(Err(NS_ERROR_FAILURE))?; + notify_observers(PromptTarget::Browser, json) +} + +fn cancel_prompts(tid: u64) -> Result<(), nsresult> { + send_prompt(BrowserPromptType::Cancel, tid, None, None)?; + Ok(()) +} + +#[xpcom(implement(nsIWebAuthnRegisterResult), atomic)] +pub struct WebAuthnRegisterResult { + result: RegisterResult, +} + +impl WebAuthnRegisterResult { + xpcom_method!(get_client_data_json => GetClientDataJSON() -> nsACString); + fn get_client_data_json(&self) -> Result<nsCString, nsresult> { + Err(NS_ERROR_NOT_AVAILABLE) + } + + xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>); + fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> { + let mut out = ThinVec::new(); + serde_cbor::to_writer(&mut out, &self.result.att_obj).or(Err(NS_ERROR_FAILURE))?; + Ok(out) + } + + xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>); + fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> { + let Some(credential_data) = &self.result.att_obj.auth_data.credential_data else { + return Err(NS_ERROR_FAILURE); + }; + Ok(credential_data.credential_id.as_slice().into()) + } + + xpcom_method!(get_transports => GetTransports() -> ThinVec<nsString>); + fn get_transports(&self) -> Result<ThinVec<nsString>, nsresult> { + // The list that we return here might be included in a future GetAssertion request as a + // hint as to which transports to try. In production, we only support the "usb" transport. + // In tests, the result is not very important, but we can at least return "internal" if + // we're simulating platform attachment. + if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") + && self.result.attachment == AuthenticatorAttachment::Platform + { + Ok(thin_vec![nsString::from("internal")]) + } else { + Ok(thin_vec![nsString::from("usb")]) + } + } + + xpcom_method!(get_hmac_create_secret => GetHmacCreateSecret() -> bool); + fn get_hmac_create_secret(&self) -> Result<bool, nsresult> { + let Some(hmac_create_secret) = self.result.extensions.hmac_create_secret else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + Ok(hmac_create_secret) + } + + xpcom_method!(get_cred_props_rk => GetCredPropsRk() -> bool); + fn get_cred_props_rk(&self) -> Result<bool, nsresult> { + let Some(cred_props) = &self.result.extensions.cred_props else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + Ok(cred_props.rk) + } + + xpcom_method!(set_cred_props_rk => SetCredPropsRk(aCredPropsRk: bool)); + fn set_cred_props_rk(&self, _cred_props_rk: bool) -> Result<(), nsresult> { + Err(NS_ERROR_NOT_IMPLEMENTED) + } + + xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString); + fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> { + match self.result.attachment { + AuthenticatorAttachment::CrossPlatform => Ok(nsString::from("cross-platform")), + AuthenticatorAttachment::Platform => Ok(nsString::from("platform")), + AuthenticatorAttachment::Unknown => Err(NS_ERROR_NOT_AVAILABLE), + } + } +} + +#[xpcom(implement(nsIWebAuthnAttObj), atomic)] +pub struct WebAuthnAttObj { + att_obj: AttestationObject, +} + +impl WebAuthnAttObj { + xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>); + fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> { + let mut out = ThinVec::new(); + serde_cbor::to_writer(&mut out, &self.att_obj).or(Err(NS_ERROR_FAILURE))?; + Ok(out) + } + + xpcom_method!(get_authenticator_data => GetAuthenticatorData() -> ThinVec<u8>); + fn get_authenticator_data(&self) -> Result<ThinVec<u8>, nsresult> { + // TODO(https://github.com/mozilla/authenticator-rs/issues/302) use to_writer + Ok(self.att_obj.auth_data.to_vec().into()) + } + + xpcom_method!(get_public_key => GetPublicKey() -> ThinVec<u8>); + fn get_public_key(&self) -> Result<ThinVec<u8>, nsresult> { + let Some(credential_data) = &self.att_obj.auth_data.credential_data else { + return Err(NS_ERROR_FAILURE); + }; + Ok(credential_data + .credential_public_key + .der_spki() + .or(Err(NS_ERROR_NOT_AVAILABLE))? + .into()) + } + + xpcom_method!(get_public_key_algorithm => GetPublicKeyAlgorithm() -> i32); + fn get_public_key_algorithm(&self) -> Result<i32, nsresult> { + let Some(credential_data) = &self.att_obj.auth_data.credential_data else { + return Err(NS_ERROR_FAILURE); + }; + // safe to cast to i32 by inspection of defined values + Ok(credential_data.credential_public_key.alg as i32) + } +} + +#[xpcom(implement(nsIWebAuthnSignResult), atomic)] +pub struct WebAuthnSignResult { + result: SignResult, +} + +impl WebAuthnSignResult { + xpcom_method!(get_client_data_json => GetClientDataJSON() -> nsACString); + fn get_client_data_json(&self) -> Result<nsCString, nsresult> { + Err(NS_ERROR_NOT_AVAILABLE) + } + + xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>); + fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> { + let Some(cred) = &self.result.assertion.credentials else { + return Err(NS_ERROR_FAILURE); + }; + Ok(cred.id.as_slice().into()) + } + + xpcom_method!(get_signature => GetSignature() -> ThinVec<u8>); + fn get_signature(&self) -> Result<ThinVec<u8>, nsresult> { + Ok(self.result.assertion.signature.as_slice().into()) + } + + xpcom_method!(get_authenticator_data => GetAuthenticatorData() -> ThinVec<u8>); + fn get_authenticator_data(&self) -> Result<ThinVec<u8>, nsresult> { + Ok(self.result.assertion.auth_data.to_vec().into()) + } + + xpcom_method!(get_user_handle => GetUserHandle() -> ThinVec<u8>); + fn get_user_handle(&self) -> Result<ThinVec<u8>, nsresult> { + let Some(user) = &self.result.assertion.user else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + Ok(user.id.as_slice().into()) + } + + xpcom_method!(get_user_name => GetUserName() -> nsACString); + fn get_user_name(&self) -> Result<nsCString, nsresult> { + let Some(user) = &self.result.assertion.user else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + let Some(name) = &user.name else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + Ok(nsCString::from(name)) + } + + xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString); + fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> { + match self.result.attachment { + AuthenticatorAttachment::CrossPlatform => Ok(nsString::from("cross-platform")), + AuthenticatorAttachment::Platform => Ok(nsString::from("platform")), + AuthenticatorAttachment::Unknown => Err(NS_ERROR_NOT_AVAILABLE), + } + } + + xpcom_method!(get_used_app_id => GetUsedAppId() -> bool); + fn get_used_app_id(&self) -> Result<bool, nsresult> { + self.result.extensions.app_id.ok_or(NS_ERROR_NOT_AVAILABLE) + } + + xpcom_method!(set_used_app_id => SetUsedAppId(aUsedAppId: bool)); + fn set_used_app_id(&self, _used_app_id: bool) -> Result<(), nsresult> { + Err(NS_ERROR_NOT_IMPLEMENTED) + } +} + +// A transaction may create a channel to ask a user for additional input, e.g. a PIN. The Sender +// component of this channel is sent to an AuthrsServide in a StatusUpdate. AuthrsService +// caches the sender along with the expected (u64) transaction ID, which is used as a consistency +// check in callbacks. +type PinReceiver = Option<(u64, Sender<Pin>)>; +type SelectionReceiver = Option<(u64, Sender<Option<usize>>)>; + +fn status_callback( + status_rx: Receiver<StatusUpdate>, + tid: u64, + origin: &String, + browsing_context_id: u64, + transaction: Arc<Mutex<Option<TransactionState>>>, /* Shared with an AuthrsService */ +) -> Result<(), nsresult> { + let origin = Some(origin.as_str()); + let browsing_context_id = Some(browsing_context_id); + loop { + match status_rx.recv() { + Ok(StatusUpdate::SelectDeviceNotice) => { + debug!("STATUS: Please select a device by touching one of them."); + send_prompt( + BrowserPromptType::SelectDevice, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PresenceRequired) => { + debug!("STATUS: Waiting for user presence"); + send_prompt( + BrowserPromptType::Presence, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { + let mut guard = transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + warn!("STATUS: received status update after end of transaction."); + break; + }; + transaction.pin_receiver.replace((tid, sender)); + send_prompt( + BrowserPromptType::PinRequired, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, retries))) => { + let mut guard = transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + warn!("STATUS: received status update after end of transaction."); + break; + }; + transaction.pin_receiver.replace((tid, sender)); + send_prompt( + BrowserPromptType::PinInvalid { retries }, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => { + send_prompt( + BrowserPromptType::PinAuthBlocked, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => { + send_prompt( + BrowserPromptType::DeviceBlocked, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinNotSet)) => { + send_prompt( + BrowserPromptType::PinNotSet, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(retries))) => { + send_prompt( + BrowserPromptType::UvInvalid { retries }, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => { + send_prompt( + BrowserPromptType::UvBlocked, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinIsTooShort)) + | Ok(StatusUpdate::PinUvError(StatusPinUv::PinIsTooLong(..))) => { + // These should never happen. + warn!("STATUS: Got unexpected StatusPinUv-error."); + } + Ok(StatusUpdate::InteractiveManagement(_)) => { + debug!("STATUS: interactive management"); + } + Ok(StatusUpdate::SelectResultNotice(sender, entities)) => { + debug!("STATUS: select result notice"); + let mut guard = transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + warn!("STATUS: received status update after end of transaction."); + break; + }; + transaction.selection_receiver.replace((tid, sender)); + send_prompt( + BrowserPromptType::SelectSignResult { + entities: &entities, + }, + tid, + origin, + browsing_context_id, + )?; + } + Err(RecvError) => { + debug!("STATUS: end"); + break; + } + } + } + Ok(()) +} + +#[derive(Clone)] +struct RegisterPromise(RefPtr<nsIWebAuthnRegisterPromise>); + +impl RegisterPromise { + fn resolve_or_reject(&self, result: Result<RegisterResult, nsresult>) -> Result<(), nsresult> { + match result { + Ok(result) => { + let wrapped_result = + WebAuthnRegisterResult::allocate(InitWebAuthnRegisterResult { result }) + .query_interface::<nsIWebAuthnRegisterResult>() + .ok_or(NS_ERROR_FAILURE)?; + unsafe { self.0.Resolve(wrapped_result.coerce()) }; + } + Err(result) => { + unsafe { self.0.Reject(result) }; + } + } + Ok(()) + } +} + +#[derive(Clone)] +struct SignPromise(RefPtr<nsIWebAuthnSignPromise>); + +impl SignPromise { + fn resolve_or_reject(&self, result: Result<SignResult, nsresult>) -> Result<(), nsresult> { + match result { + Ok(result) => { + let wrapped_result = + WebAuthnSignResult::allocate(InitWebAuthnSignResult { result }) + .query_interface::<nsIWebAuthnSignResult>() + .ok_or(NS_ERROR_FAILURE)?; + unsafe { self.0.Resolve(wrapped_result.coerce()) }; + } + Err(result) => { + unsafe { self.0.Reject(result) }; + } + } + Ok(()) + } +} + +#[derive(Clone)] +enum TransactionPromise { + Listen, + Register(RegisterPromise), + Sign(SignPromise), +} + +impl TransactionPromise { + fn reject(&self, err: nsresult) -> Result<(), nsresult> { + match self { + TransactionPromise::Listen => Ok(()), + TransactionPromise::Register(promise) => promise.resolve_or_reject(Err(err)), + TransactionPromise::Sign(promise) => promise.resolve_or_reject(Err(err)), + } + } +} + +enum TransactionArgs { + Register(/* timeout */ u64, RegisterArgs), + Sign(/* timeout */ u64, SignArgs), +} + +struct TransactionState { + tid: u64, + browsing_context_id: u64, + pending_args: Option<TransactionArgs>, + promise: TransactionPromise, + pin_receiver: PinReceiver, + selection_receiver: SelectionReceiver, + interactive_receiver: InteractiveManagementReceiver, + puat_cache: Option<PinUvAuthResult>, // Cached credential to avoid repeated PIN-entries +} + +// AuthrsService provides an nsIWebAuthnService built on top of authenticator-rs. +#[xpcom(implement(nsIWebAuthnService), atomic)] +pub struct AuthrsService { + usb_token_manager: Mutex<StateMachine>, + test_token_manager: TestTokenManager, + transaction: Arc<Mutex<Option<TransactionState>>>, +} + +impl AuthrsService { + xpcom_method!(pin_callback => PinCallback(aTransactionId: u64, aPin: *const nsACString)); + fn pin_callback(&self, transaction_id: u64, pin: &nsACString) -> Result<(), nsresult> { + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + let mut guard = self.transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + // No ongoing transaction + return Err(NS_ERROR_FAILURE); + }; + let Some((tid, channel)) = transaction.pin_receiver.take() else { + // We weren't expecting a pin. + return Err(NS_ERROR_FAILURE); + }; + if tid != transaction_id { + // The browser is confused about which transaction is active. + // This shouldn't happen + return Err(NS_ERROR_FAILURE); + } + channel + .send(Pin::new(&pin.to_string())) + .or(Err(NS_ERROR_FAILURE)) + } else { + // Silently accept request, if all webauthn-options are disabled. + // Used for testing. + Ok(()) + } + } + + xpcom_method!(selection_callback => SelectionCallback(aTransactionId: u64, aSelection: u64)); + fn selection_callback(&self, transaction_id: u64, selection: u64) -> Result<(), nsresult> { + let mut guard = self.transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + // No ongoing transaction + return Err(NS_ERROR_FAILURE); + }; + let Some((tid, channel)) = transaction.selection_receiver.take() else { + // We weren't expecting a selection. + return Err(NS_ERROR_FAILURE); + }; + if tid != transaction_id { + // The browser is confused about which transaction is active. + // This shouldn't happen + return Err(NS_ERROR_FAILURE); + } + channel + .send(Some(selection as usize)) + .or(Err(NS_ERROR_FAILURE)) + } + + xpcom_method!(get_is_uvpaa => GetIsUVPAA() -> bool); + fn get_is_uvpaa(&self) -> Result<bool, nsresult> { + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + Ok(false) + } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + Ok(self.test_token_manager.has_platform_authenticator()) + } else { + Err(NS_ERROR_NOT_AVAILABLE) + } + } + + xpcom_method!(make_credential => MakeCredential(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsIWebAuthnRegisterArgs, aPromise: *const nsIWebAuthnRegisterPromise)); + fn make_credential( + &self, + tid: u64, + browsing_context_id: u64, + args: &nsIWebAuthnRegisterArgs, + promise: &nsIWebAuthnRegisterPromise, + ) -> Result<(), nsresult> { + self.reset()?; + + let promise = RegisterPromise(RefPtr::new(promise)); + + let mut origin = nsString::new(); + unsafe { args.GetOrigin(&mut *origin) }.to_result()?; + + let mut relying_party_id = nsString::new(); + unsafe { args.GetRpId(&mut *relying_party_id) }.to_result()?; + + let mut client_data_hash = ThinVec::new(); + unsafe { args.GetClientDataHash(&mut client_data_hash) }.to_result()?; + let mut client_data_hash_arr = [0u8; 32]; + client_data_hash_arr.copy_from_slice(&client_data_hash); + + let mut timeout_ms = 0u32; + unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?; + + let mut exclude_list = ThinVec::new(); + unsafe { args.GetExcludeList(&mut exclude_list) }.to_result()?; + let exclude_list = exclude_list + .iter_mut() + .map(|id| PublicKeyCredentialDescriptor { + id: id.to_vec(), + transports: vec![], + }) + .collect(); + + let mut relying_party_name = nsString::new(); + unsafe { args.GetRpName(&mut *relying_party_name) }.to_result()?; + + let mut user_id = ThinVec::new(); + unsafe { args.GetUserId(&mut user_id) }.to_result()?; + + let mut user_name = nsString::new(); + unsafe { args.GetUserName(&mut *user_name) }.to_result()?; + + let mut user_display_name = nsString::new(); + unsafe { args.GetUserDisplayName(&mut *user_display_name) }.to_result()?; + + let mut cose_algs = ThinVec::new(); + unsafe { args.GetCoseAlgs(&mut cose_algs) }.to_result()?; + let pub_cred_params = cose_algs + .iter() + .filter_map(|alg| PublicKeyCredentialParameters::try_from(*alg).ok()) + .collect(); + + let mut resident_key = nsString::new(); + unsafe { args.GetResidentKey(&mut *resident_key) }.to_result()?; + let resident_key_req = if resident_key.eq("required") { + ResidentKeyRequirement::Required + } else if resident_key.eq("preferred") { + ResidentKeyRequirement::Preferred + } else if resident_key.eq("discouraged") { + ResidentKeyRequirement::Discouraged + } else { + return Err(NS_ERROR_FAILURE); + }; + + let mut user_verification = nsString::new(); + unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?; + let user_verification_req = if user_verification.eq("required") { + UserVerificationRequirement::Required + } else if user_verification.eq("discouraged") { + UserVerificationRequirement::Discouraged + } else { + UserVerificationRequirement::Preferred + }; + + let mut authenticator_attachment = nsString::new(); + if unsafe { args.GetAuthenticatorAttachment(&mut *authenticator_attachment) } + .to_result() + .is_ok() + { + if authenticator_attachment.eq("platform") { + return Err(NS_ERROR_FAILURE); + } + } + + let mut attestation_conveyance_preference = nsString::new(); + unsafe { args.GetAttestationConveyancePreference(&mut *attestation_conveyance_preference) } + .to_result()?; + let none_attestation = !(attestation_conveyance_preference.eq("indirect") + || attestation_conveyance_preference.eq("direct") + || attestation_conveyance_preference.eq("enterprise")); + + let mut cred_props = false; + unsafe { args.GetCredProps(&mut cred_props) }.to_result()?; + + let mut min_pin_length = false; + unsafe { args.GetMinPinLength(&mut min_pin_length) }.to_result()?; + + // TODO(Bug 1593571) - Add this to the extensions + // let mut hmac_create_secret = None; + // let mut maybe_hmac_create_secret = false; + // match unsafe { args.GetHmacCreateSecret(&mut maybe_hmac_create_secret) }.to_result() { + // Ok(_) => hmac_create_secret = Some(maybe_hmac_create_secret), + // _ => (), + // } + + let origin = origin.to_string(); + let info = RegisterArgs { + client_data_hash: client_data_hash_arr, + relying_party: RelyingParty { + id: relying_party_id.to_string(), + name: None, + }, + origin: origin.clone(), + user: PublicKeyCredentialUserEntity { + id: user_id.to_vec(), + name: Some(user_name.to_string()), + display_name: None, + }, + pub_cred_params, + exclude_list, + user_verification_req, + resident_key_req, + extensions: AuthenticationExtensionsClientInputs { + cred_props: cred_props.then_some(true), + min_pin_length: min_pin_length.then_some(true), + ..Default::default() + }, + pin: None, + use_ctap1_fallback: !static_prefs::pref!("security.webauthn.ctap2"), + }; + + *self.transaction.lock().unwrap() = Some(TransactionState { + tid, + browsing_context_id, + pending_args: Some(TransactionArgs::Register(timeout_ms as u64, info)), + promise: TransactionPromise::Register(promise), + pin_receiver: None, + selection_receiver: None, + interactive_receiver: None, + puat_cache: None, + }); + + if none_attestation + || static_prefs::pref!("security.webauth.webauthn_testing_allow_direct_attestation") + { + self.resume_make_credential(tid, none_attestation) + } else { + send_prompt( + BrowserPromptType::RegisterDirect, + tid, + Some(&origin), + Some(browsing_context_id), + ) + } + } + + xpcom_method!(resume_make_credential => ResumeMakeCredential(aTid: u64, aForceNoneAttestation: bool)); + fn resume_make_credential( + &self, + tid: u64, + force_none_attestation: bool, + ) -> Result<(), nsresult> { + let mut guard = self.transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return Err(NS_ERROR_FAILURE); + }; + if state.tid != tid { + return Err(NS_ERROR_FAILURE); + }; + let browsing_context_id = state.browsing_context_id; + let Some(TransactionArgs::Register(timeout_ms, info)) = state.pending_args.take() else { + return Err(NS_ERROR_FAILURE); + }; + // We have to drop the guard here, as there _may_ still be another operation + // ongoing and `register()` below will try to cancel it. This will call the state + // callback of that operation, which in turn may try to access `transaction`, deadlocking. + drop(guard); + + let (status_tx, status_rx) = channel::<StatusUpdate>(); + let status_transaction = self.transaction.clone(); + let status_origin = info.origin.clone(); + RunnableBuilder::new("AuthrsService::MakeCredential::StatusReceiver", move || { + let _ = status_callback( + status_rx, + tid, + &status_origin, + browsing_context_id, + status_transaction, + ); + }) + .may_block(true) + .dispatch_background_task()?; + + let callback_transaction = self.transaction.clone(); + let callback_origin = info.origin.clone(); + let state_callback = StateCallback::<Result<RegisterResult, AuthenticatorError>>::new( + Box::new(move |mut result| { + let mut guard = callback_transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return; + }; + if state.tid != tid { + return; + } + let TransactionPromise::Register(ref promise) = state.promise else { + return; + }; + if let Ok(inner) = result.as_mut() { + // Tokens always provide attestation, but the user may have asked we not + // include the attestation statement in the response. + if force_none_attestation { + inner.att_obj.anonymize(); + } + } + if let Err(AuthenticatorError::CredentialExcluded) = result { + let _ = send_prompt( + BrowserPromptType::AlreadyRegistered, + tid, + Some(&callback_origin), + Some(browsing_context_id), + ); + } + if should_cancel_prompts(&result) { + // Some errors are accompanied by prompts that should persist after the + // operation terminates. + let _ = cancel_prompts(tid); + } + let _ = promise.resolve_or_reject(result.map_err(authrs_to_nserror)); + *guard = None; + }), + ); + + // The authenticator crate provides an `AuthenticatorService` which can dispatch a request + // in parallel to any number of transports. We only support the USB transport in production + // configurations, so we do not need the full generality of `AuthenticatorService` here. + // We disable the USB transport in tests that use virtual devices. + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + // TODO(Bug 1855290) Remove this presence prompt + send_prompt( + BrowserPromptType::Presence, + tid, + Some(&info.origin), + Some(browsing_context_id), + )?; + self.usb_token_manager.lock().unwrap().register( + timeout_ms, + info, + status_tx, + state_callback, + ); + } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + self.test_token_manager + .register(timeout_ms, info, status_tx, state_callback); + } else { + return Err(NS_ERROR_FAILURE); + } + + Ok(()) + } + + xpcom_method!(get_assertion => GetAssertion(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsIWebAuthnSignArgs, aPromise: *const nsIWebAuthnSignPromise)); + fn get_assertion( + &self, + tid: u64, + browsing_context_id: u64, + args: &nsIWebAuthnSignArgs, + promise: &nsIWebAuthnSignPromise, + ) -> Result<(), nsresult> { + self.reset()?; + + let promise = SignPromise(RefPtr::new(promise)); + + let mut origin = nsString::new(); + unsafe { args.GetOrigin(&mut *origin) }.to_result()?; + + let mut relying_party_id = nsString::new(); + unsafe { args.GetRpId(&mut *relying_party_id) }.to_result()?; + + let mut client_data_hash = ThinVec::new(); + unsafe { args.GetClientDataHash(&mut client_data_hash) }.to_result()?; + let mut client_data_hash_arr = [0u8; 32]; + client_data_hash_arr.copy_from_slice(&client_data_hash); + + let mut timeout_ms = 0u32; + unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?; + + let mut allow_list = ThinVec::new(); + unsafe { args.GetAllowList(&mut allow_list) }.to_result()?; + let allow_list = allow_list + .iter() + .map(|id| PublicKeyCredentialDescriptor { + id: id.to_vec(), + transports: vec![], + }) + .collect(); + + let mut user_verification = nsString::new(); + unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?; + let user_verification_req = if user_verification.eq("required") { + UserVerificationRequirement::Required + } else if user_verification.eq("discouraged") { + UserVerificationRequirement::Discouraged + } else { + UserVerificationRequirement::Preferred + }; + + let mut app_id = None; + let mut maybe_app_id = nsString::new(); + match unsafe { args.GetAppId(&mut *maybe_app_id) }.to_result() { + Ok(_) => app_id = Some(maybe_app_id.to_string()), + _ => (), + } + + let mut conditionally_mediated = false; + unsafe { args.GetConditionallyMediated(&mut conditionally_mediated) }.to_result()?; + + let info = SignArgs { + client_data_hash: client_data_hash_arr, + relying_party_id: relying_party_id.to_string(), + origin: origin.to_string(), + allow_list, + user_verification_req, + user_presence_req: true, + extensions: AuthenticationExtensionsClientInputs { + app_id, + ..Default::default() + }, + pin: None, + use_ctap1_fallback: !static_prefs::pref!("security.webauthn.ctap2"), + }; + + let mut guard = self.transaction.lock().unwrap(); + *guard = Some(TransactionState { + tid, + browsing_context_id, + pending_args: Some(TransactionArgs::Sign(timeout_ms as u64, info)), + promise: TransactionPromise::Sign(promise), + pin_receiver: None, + selection_receiver: None, + interactive_receiver: None, + puat_cache: None, + }); + + if !conditionally_mediated { + // Immediately proceed to the modal UI flow. + self.do_get_assertion(None, guard) + } else { + // Cache the request and wait for the conditional UI to request autofill entries, etc. + Ok(()) + } + } + + fn do_get_assertion( + &self, + mut selected_credential_id: Option<Vec<u8>>, + mut guard: MutexGuard<Option<TransactionState>>, + ) -> Result<(), nsresult> { + let Some(state) = guard.as_mut() else { + return Err(NS_ERROR_FAILURE); + }; + let browsing_context_id = state.browsing_context_id; + let tid = state.tid; + let (timeout_ms, mut info) = match state.pending_args.take() { + Some(TransactionArgs::Sign(timeout_ms, info)) => (timeout_ms, info), + _ => return Err(NS_ERROR_FAILURE), + }; + + if let Some(id) = selected_credential_id.take() { + if info.allow_list.is_empty() { + info.allow_list.push(PublicKeyCredentialDescriptor { + id, + transports: vec![], + }); + } else { + // We need to ensure that the selected credential id + // was in the original allow_list. + info.allow_list.retain(|cred| cred.id == id); + if info.allow_list.is_empty() { + return Err(NS_ERROR_FAILURE); + } + } + } + + let (status_tx, status_rx) = channel::<StatusUpdate>(); + let status_transaction = self.transaction.clone(); + let status_origin = info.origin.to_string(); + RunnableBuilder::new("AuthrsService::GetAssertion::StatusReceiver", move || { + let _ = status_callback( + status_rx, + tid, + &status_origin, + browsing_context_id, + status_transaction, + ); + }) + .may_block(true) + .dispatch_background_task()?; + + let uniq_allowed_cred = if info.allow_list.len() == 1 { + info.allow_list.first().cloned() + } else { + None + }; + + let callback_transaction = self.transaction.clone(); + let state_callback = StateCallback::<Result<SignResult, AuthenticatorError>>::new( + Box::new(move |mut result| { + let mut guard = callback_transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return; + }; + if state.tid != tid { + return; + } + let TransactionPromise::Sign(ref promise) = state.promise else { + return; + }; + if uniq_allowed_cred.is_some() { + // In CTAP 2.0, but not CTAP 2.1, the assertion object's credential field + // "May be omitted if the allowList has exactly one credential." If we had + // a unique allowed credential, then copy its descriptor to the output. + if let Ok(inner) = result.as_mut() { + inner.assertion.credentials = uniq_allowed_cred; + } + } + if should_cancel_prompts(&result) { + // Some errors are accompanied by prompts that should persist after the + // operation terminates. + let _ = cancel_prompts(tid); + } + let _ = promise.resolve_or_reject(result.map_err(authrs_to_nserror)); + *guard = None; + }), + ); + + // TODO(Bug 1855290) Remove this presence prompt + send_prompt( + BrowserPromptType::Presence, + tid, + Some(&info.origin), + Some(browsing_context_id), + )?; + + // As in `register`, we are intentionally avoiding `AuthenticatorService` here. + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + self.usb_token_manager.lock().unwrap().sign( + timeout_ms as u64, + info, + status_tx, + state_callback, + ); + } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + self.test_token_manager + .sign(timeout_ms as u64, info, status_tx, state_callback); + } else { + return Err(NS_ERROR_FAILURE); + } + + Ok(()) + } + + xpcom_method!(has_pending_conditional_get => HasPendingConditionalGet(aBrowsingContextId: u64, aOrigin: *const nsAString) -> u64); + fn has_pending_conditional_get( + &self, + browsing_context_id: u64, + origin: &nsAString, + ) -> Result<u64, nsresult> { + let mut guard = self.transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return Ok(0); + }; + let Some(TransactionArgs::Sign(_, info)) = state.pending_args.as_ref() else { + return Ok(0); + }; + if state.browsing_context_id != browsing_context_id { + return Ok(0); + } + if !info.origin.eq(&origin.to_string()) { + return Ok(0); + } + Ok(state.tid) + } + + xpcom_method!(get_autofill_entries => GetAutoFillEntries(aTransactionId: u64) -> ThinVec<Option<RefPtr<nsIWebAuthnAutoFillEntry>>>); + fn get_autofill_entries( + &self, + tid: u64, + ) -> Result<ThinVec<Option<RefPtr<nsIWebAuthnAutoFillEntry>>>, nsresult> { + let mut guard = self.transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + if state.tid != tid { + return Err(NS_ERROR_NOT_AVAILABLE); + } + let Some(TransactionArgs::Sign(_, info)) = state.pending_args.as_ref() else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + // We don't currently support silent discovery for credentials on USB tokens. + return Ok(thin_vec![]); + } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + return self + .test_token_manager + .get_autofill_entries(&info.relying_party_id, &info.allow_list); + } else { + return Err(NS_ERROR_FAILURE); + } + } + + xpcom_method!(select_autofill_entry => SelectAutoFillEntry(aTid: u64, aCredentialId: *const ThinVec<u8>)); + fn select_autofill_entry(&self, tid: u64, credential_id: &ThinVec<u8>) -> Result<(), nsresult> { + let mut guard = self.transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return Err(NS_ERROR_FAILURE); + }; + if tid != state.tid { + return Err(NS_ERROR_FAILURE); + } + self.do_get_assertion(Some(credential_id.to_vec()), guard) + } + + xpcom_method!(resume_conditional_get => ResumeConditionalGet(aTid: u64)); + fn resume_conditional_get(&self, tid: u64) -> Result<(), nsresult> { + let mut guard = self.transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return Err(NS_ERROR_FAILURE); + }; + if tid != state.tid { + return Err(NS_ERROR_FAILURE); + } + self.do_get_assertion(None, guard) + } + + // Clears the transaction state if tid matches the ongoing transaction ID. + // Returns whether the tid was a match. + fn clear_transaction(&self, tid: u64) -> bool { + let mut guard = self.transaction.lock().unwrap(); + let Some(state) = guard.as_ref() else { + return true; // workaround for Bug 1864526. + }; + if state.tid != tid { + // Ignore the cancellation request if the transaction + // ID does not match. + return false; + } + // It's possible that we haven't dispatched the request to the usb_token_manager yet, + // e.g. if we're waiting for resume_make_credential. So reject the promise and drop the + // state here rather than from the StateCallback + let _ = state.promise.reject(NS_ERROR_DOM_NOT_ALLOWED_ERR); + *guard = None; + true + } + + xpcom_method!(cancel => Cancel(aTransactionId: u64)); + fn cancel(&self, tid: u64) -> Result<(), nsresult> { + if self.clear_transaction(tid) { + self.usb_token_manager.lock().unwrap().cancel(); + } + Ok(()) + } + + xpcom_method!(reset => Reset()); + fn reset(&self) -> Result<(), nsresult> { + { + if let Some(state) = self.transaction.lock().unwrap().take() { + cancel_prompts(state.tid)?; + state.promise.reject(NS_ERROR_DOM_ABORT_ERR)?; + } + } // release the transaction lock so a StateCallback can take it + self.usb_token_manager.lock().unwrap().cancel(); + Ok(()) + } + + xpcom_method!( + add_virtual_authenticator => AddVirtualAuthenticator( + protocol: *const nsACString, + transport: *const nsACString, + hasResidentKey: bool, + hasUserVerification: bool, + isUserConsenting: bool, + isUserVerified: bool) -> u64 + ); + fn add_virtual_authenticator( + &self, + protocol: &nsACString, + transport: &nsACString, + has_resident_key: bool, + has_user_verification: bool, + is_user_consenting: bool, + is_user_verified: bool, + ) -> Result<u64, nsresult> { + let protocol = match protocol.to_string().as_str() { + "ctap1/u2f" => AuthenticatorVersion::U2F_V2, + "ctap2" => AuthenticatorVersion::FIDO_2_0, + "ctap2_1" => AuthenticatorVersion::FIDO_2_1, + _ => return Err(NS_ERROR_INVALID_ARG), + }; + let transport = transport.to_string(); + match transport.as_str() { + "usb" | "nfc" | "ble" | "smart-card" | "hybrid" | "internal" => (), + _ => return Err(NS_ERROR_INVALID_ARG), + }; + self.test_token_manager.add_virtual_authenticator( + protocol, + transport, + has_resident_key, + has_user_verification, + is_user_consenting, + is_user_verified, + ) + } + + xpcom_method!(remove_virtual_authenticator => RemoveVirtualAuthenticator(authenticatorId: u64)); + fn remove_virtual_authenticator(&self, authenticator_id: u64) -> Result<(), nsresult> { + self.test_token_manager + .remove_virtual_authenticator(authenticator_id) + } + + xpcom_method!( + add_credential => AddCredential( + authenticatorId: u64, + credentialId: *const nsACString, + isResidentCredential: bool, + rpId: *const nsACString, + privateKey: *const nsACString, + userHandle: *const nsACString, + signCount: u32) + ); + fn add_credential( + &self, + authenticator_id: u64, + credential_id: &nsACString, + is_resident_credential: bool, + rp_id: &nsACString, + private_key: &nsACString, + user_handle: &nsACString, + sign_count: u32, + ) -> Result<(), nsresult> { + let credential_id = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(credential_id) + .or(Err(NS_ERROR_INVALID_ARG))?; + let private_key = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(private_key) + .or(Err(NS_ERROR_INVALID_ARG))?; + let user_handle = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(user_handle) + .or(Err(NS_ERROR_INVALID_ARG))?; + self.test_token_manager.add_credential( + authenticator_id, + &credential_id, + &private_key, + &user_handle, + sign_count, + rp_id.to_string(), + is_resident_credential, + ) + } + + xpcom_method!(get_credentials => GetCredentials(authenticatorId: u64) -> ThinVec<Option<RefPtr<nsICredentialParameters>>>); + fn get_credentials( + &self, + authenticator_id: u64, + ) -> Result<ThinVec<Option<RefPtr<nsICredentialParameters>>>, nsresult> { + self.test_token_manager.get_credentials(authenticator_id) + } + + xpcom_method!(remove_credential => RemoveCredential(authenticatorId: u64, credentialId: *const nsACString)); + fn remove_credential( + &self, + authenticator_id: u64, + credential_id: &nsACString, + ) -> Result<(), nsresult> { + let credential_id = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(credential_id) + .or(Err(NS_ERROR_INVALID_ARG))?; + self.test_token_manager + .remove_credential(authenticator_id, credential_id.as_ref()) + } + + xpcom_method!(remove_all_credentials => RemoveAllCredentials(authenticatorId: u64)); + fn remove_all_credentials(&self, authenticator_id: u64) -> Result<(), nsresult> { + self.test_token_manager + .remove_all_credentials(authenticator_id) + } + + xpcom_method!(set_user_verified => SetUserVerified(authenticatorId: u64, isUserVerified: bool)); + fn set_user_verified( + &self, + authenticator_id: u64, + is_user_verified: bool, + ) -> Result<(), nsresult> { + self.test_token_manager + .set_user_verified(authenticator_id, is_user_verified) + } + + xpcom_method!(listen => Listen()); + pub(crate) fn listen(&self) -> Result<(), nsresult> { + // For now, we don't support softtokens + if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + return Ok(()); + } + + { + let mut guard = self.transaction.lock().unwrap(); + if guard.as_ref().is_some() { + // ignore listen() and continue with ongoing transaction + return Ok(()); + } + *guard = Some(TransactionState { + tid: 0, + browsing_context_id: 0, + pending_args: None, + promise: TransactionPromise::Listen, + pin_receiver: None, + selection_receiver: None, + interactive_receiver: None, + puat_cache: None, + }); + } + + // We may get from status_updates info about certain errors (e.g. PinErrors) + // which we want to present to the user. We will ignore the following error + // which is caused by us "hanging up" on the StatusUpdate-channel and return + // the PinError instead, via `upcoming_error`. + let upcoming_error = Arc::new(Mutex::new(None)); + let upcoming_error_c = upcoming_error.clone(); + let callback_transaction = self.transaction.clone(); + let state_callback = StateCallback::<Result<ManageResult, AuthenticatorError>>::new( + Box::new(move |result| { + let mut guard = callback_transaction.lock().unwrap(); + match guard.as_mut() { + Some(state) => { + match state.promise { + TransactionPromise::Listen => (), + _ => return, + } + *guard = None; + } + // We have no transaction anymore, this means cancel() was called + None => (), + } + let msg = match result { + Ok(_) => BrowserPromptType::ListenSuccess, + Err(e) => { + // See if we have a cached error that should replace this error + let replacement = if let Ok(mut x) = upcoming_error_c.lock() { + x.take() + } else { + None + }; + let replaced_err = replacement.unwrap_or(e); + let err = authrs_to_prompt(replaced_err); + BrowserPromptType::ListenError { + error: Box::new(err), + } + } + }; + let _ = send_about_prompt(&msg); + }), + ); + + // Calling `manage()` within the lock, to avoid race conditions + // where we might check listen_blocked, see that it's false, + // continue along, but in parallel `make_credential()` aborts the + // interactive process shortly after, setting listen_blocked to true, + // then accessing usb_token_manager afterwards and at the same time + // we do it here, causing a runtime crash for trying to mut-borrow it twice. + let (status_tx, status_rx) = channel::<StatusUpdate>(); + let status_transaction = self.transaction.clone(); + RunnableBuilder::new( + "AuthrsTransport::AboutWebauthn::StatusReceiver", + move || { + let _ = interactive_status_callback(status_rx, status_transaction, upcoming_error); + }, + ) + .may_block(true) + .dispatch_background_task()?; + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + self.usb_token_manager.lock().unwrap().manage( + 60 * 1000 * 1000, + status_tx, + state_callback, + ); + } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + // We don't yet support softtoken + } else { + // Silently accept request, if all webauthn-options are disabled. + // Used for testing. + } + Ok(()) + } + + xpcom_method!(run_command => RunCommand(c_cmd: *const nsACString)); + pub fn run_command(&self, c_cmd: &nsACString) -> Result<(), nsresult> { + // Always test if it can be parsed from incoming JSON (even for tests) + let incoming: RequestWrapper = + serde_json::from_str(&c_cmd.to_utf8()).or(Err(NS_ERROR_DOM_OPERATION_ERR))?; + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + let guard = self.transaction.lock().unwrap(); + let puat = guard.as_ref().and_then(|g| g.puat_cache.clone()); + let command = match incoming { + RequestWrapper::Quit => InteractiveRequest::Quit, + RequestWrapper::ChangePIN(a, b) => InteractiveRequest::ChangePIN(a, b), + RequestWrapper::SetPIN(a) => InteractiveRequest::SetPIN(a), + RequestWrapper::CredentialManagement(c) => { + InteractiveRequest::CredentialManagement(c, puat) + } + RequestWrapper::BioEnrollment(c) => InteractiveRequest::BioEnrollment(c, puat), + }; + match &guard.as_ref().unwrap().interactive_receiver { + Some(channel) => channel.send(command).or(Err(NS_ERROR_FAILURE)), + // Either we weren't expecting a pin, or the controller is confused + // about which transaction is active. Neither is recoverable, so it's + // OK to drop the PinReceiver here. + _ => Err(NS_ERROR_FAILURE), + } + } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + // We don't yet support softtoken + Ok(()) + } else { + // Silently accept request, if all webauthn-options are disabled. + // Used for testing. + Ok(()) + } + } +} + +#[no_mangle] +pub extern "C" fn authrs_service_constructor(result: *mut *const nsIWebAuthnService) -> nsresult { + let wrapper = AuthrsService::allocate(InitAuthrsService { + usb_token_manager: Mutex::new(StateMachine::new()), + test_token_manager: TestTokenManager::new(), + transaction: Arc::new(Mutex::new(None)), + }); + + #[cfg(feature = "fuzzing")] + { + let fuzzing_config = static_prefs::pref!("fuzzing.webauthn.authenticator_config"); + if fuzzing_config != 0 { + let is_user_verified = (fuzzing_config & 0x01) != 0; + let is_user_consenting = (fuzzing_config & 0x02) != 0; + let has_user_verification = (fuzzing_config & 0x04) != 0; + let has_resident_key = (fuzzing_config & 0x08) != 0; + let transport = nsCString::from(match (fuzzing_config & 0x10) >> 4 { + 0 => "usb", + 1 => "internal", + _ => unreachable!(), + }); + let protocol = nsCString::from(match (fuzzing_config & 0x60) >> 5 { + 0 => "", // reserved + 1 => "ctap1/u2f", + 2 => "ctap2", + 3 => "ctap2_1", + _ => unreachable!(), + }); + // If this fails it's probably because the protocol bits were zero, + // we'll just ignore it. + let _ = wrapper.add_virtual_authenticator( + &protocol, + &transport, + has_resident_key, + has_user_verification, + is_user_consenting, + is_user_verified, + ); + } + } + + unsafe { + RefPtr::new(wrapper.coerce::<nsIWebAuthnService>()).forget(&mut *result); + } + NS_OK +} + +#[no_mangle] +pub extern "C" fn authrs_webauthn_att_obj_constructor( + att_obj_bytes: &ThinVec<u8>, + anonymize: bool, + result: *mut *const nsIWebAuthnAttObj, +) -> nsresult { + if result.is_null() { + return NS_ERROR_NULL_POINTER; + } + + let mut att_obj: AttestationObject = match serde_cbor::from_slice(att_obj_bytes) { + Ok(att_obj) => att_obj, + Err(_) => return NS_ERROR_INVALID_ARG, + }; + + if anonymize { + att_obj.anonymize(); + } + + let wrapper = WebAuthnAttObj::allocate(InitWebAuthnAttObj { att_obj }); + + unsafe { + RefPtr::new(wrapper.coerce::<nsIWebAuthnAttObj>()).forget(&mut *result); + } + + NS_OK +} diff --git a/dom/webauthn/authrs_bridge/src/test_token.rs b/dom/webauthn/authrs_bridge/src/test_token.rs new file mode 100644 index 0000000000..afc2ddbc75 --- /dev/null +++ b/dom/webauthn/authrs_bridge/src/test_token.rs @@ -0,0 +1,974 @@ +/* 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 authenticator::authenticatorservice::{RegisterArgs, SignArgs}; +use authenticator::crypto::{ecdsa_p256_sha256_sign_raw, COSEAlgorithm, COSEKey, SharedSecret}; +use authenticator::ctap2::{ + attestation::{ + AAGuid, AttestationObject, AttestationStatement, AttestationStatementPacked, + AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags, Extension, + }, + client_data::ClientDataHash, + commands::{ + client_pin::{ClientPIN, ClientPinResponse, PINSubcommand}, + get_assertion::{GetAssertion, GetAssertionResponse, GetAssertionResult}, + get_info::{AuthenticatorInfo, AuthenticatorOptions, AuthenticatorVersion}, + get_version::{GetVersion, U2FInfo}, + make_credentials::{MakeCredentials, MakeCredentialsResult}, + reset::Reset, + selection::Selection, + RequestCtap1, RequestCtap2, StatusCode, + }, + preflight::CheckKeyHandle, + server::{ + AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, + RelyingParty, + }, +}; +use authenticator::errors::{AuthenticatorError, CommandError, HIDError, U2FTokenError}; +use authenticator::{ctap2, statecallback::StateCallback}; +use authenticator::{FidoDevice, FidoDeviceIO, FidoProtocol, VirtualFidoDevice}; +use authenticator::{RegisterResult, SignResult, StatusUpdate}; +use base64::Engine; +use moz_task::RunnableBuilder; +use nserror::{nsresult, NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_OK}; +use nsstring::{nsACString, nsAString, nsCString, nsString}; +use rand::{thread_rng, RngCore}; +use std::cell::{Ref, RefCell}; +use std::collections::{hash_map::Entry, HashMap}; +use std::ops::{Deref, DerefMut}; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::mpsc::Sender; +use std::sync::{Arc, Mutex}; +use thin_vec::ThinVec; +use xpcom::interfaces::{nsICredentialParameters, nsIWebAuthnAutoFillEntry}; +use xpcom::{xpcom_method, RefPtr}; + +// All TestTokens use this fixed, randomly generated, AAGUID +const VIRTUAL_TOKEN_AAGUID: AAGuid = AAGuid([ + 0x68, 0xe1, 0x00, 0xa5, 0x0b, 0x47, 0x91, 0x04, 0xb8, 0x54, 0x97, 0xa9, 0xba, 0x51, 0x06, 0x38, +]); + +#[derive(Debug)] +struct TestTokenCredential { + id: Vec<u8>, + privkey: Vec<u8>, + user_handle: Vec<u8>, + sign_count: AtomicU32, + is_discoverable_credential: bool, + rp: RelyingParty, +} + +impl TestTokenCredential { + fn assert( + &self, + client_data_hash: &ClientDataHash, + flags: AuthenticatorDataFlags, + ) -> Result<GetAssertionResponse, HIDError> { + let credentials = Some(PublicKeyCredentialDescriptor { + id: self.id.clone(), + transports: vec![], + }); + + let auth_data = AuthenticatorData { + rp_id_hash: self.rp.hash(), + flags, + counter: self.sign_count.fetch_add(1, Ordering::Relaxed), + credential_data: None, + extensions: Extension::default(), + }; + + let user = Some(PublicKeyCredentialUserEntity { + id: self.user_handle.clone(), + ..Default::default() + }); + + let mut data = auth_data.to_vec(); + data.extend_from_slice(client_data_hash.as_ref()); + let signature = + ecdsa_p256_sha256_sign_raw(&self.privkey, &data).or(Err(HIDError::DeviceError))?; + + Ok(GetAssertionResponse { + credentials, + auth_data, + signature, + user, + number_of_credentials: Some(1), + }) + } +} + +#[derive(Debug)] +struct TestToken { + protocol: FidoProtocol, + transport: String, + versions: Vec<AuthenticatorVersion>, + has_resident_key: bool, + has_user_verification: bool, + is_user_consenting: bool, + is_user_verified: bool, + // This is modified in `make_credentials` which takes a &TestToken, but we only allow one transaction at a time. + credentials: RefCell<Vec<TestTokenCredential>>, + pin_token: [u8; 32], + shared_secret: Option<SharedSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl TestToken { + fn new( + versions: Vec<AuthenticatorVersion>, + transport: String, + has_resident_key: bool, + has_user_verification: bool, + is_user_consenting: bool, + is_user_verified: bool, + ) -> TestToken { + let mut pin_token = [0u8; 32]; + thread_rng().fill_bytes(&mut pin_token); + Self { + protocol: FidoProtocol::CTAP2, + transport, + versions, + has_resident_key, + has_user_verification, + is_user_consenting, + is_user_verified, + credentials: RefCell::new(vec![]), + pin_token, + shared_secret: None, + authenticator_info: None, + } + } + + fn insert_credential( + &self, + id: &[u8], + privkey: &[u8], + rp: &RelyingParty, + is_discoverable_credential: bool, + user_handle: &[u8], + sign_count: u32, + ) { + let c = TestTokenCredential { + id: id.to_vec(), + privkey: privkey.to_vec(), + rp: rp.clone(), + is_discoverable_credential, + user_handle: user_handle.to_vec(), + sign_count: AtomicU32::new(sign_count), + }; + + let mut credlist = self.credentials.borrow_mut(); + + match credlist.binary_search_by_key(&id, |probe| &probe.id) { + Ok(_) => {} + Err(idx) => credlist.insert(idx, c), + } + } + + fn get_credentials(&self) -> Ref<Vec<TestTokenCredential>> { + self.credentials.borrow() + } + + fn delete_credential(&mut self, id: &[u8]) -> bool { + let mut credlist = self.credentials.borrow_mut(); + if let Ok(idx) = credlist.binary_search_by_key(&id, |probe| &probe.id) { + credlist.remove(idx); + return true; + } + + false + } + + fn delete_all_credentials(&mut self) { + self.credentials.borrow_mut().clear(); + } + + fn has_credential(&self, id: &[u8]) -> bool { + self.credentials + .borrow() + .binary_search_by_key(&id, |probe| &probe.id) + .is_ok() + } + + fn max_supported_version(&self) -> AuthenticatorVersion { + self.authenticator_info + .as_ref() + .map_or(AuthenticatorVersion::U2F_V2, |info| { + info.max_supported_version() + }) + } +} + +impl FidoDevice for TestToken { + fn pre_init(&mut self) -> Result<(), HIDError> { + Ok(()) + } + + fn should_try_ctap2(&self) -> bool { + true + } + + fn initialized(&self) -> bool { + true + } + + fn is_u2f(&mut self) -> bool { + true + } + + fn get_shared_secret(&self) -> Option<&SharedSecret> { + self.shared_secret.as_ref() + } + + fn set_shared_secret(&mut self, shared_secret: SharedSecret) { + self.shared_secret = Some(shared_secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } + + fn get_protocol(&self) -> FidoProtocol { + self.protocol + } + + fn downgrade_to_ctap1(&mut self) { + self.protocol = FidoProtocol::CTAP1 + } +} + +impl FidoDeviceIO for TestToken { + fn send_msg_cancellable<Out, Req: RequestCtap1<Output = Out> + RequestCtap2<Output = Out>>( + &mut self, + msg: &Req, + keep_alive: &dyn Fn() -> bool, + ) -> Result<Out, HIDError> { + if !self.initialized() { + return Err(HIDError::DeviceNotInitialized); + } + + match self.get_protocol() { + FidoProtocol::CTAP1 => self.send_ctap1_cancellable(msg, keep_alive), + FidoProtocol::CTAP2 => self.send_cbor_cancellable(msg, keep_alive), + } + } + + fn send_cbor_cancellable<Req: RequestCtap2>( + &mut self, + msg: &Req, + _keep_alive: &dyn Fn() -> bool, + ) -> Result<Req::Output, HIDError> { + msg.send_to_virtual_device(self) + } + + fn send_ctap1_cancellable<Req: RequestCtap1>( + &mut self, + msg: &Req, + _keep_alive: &dyn Fn() -> bool, + ) -> Result<Req::Output, HIDError> { + msg.send_to_virtual_device(self) + } +} + +impl VirtualFidoDevice for TestToken { + fn check_key_handle(&self, req: &CheckKeyHandle) -> Result<(), HIDError> { + let credlist = self.credentials.borrow(); + let req_rp_hash = req.rp.hash(); + let eligible_cred_iter = credlist.iter().filter(|x| x.rp.hash() == req_rp_hash); + for credential in eligible_cred_iter { + if req.key_handle == credential.id { + return Ok(()); + } + } + Err(HIDError::DeviceError) + } + + fn client_pin(&self, req: &ClientPIN) -> Result<ClientPinResponse, HIDError> { + match req.subcommand { + PINSubcommand::GetKeyAgreement => { + // We don't need to save, or even know, the private key for the public key returned + // here because we have access to the shared secret derived on the client side. + let (_private, public) = COSEKey::generate(COSEAlgorithm::ECDH_ES_HKDF256) + .map_err(|_| HIDError::DeviceError)?; + Ok(ClientPinResponse { + key_agreement: Some(public), + ..Default::default() + }) + } + PINSubcommand::GetPinUvAuthTokenUsingUvWithPermissions => { + // TODO: permissions + if !self.is_user_consenting || !self.is_user_verified { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::OperationDenied, + None, + ))); + } + let secret = match self.shared_secret.as_ref() { + Some(secret) => secret, + _ => return Err(HIDError::DeviceError), + }; + let encrypted_pin_token = match secret.encrypt(&self.pin_token) { + Ok(token) => token, + _ => return Err(HIDError::DeviceError), + }; + Ok(ClientPinResponse { + pin_token: Some(encrypted_pin_token), + ..Default::default() + }) + } + _ => Err(HIDError::UnsupportedCommand), + } + } + + fn get_assertion(&self, req: &GetAssertion) -> Result<Vec<GetAssertionResult>, HIDError> { + // Algorithm 6.2.2 from CTAP 2.1 + // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-makeCred-authnr-alg + + // 1. zero length pinUvAuthParam + // (not implemented) + + // 2. Validate pinUvAuthParam + // Handled by caller + + // 3. Initialize "uv" and "up" bits to false + let mut flags = AuthenticatorDataFlags::empty(); + + // 4. Handle all options + // 4.1 and 4.2 + let effective_uv_opt = + req.options.user_verification.unwrap_or(false) && req.pin_uv_auth_param.is_none(); + + // 4.3 + if effective_uv_opt && !self.has_user_verification { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::InvalidOption, + None, + ))); + } + + // 4.4 rk + // (not implemented, we don't encode it) + + // 4.5 + let effective_up_opt = req.options.user_presence.unwrap_or(true); + + // 5. alwaysUv + // (not implemented) + + // 6. User verification + // TODO: Permissions, (maybe) validate pinUvAuthParam + if self.is_user_verified && (effective_uv_opt || req.pin_uv_auth_param.is_some()) { + flags |= AuthenticatorDataFlags::USER_VERIFIED; + } + + // 7. Locate credentials + let credlist = self.credentials.borrow(); + let req_rp_hash = req.rp.hash(); + let eligible_cred_iter = credlist.iter().filter(|x| x.rp.hash() == req_rp_hash); + + // 8. Set up=true if evidence of user interaction was provided in step 6. + // (not applicable, we use pinUvAuthParam) + + // 9. User presence test + if effective_up_opt { + if self.is_user_consenting { + flags |= AuthenticatorDataFlags::USER_PRESENT; + } else { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::UpRequired, + None, + ))); + } + } + + // 10. Extensions + // (not implemented) + + let mut assertions: Vec<GetAssertionResult> = vec![]; + if !req.allow_list.is_empty() { + // 11. Non-discoverable credential case + // return at most one assertion matching an allowed credential ID + for credential in eligible_cred_iter { + if req.allow_list.iter().any(|x| x.id == credential.id) { + let mut assertion: GetAssertionResponse = + credential.assert(&req.client_data_hash, flags)?; + if req.allow_list.len() == 1 + && self.max_supported_version() == AuthenticatorVersion::FIDO_2_0 + { + // CTAP 2.0 authenticators are allowed to omit the credential ID in the + // response if the allow list contains exactly one entry. This behavior is + // a common source of bugs, e.g. Bug 1864504, so we'll exercise it here. + assertion.credentials = None; + } + assertions.push(GetAssertionResult { + assertion: assertion.into(), + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }); + break; + } + } + } else { + // 12. Discoverable credential case + // return any number of assertions from credentials bound to this RP ID + for credential in eligible_cred_iter.filter(|x| x.is_discoverable_credential) { + let assertion = credential.assert(&req.client_data_hash, flags)?.into(); + assertions.push(GetAssertionResult { + assertion, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }); + } + } + + if assertions.is_empty() { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + None, + ))); + } + + Ok(assertions) + } + + fn get_info(&self) -> Result<AuthenticatorInfo, HIDError> { + // This is a CTAP2.1 device with internal user verification support + Ok(AuthenticatorInfo { + versions: self.versions.clone(), + options: AuthenticatorOptions { + platform_device: self.transport == "internal", + resident_key: self.has_resident_key, + pin_uv_auth_token: Some(self.has_user_verification), + user_verification: Some(self.has_user_verification), + ..Default::default() + }, + ..Default::default() + }) + } + + fn get_version(&self, _req: &GetVersion) -> Result<U2FInfo, HIDError> { + Err(HIDError::UnsupportedCommand) + } + + fn make_credentials(&self, req: &MakeCredentials) -> Result<MakeCredentialsResult, HIDError> { + // Algorithm 6.1.2 from CTAP 2.1 + // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-makeCred-authnr-alg + + // 1. zero length pinUvAuthParam + // (not implemented) + + // 2. Validate pinUvAuthParam + // Handled by caller + + // 3. Validate pubKeyCredParams + if !req + .pub_cred_params + .iter() + .any(|x| x.alg == COSEAlgorithm::ES256) + { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::UnsupportedAlgorithm, + None, + ))); + } + + // 4. initialize "uv" and "up" bits to false + let mut flags = AuthenticatorDataFlags::empty(); + + // 5. process all options + + // 5.1 and 5.2 + let effective_uv_opt = + req.options.user_verification.unwrap_or(false) && req.pin_uv_auth_param.is_none(); + + // 5.3 + if effective_uv_opt && !self.has_user_verification { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::InvalidOption, + None, + ))); + } + + // 5.4 + if req.options.resident_key.unwrap_or(false) && !self.has_resident_key { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::UnsupportedOption, + None, + ))); + } + + // 5.6 and 5.7 + // Nothing to do. We don't provide a way to set up=false. + + // 6. alwaysUv option ID + // (not implemented) + + // 7. and 8. makeCredUvNotRqd option ID + // (not implemented) + + // 9. enterprise attestation + // (not implemented) + + // 11. User verification + // TODO: Permissions, (maybe) validate pinUvAuthParam + if self.is_user_verified { + flags |= AuthenticatorDataFlags::USER_VERIFIED; + } + + // 12. exclude list + // TODO: credProtect + if req.exclude_list.iter().any(|x| self.has_credential(&x.id)) { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::CredentialExcluded, + None, + ))); + } + + // 13. Set up=true if evidence of user interaction was provided in step 11. + // (not applicable, we use pinUvAuthParam) + + // 14. User presence test + if self.is_user_consenting { + flags |= AuthenticatorDataFlags::USER_PRESENT; + } else { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::UpRequired, + None, + ))); + } + + // 15. process extensions + let mut extensions = Extension::default(); + if req.extensions.min_pin_length == Some(true) { + // a real authenticator would + // 1) return an actual minimum pin length, and + // 2) check the RP ID against an allowlist before providing any data + extensions.min_pin_length = Some(4); + } + + if extensions.has_some() { + flags |= AuthenticatorDataFlags::EXTENSION_DATA; + } + + // 16. Generate a new credential. + let (private, public) = + COSEKey::generate(COSEAlgorithm::ES256).map_err(|_| HIDError::DeviceError)?; + let counter = 0; + + // 17. and 18. Store credential + // + // All of the credentials that we create are "resident"---we store the private key locally, + // and use a random value for the credential ID. The `req.options.resident_key` field + // determines whether we make the credential "discoverable". + let mut id = [0u8; 32]; + thread_rng().fill_bytes(&mut id); + self.insert_credential( + &id, + &private, + &req.rp, + req.options.resident_key.unwrap_or(false), + &req.user.clone().unwrap_or_default().id, + counter, + ); + + // 19. Generate attestation statement + flags |= AuthenticatorDataFlags::ATTESTED; + + let auth_data = AuthenticatorData { + rp_id_hash: req.rp.hash(), + flags, + counter, + credential_data: Some(AttestedCredentialData { + aaguid: VIRTUAL_TOKEN_AAGUID, + credential_id: id.to_vec(), + credential_public_key: public, + }), + extensions, + }; + + let mut data = auth_data.to_vec(); + data.extend_from_slice(req.client_data_hash.as_ref()); + + let sig = ecdsa_p256_sha256_sign_raw(&private, &data).or(Err(HIDError::DeviceError))?; + + let att_stmt = AttestationStatement::Packed(AttestationStatementPacked { + alg: COSEAlgorithm::ES256, + sig: sig.as_slice().into(), + attestation_cert: vec![], + }); + + let result = MakeCredentialsResult { + attachment: AuthenticatorAttachment::Unknown, + att_obj: AttestationObject { + att_stmt, + auth_data, + }, + extensions: Default::default(), + }; + Ok(result) + } + + fn reset(&self, _req: &Reset) -> Result<(), HIDError> { + Err(HIDError::UnsupportedCommand) + } + + fn selection(&self, _req: &Selection) -> Result<(), HIDError> { + Err(HIDError::UnsupportedCommand) + } +} + +#[xpcom(implement(nsICredentialParameters), atomic)] +struct CredentialParameters { + credential_id: Vec<u8>, + is_resident_credential: bool, + rp_id: String, + private_key: Vec<u8>, + user_handle: Vec<u8>, + sign_count: u32, +} + +impl CredentialParameters { + xpcom_method!(get_credential_id => GetCredentialId() -> nsACString); + fn get_credential_id(&self) -> Result<nsCString, nsresult> { + Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD + .encode(&self.credential_id) + .into()) + } + + xpcom_method!(get_is_resident_credential => GetIsResidentCredential() -> bool); + fn get_is_resident_credential(&self) -> Result<bool, nsresult> { + Ok(self.is_resident_credential) + } + + xpcom_method!(get_rp_id => GetRpId() -> nsACString); + fn get_rp_id(&self) -> Result<nsCString, nsresult> { + Ok(nsCString::from(&self.rp_id)) + } + + xpcom_method!(get_private_key => GetPrivateKey() -> nsACString); + fn get_private_key(&self) -> Result<nsCString, nsresult> { + Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD + .encode(&self.private_key) + .into()) + } + + xpcom_method!(get_user_handle => GetUserHandle() -> nsACString); + fn get_user_handle(&self) -> Result<nsCString, nsresult> { + Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD + .encode(&self.user_handle) + .into()) + } + + xpcom_method!(get_sign_count => GetSignCount() -> u32); + fn get_sign_count(&self) -> Result<u32, nsresult> { + Ok(self.sign_count) + } +} + +#[xpcom(implement(nsIWebAuthnAutoFillEntry), atomic)] +struct WebAuthnAutoFillEntry { + rp: String, + credential_id: Vec<u8>, +} + +impl WebAuthnAutoFillEntry { + xpcom_method!(get_provider => GetProvider() -> u8); + fn get_provider(&self) -> Result<u8, nsresult> { + Ok(nsIWebAuthnAutoFillEntry::PROVIDER_TEST_TOKEN) + } + + xpcom_method!(get_user_name => GetUserName() -> nsAString); + fn get_user_name(&self) -> Result<nsString, nsresult> { + Ok(nsString::from("Test User")) + } + + xpcom_method!(get_rp_id => GetRpId() -> nsAString); + fn get_rp_id(&self) -> Result<nsString, nsresult> { + Ok(nsString::from(&self.rp)) + } + + xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>); + fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> { + Ok(self.credential_id.as_slice().into()) + } +} + +#[derive(Default)] +pub(crate) struct TestTokenManager { + state: Arc<Mutex<HashMap<u64, TestToken>>>, +} + +impl TestTokenManager { + pub fn new() -> Self { + Default::default() + } + + pub fn add_virtual_authenticator( + &self, + protocol: AuthenticatorVersion, + transport: String, + has_resident_key: bool, + has_user_verification: bool, + is_user_consenting: bool, + is_user_verified: bool, + ) -> Result<u64, nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let token = TestToken::new( + vec![protocol], + transport, + has_resident_key, + has_user_verification, + is_user_consenting, + is_user_verified, + ); + loop { + let id = rand::random::<u64>() & 0x1f_ffff_ffff_ffffu64; // Make the id safe for JS (53 bits) + match guard.deref_mut().entry(id) { + Entry::Occupied(_) => continue, + Entry::Vacant(v) => { + v.insert(token); + return Ok(id); + } + }; + } + } + + pub fn remove_virtual_authenticator(&self, authenticator_id: u64) -> Result<(), nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + guard + .deref_mut() + .remove(&authenticator_id) + .ok_or(NS_ERROR_INVALID_ARG)?; + Ok(()) + } + + pub fn add_credential( + &self, + authenticator_id: u64, + id: &[u8], + privkey: &[u8], + user_handle: &[u8], + sign_count: u32, + rp_id: String, + is_resident_credential: bool, + ) -> Result<(), nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let token = guard + .deref_mut() + .get_mut(&authenticator_id) + .ok_or(NS_ERROR_INVALID_ARG)?; + let rp = RelyingParty::from(rp_id); + token.insert_credential( + id, + privkey, + &rp, + is_resident_credential, + user_handle, + sign_count, + ); + Ok(()) + } + + pub fn get_credentials( + &self, + authenticator_id: u64, + ) -> Result<ThinVec<Option<RefPtr<nsICredentialParameters>>>, nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let token = guard + .get_mut(&authenticator_id) + .ok_or(NS_ERROR_INVALID_ARG)?; + let credentials = token.get_credentials(); + let mut credentials_parameters = ThinVec::with_capacity(credentials.len()); + for credential in credentials.deref() { + // CTAP1 credentials are not currently supported here. + let credential_parameters = CredentialParameters::allocate(InitCredentialParameters { + credential_id: credential.id.clone(), + is_resident_credential: credential.is_discoverable_credential, + rp_id: credential.rp.id.clone(), + private_key: credential.privkey.clone(), + user_handle: credential.user_handle.clone(), + sign_count: credential.sign_count.load(Ordering::Relaxed), + }) + .query_interface::<nsICredentialParameters>() + .ok_or(NS_ERROR_FAILURE)?; + credentials_parameters.push(Some(credential_parameters)); + } + Ok(credentials_parameters) + } + + pub fn remove_credential(&self, authenticator_id: u64, id: &[u8]) -> Result<(), nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let token = guard + .deref_mut() + .get_mut(&authenticator_id) + .ok_or(NS_ERROR_INVALID_ARG)?; + if token.delete_credential(id) { + Ok(()) + } else { + Err(NS_ERROR_INVALID_ARG) + } + } + + pub fn remove_all_credentials(&self, authenticator_id: u64) -> Result<(), nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let token = guard + .deref_mut() + .get_mut(&authenticator_id) + .ok_or(NS_ERROR_INVALID_ARG)?; + token.delete_all_credentials(); + Ok(()) + } + + pub fn set_user_verified( + &self, + authenticator_id: u64, + is_user_verified: bool, + ) -> Result<(), nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let token = guard + .deref_mut() + .get_mut(&authenticator_id) + .ok_or(NS_ERROR_INVALID_ARG)?; + token.is_user_verified = is_user_verified; + Ok(()) + } + + pub fn register( + &self, + _timeout_ms: u64, + ctap_args: RegisterArgs, + status: Sender<StatusUpdate>, + callback: StateCallback<Result<RegisterResult, AuthenticatorError>>, + ) { + if !static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + return; + } + + let state_obj = self.state.clone(); + + // Registration doesn't currently block, but it might in a future version, so we run it on + // a background thread. + let _ = RunnableBuilder::new("TestTokenManager::register", move || { + // TODO(Bug 1854278) We should actually run one thread per token here + // and attempt to fulfill this request in parallel. + for token in state_obj.lock().unwrap().values_mut() { + let _ = token.init(); + if ctap2::register( + token, + ctap_args.clone(), + status.clone(), + callback.clone(), + &|| true, + ) { + // callback was called + return; + } + } + + // Send an error, if the callback wasn't called already. + callback.call(Err(AuthenticatorError::U2FToken(U2FTokenError::NotAllowed))); + }) + .may_block(true) + .dispatch_background_task(); + } + + pub fn sign( + &self, + _timeout_ms: u64, + ctap_args: SignArgs, + status: Sender<StatusUpdate>, + callback: StateCallback<Result<SignResult, AuthenticatorError>>, + ) { + if !static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + return; + } + + let state_obj = self.state.clone(); + + // Signing can block during signature selection, so we need to run it on a background thread. + let _ = RunnableBuilder::new("TestTokenManager::sign", move || { + // TODO(Bug 1854278) We should actually run one thread per token here + // and attempt to fulfill this request in parallel. + for token in state_obj.lock().unwrap().values_mut() { + let _ = token.init(); + if ctap2::sign( + token, + ctap_args.clone(), + status.clone(), + callback.clone(), + &|| true, + ) { + // callback was called + return; + } + } + + // Send an error, if the callback wasn't called already. + callback.call(Err(AuthenticatorError::U2FToken(U2FTokenError::NotAllowed))); + }) + .may_block(true) + .dispatch_background_task(); + } + + pub fn has_platform_authenticator(&self) -> bool { + if !static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + return false; + } + + for token in self.state.lock().unwrap().values_mut() { + let _ = token.init(); + if token.transport.as_str() == "internal" { + return true; + } + } + + false + } + + pub fn get_autofill_entries( + &self, + rp_id: &str, + credential_filter: &Vec<PublicKeyCredentialDescriptor>, + ) -> Result<ThinVec<Option<RefPtr<nsIWebAuthnAutoFillEntry>>>, nsresult> { + let guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let mut entries = ThinVec::new(); + + for token in guard.values() { + let credentials = token.get_credentials(); + for credential in credentials.deref() { + // The relying party ID must match. + if !rp_id.eq(&credential.rp.id) { + continue; + } + // Only discoverable credentials are admissible. + if !credential.is_discoverable_credential { + continue; + } + // Only credentials listed in the credential filter (if it is + // non-empty) are admissible. + if credential_filter.len() > 0 + && credential_filter + .iter() + .find(|cred| cred.id == credential.id) + .is_none() + { + continue; + } + let entry = WebAuthnAutoFillEntry::allocate(InitWebAuthnAutoFillEntry { + rp: credential.rp.id.clone(), + credential_id: credential.id.clone(), + }) + .query_interface::<nsIWebAuthnAutoFillEntry>() + .ok_or(NS_ERROR_FAILURE)?; + entries.push(Some(entry)); + } + } + Ok(entries) + } +} diff --git a/dom/webauthn/components.conf b/dom/webauthn/components.conf new file mode 100644 index 0000000000..f49a7833e6 --- /dev/null +++ b/dom/webauthn/components.conf @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{ebe8a51d-bd54-4838-b031-cd2289990e14}', + 'contract_ids': ['@mozilla.org/webauthn/service;1'], + 'headers': ['/dom/webauthn/WebAuthnService.h'], + 'constructor': 'mozilla::dom::NewWebAuthnService', + }, +] diff --git a/dom/webauthn/libudev-sys/Cargo.toml b/dom/webauthn/libudev-sys/Cargo.toml new file mode 100644 index 0000000000..b438a56927 --- /dev/null +++ b/dom/webauthn/libudev-sys/Cargo.toml @@ -0,0 +1,38 @@ +# This is a fork of libudev-sys that dynamically loads its symbols with +# dlopen when they are first used. This continues a precedent established +# by Firefox's gamepad APIs to minimize the footprint of features when they +# aren't being used. Specifically, this avoids the cost of dynamically linking +# in libudev at process startup. +# +# "-sys" crates are a convention in the rust ecosystem for "a simple header +# for a C library that Rust code can use to build a richer and safer API on +# top of". In this case, libudev-sys is used by the [libudev crate]. +# +# As of this writing, this hack is being used by the [authenticator-rs] crate, +# which uses the libudev crate. +# +# The libudev crate assumes libudev is being dynamically linked in, and +# checks if its symbols are null before intializing anything else, so that +# you can use it to detect if libudev is installed and gracefully +# disable gamepads or whatever else if it's not. +# +# If we're missing any symbols the libudev crate needs, then rust will give +# us a compilation error. It's not a problem if we export additional symbols. +# +# So while this is a bit of a weird hack, it works pretty robustly, and this +# crate is basically just a header for libudev so it's not a particularly +# significant maintenance burden. +# +# authenticator-rs: https://github.com/mozilla/authenticator-rs +# libudev crate: https://crates.io/crates/libudev + +[package] +name = "libudev-sys" +version = "0.1.3" +authors = ["Tim Taubert <ttaubert@mozilla.com>"] +description = "FFI bindings to libudev" +license = "MPL-2.0" + +[dependencies] +lazy_static = "1.0" +libc = "0.2" diff --git a/dom/webauthn/libudev-sys/src/lib.rs b/dom/webauthn/libudev-sys/src/lib.rs new file mode 100644 index 0000000000..a08234a4c6 --- /dev/null +++ b/dom/webauthn/libudev-sys/src/lib.rs @@ -0,0 +1,182 @@ +/* -*- Mode: rust; rust-indent-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#![allow(non_camel_case_types)] +#![allow(non_upper_case_globals)] + +#[macro_use] +extern crate lazy_static; +extern crate libc; + +use libc::{c_void,c_int,c_char,c_ulonglong,dev_t}; +use libc::{RTLD_GLOBAL,RTLD_LAZY,RTLD_NOLOAD}; +use libc::{dlopen,dlclose,dlsym}; +use std::ffi::CString; +use std::{marker,mem,ops,ptr}; + +#[repr(C)] +pub struct udev { + __private: c_void +} + +#[repr(C)] +pub struct udev_list_entry { + __private: c_void +} + +#[repr(C)] +pub struct udev_device { + __private: c_void +} + +#[repr(C)] +pub struct udev_monitor { + __private: c_void +} + +#[repr(C)] +pub struct udev_enumerate { + __private: c_void +} + +macro_rules! ifnull { + ($a:expr, $b:expr) => { + if $a.is_null() { $b } else { $a } + } +} + +struct Library(*mut c_void); + +impl Library { + fn open(name: &'static str) -> Library { + let flags = RTLD_LAZY | RTLD_GLOBAL; + let flags_noload = flags | RTLD_NOLOAD; + let name = CString::new(name).unwrap(); + let name = name.as_ptr(); + + Library(unsafe { + ifnull!(dlopen(name, flags_noload), dlopen(name, flags)) + }) + } + + fn get(&self, name: &'static str) -> *mut c_void { + let name = CString::new(name).unwrap(); + unsafe { dlsym(self.0, name.as_ptr()) } + } +} + +impl Drop for Library { + fn drop(&mut self) { + unsafe { dlclose(self.0); } + } +} + +unsafe impl Sync for Library {} +unsafe impl Send for Library {} + +lazy_static! { + static ref LIBRARY: Library = { + Library::open("libudev.so.1") + }; +} + +pub struct Symbol<T> { + ptr: *mut c_void, + pd: marker::PhantomData<T> +} + +impl<T> Symbol<T> { + fn new(ptr: *mut c_void) -> Self { + let default = Self::default as *mut c_void; + Self { ptr: ifnull!(ptr, default), pd: marker::PhantomData } + } + + // This is the default symbol, used whenever dlopen() fails. + // Users of this library are expected to check whether udev_new() returns + // a nullptr, and if so they MUST NOT call any other exported functions. + extern "C" fn default() -> *mut c_void { + ptr::null_mut() + } +} + +impl<T> ops::Deref for Symbol<T> { + type Target = T; + + fn deref(&self) -> &T { + unsafe { mem::transmute(&self.ptr) } + } +} + +unsafe impl<T: Sync + Send> Sync for Symbol<T> {} +unsafe impl<T: Sync + Send> Send for Symbol<T> {} + +macro_rules! define { + ($name:ident, $type:ty) => { + lazy_static! { + pub static ref $name : Symbol<$type> = { + Symbol::new(LIBRARY.get(stringify!($name))) + }; + } + }; +} + +// udev +define!(udev_new, extern "C" fn () -> *mut udev); +define!(udev_unref, extern "C" fn (*mut udev) -> *mut udev); + +// udev_list +define!(udev_list_entry_get_next, extern "C" fn (*mut udev_list_entry) -> *mut udev_list_entry); +define!(udev_list_entry_get_name, extern "C" fn (*mut udev_list_entry) -> *const c_char); +define!(udev_list_entry_get_value, extern "C" fn (*mut udev_list_entry) -> *const c_char); + +// udev_device +define!(udev_device_ref, extern "C" fn (*mut udev_device) -> *mut udev_device); +define!(udev_device_unref, extern "C" fn (*mut udev_device) -> *mut udev_device); +define!(udev_device_new_from_syspath, extern "C" fn (*mut udev, *const c_char) -> *mut udev_device); +define!(udev_device_get_parent, extern "C" fn (*mut udev_device) -> *mut udev_device); +define!(udev_device_get_devpath, extern "C" fn (*mut udev_device) -> *const c_char); +define!(udev_device_get_subsystem, extern "C" fn (*mut udev_device) -> *const c_char); +define!(udev_device_get_devtype, extern "C" fn (*mut udev_device) -> *const c_char); +define!(udev_device_get_syspath, extern "C" fn (*mut udev_device) -> *const c_char); +define!(udev_device_get_sysname, extern "C" fn (*mut udev_device) -> *const c_char); +define!(udev_device_get_sysnum, extern "C" fn (*mut udev_device) -> *const c_char); +define!(udev_device_get_devnode, extern "C" fn (*mut udev_device) -> *const c_char); +define!(udev_device_get_is_initialized, extern "C" fn (*mut udev_device) -> c_int); +define!(udev_device_get_properties_list_entry, extern "C" fn (*mut udev_device) -> *mut udev_list_entry); +define!(udev_device_get_property_value, extern "C" fn (*mut udev_device, *const c_char) -> *const c_char); +define!(udev_device_get_driver, extern "C" fn (*mut udev_device) -> *const c_char); +define!(udev_device_get_devnum, extern "C" fn (*mut udev_device) -> dev_t); +define!(udev_device_get_action, extern "C" fn (*mut udev_device) -> *const c_char); +define!(udev_device_get_sysattr_value, extern "C" fn (*mut udev_device, *const c_char) -> *const c_char); +define!(udev_device_set_sysattr_value, extern "C" fn (*mut udev_device, *const c_char, *mut c_char) -> c_int); +define!(udev_device_get_sysattr_list_entry, extern "C" fn (*mut udev_device) -> *mut udev_list_entry); +define!(udev_device_get_seqnum, extern "C" fn (*mut udev_device) -> c_ulonglong); + +// udev_monitor +define!(udev_monitor_ref, extern "C" fn (*mut udev_monitor) -> *mut udev_monitor); +define!(udev_monitor_unref, extern "C" fn (*mut udev_monitor) -> *mut udev_monitor); +define!(udev_monitor_new_from_netlink, extern "C" fn (*mut udev, *const c_char) -> *mut udev_monitor); +define!(udev_monitor_enable_receiving, extern "C" fn (*mut udev_monitor) -> c_int); +define!(udev_monitor_get_fd, extern "C" fn (*mut udev_monitor) -> c_int); +define!(udev_monitor_receive_device, extern "C" fn (*mut udev_monitor) -> *mut udev_device); +define!(udev_monitor_filter_add_match_subsystem_devtype, extern "C" fn (*mut udev_monitor, *const c_char, *const c_char) -> c_int); +define!(udev_monitor_filter_add_match_tag, extern "C" fn (*mut udev_monitor, *const c_char) -> c_int); +define!(udev_monitor_filter_remove, extern "C" fn (*mut udev_monitor) -> c_int); + +// udev_enumerate +define!(udev_enumerate_unref, extern "C" fn (*mut udev_enumerate) -> *mut udev_enumerate); +define!(udev_enumerate_new, extern "C" fn (*mut udev) -> *mut udev_enumerate); +define!(udev_enumerate_add_match_subsystem, extern "C" fn (*mut udev_enumerate, *const c_char) -> c_int); +define!(udev_enumerate_add_nomatch_subsystem, extern "C" fn (*mut udev_enumerate, *const c_char) -> c_int); +define!(udev_enumerate_add_match_sysattr, extern "C" fn (*mut udev_enumerate, *const c_char, *const c_char) -> c_int); +define!(udev_enumerate_add_nomatch_sysattr, extern "C" fn (*mut udev_enumerate, *const c_char, *const c_char) -> c_int); +define!(udev_enumerate_add_match_property, extern "C" fn (*mut udev_enumerate, *const c_char, *const c_char) -> c_int); +define!(udev_enumerate_add_match_tag, extern "C" fn (*mut udev_enumerate, *const c_char) -> c_int); +define!(udev_enumerate_add_match_parent, extern "C" fn (*mut udev_enumerate, *mut udev_device) -> c_int); +define!(udev_enumerate_add_match_is_initialized, extern "C" fn (*mut udev_enumerate) -> c_int); +define!(udev_enumerate_add_match_sysname, extern "C" fn (*mut udev_enumerate, *const c_char) -> c_int); +define!(udev_enumerate_add_syspath, extern "C" fn (*mut udev_enumerate, *const c_char) -> c_int); +define!(udev_enumerate_scan_devices, extern "C" fn (*mut udev_enumerate) -> c_int); +define!(udev_enumerate_get_list_entry, extern "C" fn (*mut udev_enumerate) -> *mut udev_list_entry); diff --git a/dom/webauthn/moz.build b/dom/webauthn/moz.build new file mode 100644 index 0000000000..5d84dc06e7 --- /dev/null +++ b/dom/webauthn/moz.build @@ -0,0 +1,97 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Web Authentication") + +IPDL_SOURCES += ["PWebAuthnTransaction.ipdl"] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +XPIDL_SOURCES += [ + "nsIWebAuthnArgs.idl", + "nsIWebAuthnAttObj.idl", + "nsIWebAuthnPromise.idl", + "nsIWebAuthnResult.idl", + "nsIWebAuthnService.idl", +] + +XPIDL_MODULE = "dom_webauthn" + +EXPORTS.mozilla.dom += [ + "AuthenticatorAssertionResponse.h", + "AuthenticatorAttestationResponse.h", + "AuthenticatorResponse.h", + "PublicKeyCredential.h", + "WebAuthnManager.h", + "WebAuthnManagerBase.h", + "WebAuthnPromiseHolder.h", + "WebAuthnTransactionChild.h", + "WebAuthnTransactionParent.h", + "WebAuthnUtil.h", + "winwebauthn/webauthn.h", +] + +UNIFIED_SOURCES += [ + "AuthenticatorAssertionResponse.cpp", + "AuthenticatorAttestationResponse.cpp", + "AuthenticatorResponse.cpp", + "PublicKeyCredential.cpp", + "WebAuthnArgs.cpp", + "WebAuthnAutoFillEntry.cpp", + "WebAuthnManager.cpp", + "WebAuthnManagerBase.cpp", + "WebAuthnPromiseHolder.cpp", + "WebAuthnResult.cpp", + "WebAuthnService.cpp", + "WebAuthnTransactionChild.cpp", + "WebAuthnTransactionParent.cpp", + "WebAuthnUtil.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/dom/base", + "/dom/crypto", + "/security/manager/ssl", + "/third_party/rust", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android": + UNIFIED_SOURCES += [ + "AndroidWebAuthnService.cpp", + ] + +if CONFIG["OS_ARCH"] == "Darwin": + UNIFIED_SOURCES += [ + "MacOSWebAuthnService.mm", + ] + OS_LIBS += [ + "-framework AuthenticationServices", + ] + +if CONFIG["OS_ARCH"] == "WINNT": + OS_LIBS += [ + "hid", + ] + +if CONFIG["OS_TARGET"] == "WINNT": + EXPORTS.mozilla.dom += [ + "WinWebAuthnService.h", + ] + UNIFIED_SOURCES += [ + "WinWebAuthnService.cpp", + ] + +MOCHITEST_MANIFESTS += ["tests/mochitest.toml"] +BROWSER_CHROME_MANIFESTS += [ + "tests/browser/browser.toml", +] diff --git a/dom/webauthn/nsIWebAuthnArgs.idl b/dom/webauthn/nsIWebAuthnArgs.idl new file mode 100644 index 0000000000..72999092fa --- /dev/null +++ b/dom/webauthn/nsIWebAuthnArgs.idl @@ -0,0 +1,98 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +typedef long COSEAlgorithmIdentifier; + +[uuid(2fc8febe-a277-11ed-bda2-8f6495a5e75c)] +interface nsIWebAuthnRegisterArgs : nsISupports { + // TODO(Bug 1820035) The origin is only used for prompt callbacks. Refactor and remove. + readonly attribute AString origin; + + readonly attribute Array<octet> challenge; + + readonly attribute ACString clientDataJSON; + + readonly attribute Array<octet> clientDataHash; + + // A PublicKeyCredentialRpEntity + readonly attribute AString rpId; + [must_use] readonly attribute AString rpName; + + // A PublicKeyCredentialUserEntity + [must_use] readonly attribute Array<octet> userId; + [must_use] readonly attribute AString userName; + [must_use] readonly attribute AString userDisplayName; + + // The spec defines this as a sequence<PublicKeyCredentialParameters>. + // We require type = "public-key" and only serialize the alg fields. + [must_use] readonly attribute Array<COSEAlgorithmIdentifier> coseAlgs; + + // The spec defines this as a sequence<PublicKeyCredentialDescriptor>, + // we use separate arrays for the credential IDs and transports. + readonly attribute Array<Array<octet> > excludeList; + readonly attribute Array<octet> excludeListTransports; + + // CTAP2 passes extensions in a CBOR map of extension identifier -> + // WebAuthn AuthenticationExtensionsClientInputs. That's not feasible here. + // So we define a getter for each supported extension input and use the + // return code to signal presence. + [must_use] readonly attribute bool credProps; + [must_use] readonly attribute bool hmacCreateSecret; + [must_use] readonly attribute bool minPinLength; + + // Options. + readonly attribute AString residentKey; + readonly attribute AString userVerification; + [must_use] readonly attribute AString authenticatorAttachment; + + // This is the WebAuthn PublicKeyCredentialCreationOptions timeout. + // Arguably we don't need to pass it through since WebAuthnController can + // cancel transactions. + readonly attribute uint32_t timeoutMS; + + // This is the WebAuthn PublicKeyCredentialCreationOptions attestation. + // We might overwrite the provided value with "none" if the user declines the + // consent popup. + [must_use] readonly attribute AString attestationConveyancePreference; +}; + +[uuid(2e621cf4-a277-11ed-ae00-bf41a54ef553)] +interface nsIWebAuthnSignArgs : nsISupports { + // TODO(Bug 1820035) The origin is only used for prompt callbacks. Refactor and remove. + readonly attribute AString origin; + + // The spec only asks for the ID field of a PublicKeyCredentialRpEntity here + readonly attribute AString rpId; + + readonly attribute Array<octet> challenge; + + readonly attribute ACString clientDataJSON; + + readonly attribute Array<octet> clientDataHash; + + // The spec defines this as a sequence<PublicKeyCredentialDescriptor>, + // we use separate arrays for the credential IDs and transports. + readonly attribute Array<Array<octet> > allowList; + readonly attribute Array<octet> allowListTransports; + + // CTAP2 passes extensions in a CBOR map of extension identifier -> + // WebAuthn AuthenticationExtensionsClientInputs. That's not feasible here. + // So we define a getter for each supported extension input and use the + // return code to signal presence. + [must_use] readonly attribute bool hmacCreateSecret; + [must_use] readonly attribute AString appId; + + // Options + [must_use] readonly attribute AString userVerification; + + // This is the WebAuthn PublicKeyCredentialCreationOptions timeout. + // Arguably we don't need to pass it through since WebAuthnController can + // cancel transactions. + readonly attribute unsigned long timeoutMS; + + readonly attribute bool conditionallyMediated; +}; diff --git a/dom/webauthn/nsIWebAuthnAttObj.idl b/dom/webauthn/nsIWebAuthnAttObj.idl new file mode 100644 index 0000000000..32d4f0aba3 --- /dev/null +++ b/dom/webauthn/nsIWebAuthnAttObj.idl @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIWebAuthnArgs.idl" + +[uuid(91e41be0-ed73-4a10-b55e-3312319bfddf)] +interface nsIWebAuthnAttObj : nsISupports { + // The serialied attestation object as defined in + // https://www.w3.org/TR/webauthn-2/#sctn-attestation + readonly attribute Array<octet> attestationObject; + + readonly attribute Array<octet> authenticatorData; + + readonly attribute Array<octet> publicKey; + + readonly attribute COSEAlgorithmIdentifier publicKeyAlgorithm; +}; diff --git a/dom/webauthn/nsIWebAuthnPromise.idl b/dom/webauthn/nsIWebAuthnPromise.idl new file mode 100644 index 0000000000..245515be3f --- /dev/null +++ b/dom/webauthn/nsIWebAuthnPromise.idl @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIWebAuthnResult.idl" + +[rust_sync, uuid(3c969aec-e0e0-4aa4-9422-394b321e6918)] +interface nsIWebAuthnRegisterPromise : nsISupports +{ + [noscript] void resolve(in nsIWebAuthnRegisterResult aResult); + [noscript] void reject(in nsresult error); +}; + +[rust_sync, uuid(35e35bdc-5369-4bfe-8d5c-bdf7b782b735)] +interface nsIWebAuthnSignPromise : nsISupports +{ + [noscript] void resolve(in nsIWebAuthnSignResult aResult); + [noscript] void reject(in nsresult error); +}; diff --git a/dom/webauthn/nsIWebAuthnResult.idl b/dom/webauthn/nsIWebAuthnResult.idl new file mode 100644 index 0000000000..aaf34cc5f9 --- /dev/null +++ b/dom/webauthn/nsIWebAuthnResult.idl @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[uuid(0567c384-a728-11ed-85f7-030324a370f0)] +interface nsIWebAuthnRegisterResult : nsISupports { + readonly attribute ACString clientDataJSON; + + // The serialied attestation object as defined in + // https://www.w3.org/TR/webauthn-2/#sctn-attestation + // Includes the format, the attestation statement, and + // the authenticator data. + readonly attribute Array<octet> attestationObject; + + // The Credential ID field of the Attestation Object's Attested + // Credential Data. This is used to construct the rawID field of a + // WebAuthn PublicKeyCredential without having to parse the + // attestationObject. + readonly attribute Array<octet> credentialId; + + readonly attribute Array<AString> transports; + + readonly attribute bool hmacCreateSecret; + + [must_use] attribute bool credPropsRk; + + [must_use] readonly attribute AString authenticatorAttachment; +}; + +// The nsIWebAuthnSignResult interface is used to construct IPDL-defined +// WebAuthnGetAssertionResult from either Rust or C++. +// +[uuid(05fff816-a728-11ed-b9ac-ff38cc2c8c28)] +interface nsIWebAuthnSignResult : nsISupports { + readonly attribute ACString clientDataJSON; + + // The ID field of the PublicKeyCredentialDescriptor returned + // from authenticatorGetAssertion. + readonly attribute Array<octet> credentialId; + + // The authData field of the authenticatorGetAssertion response + readonly attribute Array<octet> authenticatorData; + + // The signature field of the authenticatorGetAssertion response + readonly attribute Array<octet> signature; + + // The ID field of the PublicKeyCredentialUserEntity returned from + // authenticatorGetAssertion. (Optional) + [must_use] readonly attribute Array<octet> userHandle; + + // The displayName field of the PublicKeyCredentialUserEntity + // returned from authenticatorGetAssertion. (Optional) + [must_use] readonly attribute ACString userName; + + // appId field of AuthenticationExtensionsClientOutputs (Optional) + [must_use] attribute bool usedAppId; + + [must_use] readonly attribute AString authenticatorAttachment; +}; diff --git a/dom/webauthn/nsIWebAuthnService.idl b/dom/webauthn/nsIWebAuthnService.idl new file mode 100644 index 0000000000..6525508057 --- /dev/null +++ b/dom/webauthn/nsIWebAuthnService.idl @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIWebAuthnArgs.idl" +#include "nsIWebAuthnPromise.idl" + +[scriptable, uuid(6c4ecd9f-57c0-4d7d-8080-bf6e4d499f8f)] +interface nsICredentialParameters : nsISupports +{ + readonly attribute ACString credentialId; + readonly attribute bool isResidentCredential; + readonly attribute ACString rpId; + readonly attribute ACString privateKey; + readonly attribute ACString userHandle; + readonly attribute uint32_t signCount; +}; + +[scriptable, uuid(686d552e-a39d-4ba2-8127-faca54274039)] +interface nsIWebAuthnAutoFillEntry: nsISupports +{ + const octet PROVIDER_UNKNOWN = 0; + const octet PROVIDER_TEST_TOKEN = 1; + const octet PROVIDER_PLATFORM_WINDOWS = 2; + const octet PROVIDER_PLATFORM_MACOS = 3; + const octet PROVIDER_PLATFORM_ANDROID = 4; + + readonly attribute octet provider; + readonly attribute AString userName; + readonly attribute AString rpId; + readonly attribute Array<uint8_t> credentialId; +}; + +[scriptable, uuid(e236a9b4-a26f-11ed-b6cc-07a9834e19b1)] +interface nsIWebAuthnService : nsISupports +{ + // IsUserVerifyingPlatformAuthenticatorAvailable + readonly attribute bool isUVPAA; + + void makeCredential( + in uint64_t aTransactionId, + in uint64_t browsingContextId, + in nsIWebAuthnRegisterArgs args, + in nsIWebAuthnRegisterPromise promise); + + void getAssertion( + in uint64_t aTransactionId, + in uint64_t browsingContextId, + in nsIWebAuthnSignArgs args, + in nsIWebAuthnSignPromise promise); + + // Cancel the ongoing transaction and any prompts that are shown, but do not reject + // its promise. This is used by the IPC parent when it receives an abort signal. + // The IPC child has already rejected the promises at this point. + [noscript] void reset(); + + // Cancel the ongoing transaction. Reject its promise, but do not cancel + // prompts. This is used by WebAuthnPromptHelper when the user hits the + // "cancel" button. + void cancel(in uint64_t aTransactionId); + + // `hasPendingConditionalGet` returns the transaction ID of a pending + // conditionally-mediated getAssertion promise. The browsing context and + // origin arguments must match those of the pending promise. If there is no + // pending getAssertion promise, or the browsing context and origin do not + // match, then `hasPendingConditionalGet` returns 0. + uint64_t hasPendingConditionalGet(in uint64_t aBrowsingContextId, in AString aOrigin); + + // If there is a pending conditionally-mediated getAssertion promise with + // transaction ID equal to `aTransactionId`, `getAutoFillEntries` returns + // an nsIWebAuthnAutoFillEntry for each silently discoverable credential + // that can be used to fullfill the request. + Array<nsIWebAuthnAutoFillEntry> getAutoFillEntries(in uint64_t aTransactionId); + + // A pending conditionally-mediated getAssertion promise is resolved by + // calling `selectAutoFillEntry` or `resumeConditionalGet`. + // `selectAutoFillEntry` specifies the credential ID that should be used to + // fulfill the request, whereas `resumeConditionalGet` indicates that any + // allowed credential can be used. + void selectAutoFillEntry(in uint64_t aTransactionId, in Array<uint8_t> aCredentialId); + void resumeConditionalGet(in uint64_t aTransactionId); + + void pinCallback(in uint64_t aTransactionId, in ACString aPin); + void resumeMakeCredential(in uint64_t aTransactionId, in bool aForceNoneAttestation); + void selectionCallback(in uint64_t aTransactionId, in uint64_t aIndex); + + // Adds a virtual (software) authenticator for use in tests (particularly + // tests run via WebDriver). See + // https://w3c.github.io/webauthn/#sctn-automation-add-virtual-authenticator. + uint64_t addVirtualAuthenticator( + in ACString protocol, + in ACString transport, + in bool hasResidentKey, + in bool hasUserVerification, + in bool isUserConsenting, + in bool isUserVerified); + + // Removes a previously-added virtual authenticator, as identified by its + // id. See + // https://w3c.github.io/webauthn/#sctn-automation-remove-virtual-authenticator + void removeVirtualAuthenticator(in uint64_t authenticatorId); + + // Adds a credential to a previously-added authenticator. See + // https://w3c.github.io/webauthn/#sctn-automation-add-credential + void addCredential( + in uint64_t authenticatorId, + in ACString credentialId, + in bool isResidentCredential, + in ACString rpId, + in ACString privateKey, + in ACString userHandle, + in uint32_t signCount); + + // Gets all credentials that have been added to a virtual authenticator. + // See https://w3c.github.io/webauthn/#sctn-automation-get-credentials + Array<nsICredentialParameters> getCredentials(in uint64_t authenticatorId); + + // Removes a credential from a virtual authenticator. See + // https://w3c.github.io/webauthn/#sctn-automation-remove-credential + void removeCredential(in uint64_t authenticatorId, in ACString credentialId); + + // Removes all credentials from a virtual authenticator. See + // https://w3c.github.io/webauthn/#sctn-automation-remove-all-credentials + void removeAllCredentials(in uint64_t authenticatorId); + + // Sets the "isUserVerified" bit on a virtual authenticator. See + // https://w3c.github.io/webauthn/#sctn-automation-set-user-verified + void setUserVerified(in uint64_t authenticatorId, in bool isUserVerified); + + // about:webauthn-specific functions + void listen(); + void runCommand(in ACString aCommand); +}; diff --git a/dom/webauthn/tests/browser/browser.toml b/dom/webauthn/tests/browser/browser.toml new file mode 100644 index 0000000000..41dc82d6ee --- /dev/null +++ b/dom/webauthn/tests/browser/browser.toml @@ -0,0 +1,35 @@ +[DEFAULT] +support-files = [ + "head.js", + "tab_webauthn_result.html", + "../pkijs/*", + "../cbor.js", + "../u2futil.js", +] +prefs = [ + "security.webauth.webauthn=true", + "security.webauth.webauthn_enable_softtoken=true", + "security.webauth.webauthn_enable_usbtoken=false", + "security.webauthn.ctap2=true", + "security.webauthn.enable_conditional_mediation=true", + "security.webauthn.enable_macos_passkeys=false", +] + +["browser_abort_visibility.js"] +skip-if = [ + "win11_2009", # Test not relevant on 1903+ +] + +["browser_fido_appid_extension.js"] +skip-if = [ + "win11_2009", # Test not relevant on 1903+ +] + +["browser_webauthn_conditional_mediation.js"] + +["browser_webauthn_ipaddress.js"] + +["browser_webauthn_prompts.js"] +skip-if = [ + "win11_2009", # Test not relevant on 1903+ +] diff --git a/dom/webauthn/tests/browser/browser_abort_visibility.js b/dom/webauthn/tests/browser/browser_abort_visibility.js new file mode 100644 index 0000000000..d8b2b5edcc --- /dev/null +++ b/dom/webauthn/tests/browser/browser_abort_visibility.js @@ -0,0 +1,277 @@ +/* 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/browser/dom/webauthn/tests/browser/tab_webauthn_result.html"; + +add_task(async function test_setup() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ], + }); +}); +add_task(test_switch_tab); +add_task(test_new_window_make); +add_task(test_new_window_get); +add_task(test_minimize_make); +add_task(test_minimize_get); + +async function assertStatus(tab, expected) { + let actual = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function () { + info("visbility state: " + content.document.visibilityState); + info("active: " + content.browsingContext.isActive); + return content.document.getElementById("status").value; + } + ); + is(actual, expected, "webauthn request " + expected); +} + +async function waitForStatus(tab, expected) { + /* eslint-disable no-shadow */ + await SpecialPowers.spawn( + tab.linkedBrowser, + [[expected]], + async function (expected) { + return ContentTaskUtils.waitForCondition(() => { + info( + "expecting " + + expected + + ", visbility state: " + + content.document.visibilityState + ); + info( + "expecting " + + expected + + ", active: " + + content.browsingContext.isActive + ); + return content.document.getElementById("status").value == expected; + }); + } + ); + /* eslint-enable no-shadow */ + + await assertStatus(tab, expected); +} + +function startMakeCredentialRequest(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const cose_alg_ECDSA_w_SHA256 = -7; + + let publicKey = { + rp: { id: content.document.domain, name: "none" }, + user: { + id: new Uint8Array(), + name: "none", + displayName: "none", + }, + challenge: content.crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + pubKeyCredParams: [{ type: "public-key", alg: cose_alg_ECDSA_w_SHA256 }], + }; + + let status = content.document.getElementById("status"); + + info( + "Attempting to create credential for origin: " + + content.document.nodePrincipal.origin + ); + content.navigator.credentials + .create({ publicKey }) + .then(() => { + status.value = "completed"; + }) + .catch(() => { + status.value = "aborted"; + }); + + status.value = "pending"; + }); +} + +function startGetAssertionRequest(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let newCredential = { + type: "public-key", + id: content.crypto.getRandomValues(new Uint8Array(16)), + transports: ["usb"], + }; + + let publicKey = { + challenge: content.crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: content.document.domain, + allowCredentials: [newCredential], + }; + + let status = content.document.getElementById("status"); + + info( + "Attempting to get credential for origin: " + + content.document.nodePrincipal.origin + ); + content.navigator.credentials + .get({ publicKey }) + .then(() => { + status.value = "completed"; + }) + .catch(ex => { + info("aborted: " + ex); + status.value = "aborted"; + }); + + status.value = "pending"; + }); +} + +// Test that MakeCredential() and GetAssertion() requests +// are aborted when the current tab loses its focus. +async function test_switch_tab() { + // Create a new tab for the MakeCredential() request. + let tab_create = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_URL + ); + + // Start the request. + await startMakeCredentialRequest(tab_create); + await assertStatus(tab_create, "pending"); + + // Open another tab and switch to it. The first will lose focus. + let tab_get = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + await assertStatus(tab_create, "pending"); + + // Start a GetAssertion() request in the second tab, the first is aborted + await startGetAssertionRequest(tab_get); + await waitForStatus(tab_create, "aborted"); + await assertStatus(tab_get, "pending"); + + // Start a second request in the second tab. It should abort. + await startGetAssertionRequest(tab_get); + await waitForStatus(tab_get, "aborted"); + + // Close tabs. + BrowserTestUtils.removeTab(tab_create); + BrowserTestUtils.removeTab(tab_get); +} + +function waitForWindowActive(win, active) { + return Promise.all([ + BrowserTestUtils.waitForEvent(win, active ? "focus" : "blur"), + BrowserTestUtils.waitForEvent(win, active ? "activate" : "deactivate"), + ]); +} + +async function test_new_window_make() { + // Create a new tab for the MakeCredential() request. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Start a MakeCredential request. + await startMakeCredentialRequest(tab); + await assertStatus(tab, "pending"); + + let windowGonePromise = waitForWindowActive(window, false); + // Open a new window. The tab will lose focus. + let win = await BrowserTestUtils.openNewBrowserWindow(); + await windowGonePromise; + await assertStatus(tab, "pending"); + + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_new_window_get() { + // Create a new tab for the GetAssertion() request. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Start a GetAssertion request. + await startGetAssertionRequest(tab); + await assertStatus(tab, "pending"); + + let windowGonePromise = waitForWindowActive(window, false); + // Open a new window. The tab will lose focus. + let win = await BrowserTestUtils.openNewBrowserWindow(); + await windowGonePromise; + await assertStatus(tab, "pending"); + + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; + + // Close tab. + BrowserTestUtils.removeTab(tab); +} + +async function test_minimize_make() { + // Minimizing windows doesn't supported in headless mode. + if (Services.env.get("MOZ_HEADLESS")) { + return; + } + + // Create a new window for the MakeCredential() request. + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + + // Start a MakeCredential request. + await startMakeCredentialRequest(tab); + await assertStatus(tab, "pending"); + + // Minimize the window. + let windowGonePromise = waitForWindowActive(win, false); + win.minimize(); + await assertStatus(tab, "pending"); + await windowGonePromise; + + // Restore the window. + await new Promise(resolve => SimpleTest.waitForFocus(resolve, win)); + await assertStatus(tab, "pending"); + + // Close window and wait for main window to be focused again. + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; +} + +async function test_minimize_get() { + // Minimizing windows doesn't supported in headless mode. + if (Services.env.get("MOZ_HEADLESS")) { + return; + } + + // Create a new window for the GetAssertion() request. + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + + // Start a GetAssertion request. + await startGetAssertionRequest(tab); + await assertStatus(tab, "pending"); + + // Minimize the window. + let windowGonePromise = waitForWindowActive(win, false); + win.minimize(); + await assertStatus(tab, "pending"); + await windowGonePromise; + + // Restore the window. + await new Promise(resolve => SimpleTest.waitForFocus(resolve, win)); + await assertStatus(tab, "pending"); + + // Close window and wait for main window to be focused again. + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; +} diff --git a/dom/webauthn/tests/browser/browser_fido_appid_extension.js b/dom/webauthn/tests/browser/browser_fido_appid_extension.js new file mode 100644 index 0000000000..fca4443074 --- /dev/null +++ b/dom/webauthn/tests/browser/browser_fido_appid_extension.js @@ -0,0 +1,131 @@ +/* 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/"; + +let expectNotSupportedError = expectError("NotSupported"); +let expectNotAllowedError = expectError("NotAllowed"); +let expectSecurityError = expectError("Security"); + +let gAppId = "https://example.com/appId"; +let gCrossOriginAppId = "https://example.org/appId"; +let gAuthenticatorId = add_virtual_authenticator(); + +add_task(async function test_appid() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // The FIDO AppId extension can't be used for MakeCredential. + await promiseWebAuthnMakeCredential(tab, "none", "discouraged", { + appid: gAppId, + }) + .then(arrivingHereIsBad) + .catch(expectNotSupportedError); + + // Side-load a credential with an RP ID matching the App ID. + let credIdB64 = await addCredential(gAuthenticatorId, gAppId); + let credId = base64ToBytesUrlSafe(credIdB64); + + // And another for a different origin + let crossOriginCredIdB64 = await addCredential( + gAuthenticatorId, + gCrossOriginAppId + ); + let crossOriginCredId = base64ToBytesUrlSafe(crossOriginCredIdB64); + + // The App ID extension is required + await promiseWebAuthnGetAssertion(tab, credId) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + + // The value in the App ID extension must match the origin. + await promiseWebAuthnGetAssertion(tab, crossOriginCredId, { + appid: gCrossOriginAppId, + }) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + + // The value in the App ID extension must match the credential's RP ID. + await promiseWebAuthnGetAssertion(tab, credId, { appid: gAppId + "2" }) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + + // Succeed with the right App ID. + let rpIdHash = await promiseWebAuthnGetAssertion(tab, credId, { + appid: gAppId, + }) + .then(({ authenticatorData, extensions }) => { + is(extensions.appid, true, "appid extension was acted upon"); + return authenticatorData.slice(0, 32); + }) + .then(rpIdHash => { + // Make sure the returned RP ID hash matches the hash of the App ID. + checkRpIdHash(rpIdHash, gAppId); + }) + .catch(arrivingHereIsBad); + + removeCredential(gAuthenticatorId, credIdB64); + removeCredential(gAuthenticatorId, crossOriginCredIdB64); + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_appid_unused() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let appid = "https://example.com/appId"; + + let { attObj, rawId } = await promiseWebAuthnMakeCredential(tab); + let { authDataObj } = await webAuthnDecodeCBORAttestation(attObj); + + // Make sure the RP ID hash matches what we calculate. + await checkRpIdHash(authDataObj.rpIdHash, "example.com"); + + // Get a new assertion. + let { clientDataJSON, authenticatorData, signature, extensions } = + await promiseWebAuthnGetAssertion(tab, rawId, { appid }); + + ok( + "appid" in extensions, + `appid should be populated in the extensions data, but saw: ` + + `${JSON.stringify(extensions)}` + ); + is(extensions.appid, false, "appid extension should indicate it was unused"); + + // Check auth data. + let attestation = await webAuthnDecodeAuthDataArray( + new Uint8Array(authenticatorData) + ); + is( + "" + (attestation.flags & flag_TUP), + "" + flag_TUP, + "Assertion's user presence byte set correctly" + ); + + // Verify the signature. + let params = await deriveAppAndChallengeParam( + "example.com", + clientDataJSON, + attestation + ); + let signedData = await assembleSignedData( + params.appParam, + params.attestation.flags, + params.attestation.counter, + params.challengeParam + ); + let valid = await verifySignature( + authDataObj.publicKeyHandle, + signedData, + signature + ); + ok(valid, "signature is valid"); + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js b/dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js new file mode 100644 index 0000000000..fff1ec5dab --- /dev/null +++ b/dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js @@ -0,0 +1,177 @@ +/* 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"; + +let gAuthenticatorId = add_virtual_authenticator(); +let gExpectNotAllowedError = expectError("NotAllowed"); +let gExpectAbortError = expectError("Abort"); +let gPendingConditionalGetSubject = "webauthn:conditional-get-pending"; +let gWebAuthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService +); + +add_task(async function test_webauthn_modal_request_cancels_conditional_get() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let browser = tab.linkedBrowser.browsingContext.embedderElement; + let browsingContextId = browser.browsingContext.id; + + let transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + TEST_URL + ); + Assert.equal(transactionId, 0, "should not have a pending conditional get"); + + let requestStarted = TestUtils.topicObserved(gPendingConditionalGetSubject); + + let active = true; + let condPromise = promiseWebAuthnGetAssertionDiscoverable(tab, "conditional") + .then(arrivingHereIsBad) + .catch(gExpectAbortError) + .then(() => (active = false)); + + await requestStarted; + + transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + TEST_URL + ); + Assert.notEqual(transactionId, 0, "should have a pending conditional get"); + + ok(active, "conditional request should still be active"); + + let promptPromise = promiseNotification("webauthn-prompt-register-direct"); + let modalPromise = promiseWebAuthnMakeCredential(tab, "direct") + .then(arrivingHereIsBad) + .catch(gExpectNotAllowedError); + + await condPromise; + + ok(!active, "conditional request should not be active"); + + // Cancel the modal request with the button. + await promptPromise; + PopupNotifications.panel.firstElementChild.secondaryButton.click(); + await modalPromise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_webauthn_resume_conditional_get() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let browser = tab.linkedBrowser.browsingContext.embedderElement; + let browsingContextId = browser.browsingContext.id; + + let transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + TEST_URL + ); + Assert.equal(transactionId, 0, "should not have a pending conditional get"); + + let requestStarted = TestUtils.topicObserved(gPendingConditionalGetSubject); + + let active = true; + let promise = promiseWebAuthnGetAssertionDiscoverable(tab, "conditional") + .then(arrivingHereIsBad) + .catch(gExpectNotAllowedError) + .then(() => (active = false)); + + await requestStarted; + + transactionId = gWebAuthnService.hasPendingConditionalGet(0, TEST_URL); + Assert.equal( + transactionId, + 0, + "hasPendingConditionalGet should check the browsing context id" + ); + + transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + "https://example.org" + ); + Assert.equal( + transactionId, + 0, + "hasPendingConditionalGet should check the origin" + ); + + transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + TEST_URL + ); + Assert.notEqual(transactionId, 0, "should have a pending conditional get"); + + ok(active, "request should still be active"); + + gWebAuthnService.resumeConditionalGet(transactionId); + await promise; + + ok(!active, "request should not be active"); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_webauthn_select_autofill_entry() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Add credentials + let cred1 = await addCredential(gAuthenticatorId, "example.com"); + let cred2 = await addCredential(gAuthenticatorId, "example.com"); + + let browser = tab.linkedBrowser.browsingContext.embedderElement; + let browsingContextId = browser.browsingContext.id; + + let transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + TEST_URL + ); + Assert.equal(transactionId, 0, "should not have a pending conditional get"); + + let requestStarted = TestUtils.topicObserved(gPendingConditionalGetSubject); + + let active = true; + let promise = promiseWebAuthnGetAssertionDiscoverable(tab, "conditional") + .catch(arrivingHereIsBad) + .then(() => (active = false)); + + await requestStarted; + + transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + TEST_URL + ); + Assert.notEqual(transactionId, 0, "should have a pending conditional get"); + + let autoFillEntries = gWebAuthnService.getAutoFillEntries(transactionId); + ok( + autoFillEntries.length == 2 && + autoFillEntries[0].rpId == "example.com" && + autoFillEntries[1].rpId == "example.com", + "should have two autofill entries for example.com" + ); + + gWebAuthnService.selectAutoFillEntry( + transactionId, + autoFillEntries[0].credentialId + ); + let result = await promise; + + ok(!active, "request should not be active"); + + // Remove credentials + gWebAuthnService.removeCredential(gAuthenticatorId, cred1); + gWebAuthnService.removeCredential(gAuthenticatorId, cred2); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js b/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js new file mode 100644 index 0000000000..6658ae5f02 --- /dev/null +++ b/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js @@ -0,0 +1,30 @@ +/* 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"; + +add_virtual_authenticator(); + +let expectSecurityError = expectError("Security"); + +add_task(async function test_setup() { + return SpecialPowers.pushPrefEnv({ + set: [["network.proxy.allow_hijacking_localhost", true]], + }); +}); + +add_task(async function test_appid() { + // 127.0.0.1 triggers special cases in ssltunnel, so let's use .2! + const TEST_URL = "https://127.0.0.2/"; + + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + await promiseWebAuthnMakeCredential(tab) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/webauthn/tests/browser/browser_webauthn_prompts.js b/dom/webauthn/tests/browser/browser_webauthn_prompts.js new file mode 100644 index 0000000000..05c77271d5 --- /dev/null +++ b/dom/webauthn/tests/browser/browser_webauthn_prompts.js @@ -0,0 +1,501 @@ +/* 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"; + +XPCOMUtils.defineLazyScriptGetter( + this, + ["FullScreen"], + "chrome://browser/content/browser-fullScreenAndPointerLock.js" +); + +const TEST_URL = "https://example.com/"; +var gAuthenticatorId; + +/** + * Waits for the PopupNotifications button enable delay to expire so the + * Notification can be interacted with using the buttons. + */ +async function waitForPopupNotificationSecurityDelay() { + let notification = PopupNotifications.panel.firstChild.notification; + let notificationEnableDelayMS = Services.prefs.getIntPref( + "security.notification_enable_delay" + ); + await TestUtils.waitForCondition( + () => { + let timeSinceShown = performance.now() - notification.timeShown; + return timeSinceShown > notificationEnableDelayMS; + }, + "Wait for security delay to expire", + 500, + 50 + ); +} + +add_task(async function test_setup_usbtoken() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ], + }); +}); +add_task(test_register); +add_task(test_register_escape); +add_task(test_register_direct_cancel); +add_task(test_register_direct_presence); +add_task(test_sign); +add_task(test_sign_escape); +add_task(test_tab_switching); +add_task(test_window_switching); +add_task(async function test_setup_fullscreen() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["browser.fullscreen.autohide", true], + ["full-screen-api.enabled", true], + ["full-screen-api.allow-trusted-requests-only", false], + ], + }); +}); +add_task(test_fullscreen_show_nav_toolbar); +add_task(test_no_fullscreen_dom); +add_task(async function test_setup_softtoken() { + gAuthenticatorId = add_virtual_authenticator(); + return SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.webauthn_enable_softtoken", true], + ["security.webauth.webauthn_enable_usbtoken", false], + ], + }); +}); +add_task(test_register_direct_proceed); +add_task(test_register_direct_proceed_anon); +add_task(test_select_sign_result); + +function promiseNavToolboxStatus(aExpectedStatus) { + let navToolboxStatus; + return TestUtils.topicObserved("fullscreen-nav-toolbox", (subject, data) => { + navToolboxStatus = data; + return data == aExpectedStatus; + }).then(() => + Assert.equal( + navToolboxStatus, + aExpectedStatus, + "nav toolbox is " + aExpectedStatus + ) + ); +} + +function promiseFullScreenPaint(aExpectedStatus) { + return TestUtils.topicObserved("fullscreen-painted"); +} + +function triggerMainPopupCommand(popup) { + info("triggering main command"); + let notifications = popup.childNodes; + ok(notifications.length, "at least one notification displayed"); + let notification = notifications[0]; + info("triggering command: " + notification.getAttribute("buttonlabel")); + + return EventUtils.synthesizeMouseAtCenter(notification.button, {}); +} + +let expectNotAllowedError = expectError("NotAllowed"); + +function verifyAnonymizedCertificate(aResult) { + return webAuthnDecodeCBORAttestation(aResult.attObj).then( + ({ fmt, attStmt }) => { + is(fmt, "none", "Is a None Attestation"); + is(typeof attStmt, "object", "attStmt is a map"); + is(Object.keys(attStmt).length, 0, "attStmt is empty"); + } + ); +} + +async function verifyDirectCertificate(aResult) { + let clientDataHash = await crypto.subtle + .digest("SHA-256", aResult.clientDataJSON) + .then(digest => new Uint8Array(digest)); + let { fmt, attStmt, authData, authDataObj } = + await webAuthnDecodeCBORAttestation(aResult.attObj); + is(fmt, "packed", "Is a Packed Attestation"); + let signedData = new Uint8Array(authData.length + clientDataHash.length); + signedData.set(authData); + signedData.set(clientDataHash, authData.length); + let valid = await verifySignature( + authDataObj.publicKeyHandle, + signedData, + new Uint8Array(attStmt.sig) + ); + ok(valid, "Signature is valid."); +} + +async function test_register() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + // Cancel the request with the button. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.button.click(); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_escape() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + // Cancel the request by hitting escape. + ok(active, "request should still be active"); + EventUtils.synthesizeKey("KEY_Escape"); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_sign() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new assertion and wait for the prompt. + let active = true; + let request = promiseWebAuthnGetAssertion(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + // Cancel the request with the button. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.button.click(); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_sign_escape() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new assertion and wait for the prompt. + let active = true; + let request = promiseWebAuthnGetAssertion(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + // Cancel the request by hitting escape. + ok(active, "request should still be active"); + EventUtils.synthesizeKey("KEY_Escape"); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_cancel() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let active = true; + let promise = promiseWebAuthnMakeCredential(tab, "direct") + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-register-direct"); + + // Cancel the request. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.secondaryButton.click(); + await promise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_presence() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let active = true; + let promise = promiseWebAuthnMakeCredential(tab, "direct") + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-register-direct"); + + // Click "proceed" and wait for presence prompt + let presence = promiseNotification("webauthn-prompt-presence"); + PopupNotifications.panel.firstElementChild.button.click(); + await presence; + + // Cancel the request. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.button.click(); + await promise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +// Add two tabs, open WebAuthn in the first, switch, assert the prompt is +// not visible, switch back, assert the prompt is there and cancel it. +async function test_tab_switching() { + // Open a new tab. + let tab_one = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab_one) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Open and switch to a second tab. + let tab_two = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.org/" + ); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "closed" + ); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + + // Go back to the first tab + await BrowserTestUtils.removeTab(tab_two); + + await promiseNotification("webauthn-prompt-presence"); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Cancel the request. + ok(active, "request should still be active"); + await triggerMainPopupCommand(PopupNotifications.panel); + await request; + ok(!active, "request should be stopped"); + + // Close tab. + await BrowserTestUtils.removeTab(tab_one); +} + +// Add two tabs, open WebAuthn in the first, switch, assert the prompt is +// not visible, switch back, assert the prompt is there and cancel it. +async function test_window_switching() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Open and switch to a second window + let new_window = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(new_window); + + await TestUtils.waitForCondition( + () => new_window.PopupNotifications.panel.state == "closed" + ); + is( + new_window.PopupNotifications.panel.state, + "closed", + "Doorhanger is hidden" + ); + + // Go back to the first tab + await BrowserTestUtils.closeWindow(new_window); + await SimpleTest.promiseFocus(window); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is still visible"); + + // Cancel the request. + ok(active, "request should still be active"); + await triggerMainPopupCommand(PopupNotifications.panel); + await request; + ok(!active, "request should be stopped"); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_proceed() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let request = promiseWebAuthnMakeCredential(tab, "direct"); + await promiseNotification("webauthn-prompt-register-direct"); + + // Proceed. + PopupNotifications.panel.firstElementChild.button.click(); + + // Ensure we got "direct" attestation. + await request.then(verifyDirectCertificate); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_proceed_anon() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let request = promiseWebAuthnMakeCredential(tab, "direct"); + await promiseNotification("webauthn-prompt-register-direct"); + + // Check "anonymize anyway" and proceed. + PopupNotifications.panel.firstElementChild.checkbox.checked = true; + PopupNotifications.panel.firstElementChild.button.click(); + + // Ensure we got "none" attestation. + await request.then(verifyAnonymizedCertificate); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_select_sign_result() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Make two discoverable credentials for the same RP ID so that + // the user has to select one to return. + let cred1 = await addCredential(gAuthenticatorId, "example.com"); + let cred2 = await addCredential(gAuthenticatorId, "example.com"); + + let active = true; + let request = promiseWebAuthnGetAssertionDiscoverable(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + + // Ensure the selection prompt is shown + await promiseNotification("webauthn-prompt-select-sign-result"); + + ok(active, "request is active"); + + // Cancel the request + PopupNotifications.panel.firstElementChild.button.click(); + await request; + + await removeCredential(gAuthenticatorId, cred1); + await removeCredential(gAuthenticatorId, cred2); + await BrowserTestUtils.removeTab(tab); +} + +async function test_fullscreen_show_nav_toolbar() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Start with the window fullscreen and the nav toolbox hidden + let fullscreenState = window.fullScreen; + + let navToolboxHiddenPromise = promiseNavToolboxStatus("hidden"); + + window.fullScreen = true; + FullScreen.hideNavToolbox(false); + + await navToolboxHiddenPromise; + + // Request a new credential and wait for the direct attestation consent + // prompt. + let promptPromise = promiseNotification("webauthn-prompt-register-direct"); + let navToolboxShownPromise = promiseNavToolboxStatus("shown"); + + let active = true; + let requestPromise = promiseWebAuthnMakeCredential(tab, "direct") + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + + await Promise.all([promptPromise, navToolboxShownPromise]); + + ok(active, "request is active"); + ok(window.fullScreen, "window is fullscreen"); + + // Cancel the request. + PopupNotifications.panel.firstElementChild.secondaryButton.click(); + await requestPromise; + + window.fullScreen = fullscreenState; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_no_fullscreen_dom() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let fullScreenPaintPromise = promiseFullScreenPaint(); + // Make a DOM element fullscreen + await ContentTask.spawn(tab.linkedBrowser, [], () => { + return content.document.body.requestFullscreen(); + }); + await fullScreenPaintPromise; + ok(!!document.fullscreenElement, "a DOM element is fullscreen"); + + // Request a new credential and wait for the direct attestation consent + // prompt. + let promptPromise = promiseNotification("webauthn-prompt-register-direct"); + fullScreenPaintPromise = promiseFullScreenPaint(); + + let active = true; + let requestPromise = promiseWebAuthnMakeCredential(tab, "direct") + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + + await Promise.all([promptPromise, fullScreenPaintPromise]); + + ok(active, "request is active"); + ok(!document.fullscreenElement, "no DOM element is fullscreen"); + + // Cancel the request. + await waitForPopupNotificationSecurityDelay(); + PopupNotifications.panel.firstElementChild.secondaryButton.click(); + await requestPromise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} diff --git a/dom/webauthn/tests/browser/head.js b/dom/webauthn/tests/browser/head.js new file mode 100644 index 0000000000..d6cbd56133 --- /dev/null +++ b/dom/webauthn/tests/browser/head.js @@ -0,0 +1,259 @@ +/* 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"; + +let exports = this; + +const scripts = [ + "pkijs/common.js", + "pkijs/asn1.js", + "pkijs/x509_schema.js", + "pkijs/x509_simpl.js", + "browser/cbor.js", + "browser/u2futil.js", +]; + +for (let script of scripts) { + Services.scriptloader.loadSubScript( + `chrome://mochitests/content/browser/dom/webauthn/tests/${script}`, + this + ); +} + +function add_virtual_authenticator(autoremove = true) { + let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService + ); + let id = webauthnService.addVirtualAuthenticator( + "ctap2_1", + "internal", + true, + true, + true, + true + ); + if (autoremove) { + registerCleanupFunction(() => { + webauthnService.removeVirtualAuthenticator(id); + }); + } + return id; +} + +async function addCredential(authenticatorId, rpId) { + let keyPair = await crypto.subtle.generateKey( + { + name: "ECDSA", + namedCurve: "P-256", + }, + true, + ["sign"] + ); + + let credId = new Uint8Array(32); + crypto.getRandomValues(credId); + credId = bytesToBase64UrlSafe(credId); + + let privateKey = await crypto.subtle + .exportKey("pkcs8", keyPair.privateKey) + .then(privateKey => bytesToBase64UrlSafe(privateKey)); + + let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService + ); + + webauthnService.addCredential( + authenticatorId, + credId, + true, // resident key + rpId, + privateKey, + "VGVzdCBVc2Vy", // "Test User" + 0 // sign count + ); + + return credId; +} + +async function removeCredential(authenticatorId, credId) { + let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService + ); + + webauthnService.removeCredential(authenticatorId, credId); +} + +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; +} + +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` + ); + }; +} + +/* eslint-disable no-shadow */ +function promiseWebAuthnMakeCredential( + tab, + attestation = "none", + residentKey = "discouraged", + extensions = {} +) { + return ContentTask.spawn( + tab.linkedBrowser, + [attestation, residentKey, extensions], + ([attestation, residentKey, extensions]) => { + 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" }, + user: { + id: new Uint8Array(), + name: "none", + displayName: "none", + }, + pubKeyCredParams, + authenticatorSelection: { + authenticatorAttachment: "cross-platform", + residentKey, + }, + extensions, + attestation, + challenge, + }; + + return content.navigator.credentials + .create({ publicKey }) + .then(credential => { + return { + clientDataJSON: credential.response.clientDataJSON, + attObj: credential.response.attestationObject, + rawId: credential.rawId, + }; + }); + } + ); +} + +function promiseWebAuthnGetAssertion(tab, key_handle = null, extensions = {}) { + return ContentTask.spawn( + tab.linkedBrowser, + [key_handle, extensions], + ([key_handle, extensions]) => { + let challenge = content.crypto.getRandomValues(new Uint8Array(16)); + if (key_handle == null) { + key_handle = 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(assertion => { + return { + authenticatorData: assertion.response.authenticatorData, + clientDataJSON: assertion.response.clientDataJSON, + extensions: assertion.getClientExtensionResults(), + signature: assertion.response.signature, + }; + }); + } + ); +} + +function promiseWebAuthnGetAssertionDiscoverable( + tab, + mediation = "optional", + extensions = {} +) { + return ContentTask.spawn( + tab.linkedBrowser, + [extensions, mediation], + ([extensions, mediation]) => { + let challenge = content.crypto.getRandomValues(new Uint8Array(16)); + + let publicKey = { + challenge, + extensions, + rpId: content.document.domain, + allowCredentials: [], + }; + + return content.navigator.credentials.get({ publicKey, mediation }); + } + ); +} + +function checkRpIdHash(rpIdHash, hostname) { + return crypto.subtle + .digest("SHA-256", string2buffer(hostname)) + .then(calculatedRpIdHash => { + let calcHashStr = bytesToBase64UrlSafe( + new Uint8Array(calculatedRpIdHash) + ); + let providedHashStr = bytesToBase64UrlSafe(new Uint8Array(rpIdHash)); + + if (calcHashStr != providedHashStr) { + throw new Error("Calculated RP ID hash doesn't match."); + } + }); +} + +function promiseNotification(id) { + return new Promise(resolve => { + PopupNotifications.panel.addEventListener("popupshown", function shown() { + let notification = PopupNotifications.getNotification(id); + if (notification) { + ok(true, `${id} prompt visible`); + PopupNotifications.panel.removeEventListener("popupshown", shown); + resolve(); + } + }); + }); +} +/* eslint-enable no-shadow */ diff --git a/dom/webauthn/tests/browser/tab_webauthn_result.html b/dom/webauthn/tests/browser/tab_webauthn_result.html new file mode 100644 index 0000000000..8e8b9f82cd --- /dev/null +++ b/dom/webauthn/tests/browser/tab_webauthn_result.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Generic W3C Web Authentication Test Result Page</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<h1>Generic W3C Web Authentication Test Result Page</h1> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1415675">Mozilla Bug 1415675</a> +<input type="text" id="status" value="init" /> + +</body> +</html> diff --git a/dom/webauthn/tests/cbor.js b/dom/webauthn/tests/cbor.js new file mode 100644 index 0000000000..3e1f300df3 --- /dev/null +++ b/dom/webauthn/tests/cbor.js @@ -0,0 +1,406 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Patrick Gansterer <paroga@paroga.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +(function(global, undefined) { "use strict"; +var POW_2_24 = 5.960464477539063e-8, + POW_2_32 = 4294967296, + POW_2_53 = 9007199254740992; + +function encode(value) { + var data = new ArrayBuffer(256); + var dataView = new DataView(data); + var lastLength; + var offset = 0; + + function prepareWrite(length) { + var newByteLength = data.byteLength; + var requiredLength = offset + length; + while (newByteLength < requiredLength) + newByteLength <<= 1; + if (newByteLength !== data.byteLength) { + var oldDataView = dataView; + data = new ArrayBuffer(newByteLength); + dataView = new DataView(data); + var uint32count = (offset + 3) >> 2; + for (var i = 0; i < uint32count; ++i) + dataView.setUint32(i << 2, oldDataView.getUint32(i << 2)); + } + + lastLength = length; + return dataView; + } + function commitWrite() { + offset += lastLength; + } + function writeFloat64(value) { + commitWrite(prepareWrite(8).setFloat64(offset, value)); + } + function writeUint8(value) { + commitWrite(prepareWrite(1).setUint8(offset, value)); + } + function writeUint8Array(value) { + var dataView = prepareWrite(value.length); + for (var i = 0; i < value.length; ++i) + dataView.setUint8(offset + i, value[i]); + commitWrite(); + } + function writeUint16(value) { + commitWrite(prepareWrite(2).setUint16(offset, value)); + } + function writeUint32(value) { + commitWrite(prepareWrite(4).setUint32(offset, value)); + } + function writeUint64(value) { + var low = value % POW_2_32; + var high = (value - low) / POW_2_32; + var dataView = prepareWrite(8); + dataView.setUint32(offset, high); + dataView.setUint32(offset + 4, low); + commitWrite(); + } + function writeTypeAndLength(type, length) { + if (length < 24) { + writeUint8(type << 5 | length); + } else if (length < 0x100) { + writeUint8(type << 5 | 24); + writeUint8(length); + } else if (length < 0x10000) { + writeUint8(type << 5 | 25); + writeUint16(length); + } else if (length < 0x100000000) { + writeUint8(type << 5 | 26); + writeUint32(length); + } else { + writeUint8(type << 5 | 27); + writeUint64(length); + } + } + + function encodeItem(value) { + var i; + + if (value === false) + return writeUint8(0xf4); + if (value === true) + return writeUint8(0xf5); + if (value === null) + return writeUint8(0xf6); + if (value === undefined) + return writeUint8(0xf7); + + switch (typeof value) { + case "number": + if (Math.floor(value) === value) { + if (0 <= value && value <= POW_2_53) + return writeTypeAndLength(0, value); + if (-POW_2_53 <= value && value < 0) + return writeTypeAndLength(1, -(value + 1)); + } + writeUint8(0xfb); + return writeFloat64(value); + + case "string": + var utf8data = []; + for (i = 0; i < value.length; ++i) { + var charCode = value.charCodeAt(i); + if (charCode < 0x80) { + utf8data.push(charCode); + } else if (charCode < 0x800) { + utf8data.push(0xc0 | charCode >> 6); + utf8data.push(0x80 | charCode & 0x3f); + } else if (charCode < 0xd800) { + utf8data.push(0xe0 | charCode >> 12); + utf8data.push(0x80 | (charCode >> 6) & 0x3f); + utf8data.push(0x80 | charCode & 0x3f); + } else { + charCode = (charCode & 0x3ff) << 10; + charCode |= value.charCodeAt(++i) & 0x3ff; + charCode += 0x10000; + + utf8data.push(0xf0 | charCode >> 18); + utf8data.push(0x80 | (charCode >> 12) & 0x3f); + utf8data.push(0x80 | (charCode >> 6) & 0x3f); + utf8data.push(0x80 | charCode & 0x3f); + } + } + + writeTypeAndLength(3, utf8data.length); + return writeUint8Array(utf8data); + + default: + var length; + if (Array.isArray(value)) { + length = value.length; + writeTypeAndLength(4, length); + for (i = 0; i < length; ++i) + encodeItem(value[i]); + } else if (value instanceof Uint8Array) { + writeTypeAndLength(2, value.length); + writeUint8Array(value); + } else { + var keys = Object.keys(value); + length = keys.length; + writeTypeAndLength(5, length); + for (i = 0; i < length; ++i) { + var key = keys[i]; + encodeItem(key); + encodeItem(value[key]); + } + } + } + } + + encodeItem(value); + + if ("slice" in data) + return data.slice(0, offset); + + var ret = new ArrayBuffer(offset); + var retView = new DataView(ret); + for (var i = 0; i < offset; ++i) + retView.setUint8(i, dataView.getUint8(i)); + return ret; +} + +function decode(data, tagger, simpleValue) { + var dataView = new DataView(data); + var offset = 0; + + if (typeof tagger !== "function") + tagger = function(value) { return value; }; + if (typeof simpleValue !== "function") + simpleValue = function() { return undefined; }; + + function commitRead(length, value) { + offset += length; + return value; + } + function readArrayBuffer(length) { + return commitRead(length, new Uint8Array(data, offset, length)); + } + function readFloat16() { + var tempArrayBuffer = new ArrayBuffer(4); + var tempDataView = new DataView(tempArrayBuffer); + var value = readUint16(); + + var sign = value & 0x8000; + var exponent = value & 0x7c00; + var fraction = value & 0x03ff; + + if (exponent === 0x7c00) + exponent = 0xff << 10; + else if (exponent !== 0) + exponent += (127 - 15) << 10; + else if (fraction !== 0) + return (sign ? -1 : 1) * fraction * POW_2_24; + + tempDataView.setUint32(0, sign << 16 | exponent << 13 | fraction << 13); + return tempDataView.getFloat32(0); + } + function readFloat32() { + return commitRead(4, dataView.getFloat32(offset)); + } + function readFloat64() { + return commitRead(8, dataView.getFloat64(offset)); + } + function readUint8() { + return commitRead(1, dataView.getUint8(offset)); + } + function readUint16() { + return commitRead(2, dataView.getUint16(offset)); + } + function readUint32() { + return commitRead(4, dataView.getUint32(offset)); + } + function readUint64() { + return readUint32() * POW_2_32 + readUint32(); + } + function readBreak() { + if (dataView.getUint8(offset) !== 0xff) + return false; + offset += 1; + return true; + } + function readLength(additionalInformation) { + if (additionalInformation < 24) + return additionalInformation; + if (additionalInformation === 24) + return readUint8(); + if (additionalInformation === 25) + return readUint16(); + if (additionalInformation === 26) + return readUint32(); + if (additionalInformation === 27) + return readUint64(); + if (additionalInformation === 31) + return -1; + throw "Invalid length encoding"; + } + function readIndefiniteStringLength(majorType) { + var initialByte = readUint8(); + if (initialByte === 0xff) + return -1; + var length = readLength(initialByte & 0x1f); + if (length < 0 || (initialByte >> 5) !== majorType) + throw "Invalid indefinite length element"; + return length; + } + + function appendUtf16Data(utf16data, length) { + for (var i = 0; i < length; ++i) { + var value = readUint8(); + if (value & 0x80) { + if (value < 0xe0) { + value = (value & 0x1f) << 6 + | (readUint8() & 0x3f); + length -= 1; + } else if (value < 0xf0) { + value = (value & 0x0f) << 12 + | (readUint8() & 0x3f) << 6 + | (readUint8() & 0x3f); + length -= 2; + } else { + value = (value & 0x0f) << 18 + | (readUint8() & 0x3f) << 12 + | (readUint8() & 0x3f) << 6 + | (readUint8() & 0x3f); + length -= 3; + } + } + + if (value < 0x10000) { + utf16data.push(value); + } else { + value -= 0x10000; + utf16data.push(0xd800 | (value >> 10)); + utf16data.push(0xdc00 | (value & 0x3ff)); + } + } + } + + function decodeItem() { + var initialByte = readUint8(); + var majorType = initialByte >> 5; + var additionalInformation = initialByte & 0x1f; + var i; + var length; + + if (majorType === 7) { + switch (additionalInformation) { + case 25: + return readFloat16(); + case 26: + return readFloat32(); + case 27: + return readFloat64(); + } + } + + length = readLength(additionalInformation); + if (length < 0 && (majorType < 2 || 6 < majorType)) + throw "Invalid length"; + + switch (majorType) { + case 0: + return length; + case 1: + return -1 - length; + case 2: + if (length < 0) { + var elements = []; + var fullArrayLength = 0; + while ((length = readIndefiniteStringLength(majorType)) >= 0) { + fullArrayLength += length; + elements.push(readArrayBuffer(length)); + } + var fullArray = new Uint8Array(fullArrayLength); + var fullArrayOffset = 0; + for (i = 0; i < elements.length; ++i) { + fullArray.set(elements[i], fullArrayOffset); + fullArrayOffset += elements[i].length; + } + return fullArray; + } + return readArrayBuffer(length); + case 3: + var utf16data = []; + if (length < 0) { + while ((length = readIndefiniteStringLength(majorType)) >= 0) + appendUtf16Data(utf16data, length); + } else + appendUtf16Data(utf16data, length); + return String.fromCharCode.apply(null, utf16data); + case 4: + var retArray; + if (length < 0) { + retArray = []; + while (!readBreak()) + retArray.push(decodeItem()); + } else { + retArray = new Array(length); + for (i = 0; i < length; ++i) + retArray[i] = decodeItem(); + } + return retArray; + case 5: + var retObject = {}; + for (i = 0; i < length || length < 0 && !readBreak(); ++i) { + var key = decodeItem(); + retObject[key] = decodeItem(); + } + return retObject; + case 6: + return tagger(decodeItem(), length); + case 7: + switch (length) { + case 20: + return false; + case 21: + return true; + case 22: + return null; + case 23: + return undefined; + default: + return simpleValue(length); + } + } + } + + var ret = decodeItem(); + if (offset !== data.byteLength) + throw "Remaining bytes"; + return ret; +} + +var obj = { encode: encode, decode: decode }; + +if (typeof define === "function" && define.amd) + define("cbor/cbor", obj); +else if (typeof module !== "undefined" && module.exports) + module.exports = obj; +else if (!global.CBOR) + global.CBOR = obj; + +})(this); diff --git a/dom/webauthn/tests/empty.html b/dom/webauthn/tests/empty.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/webauthn/tests/empty.html diff --git a/dom/webauthn/tests/get_assertion_dead_object.html b/dom/webauthn/tests/get_assertion_dead_object.html new file mode 100644 index 0000000000..e7de9d3deb --- /dev/null +++ b/dom/webauthn/tests/get_assertion_dead_object.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset=utf-8> +</head> +<body> +<script type="text/javascript"> + window.addEventListener('load', function() { + let o = []; + o[0] = window.navigator; + document.writeln(''); + // Since the USB token is enabled by default, this will pop up a notification that the + // user can insert/interact with it. Since this is just a test, this won't happen. The + // request will eventually time out. + // Unfortunately the minimum timeout is 15 seconds. + o[0].credentials.get({ publicKey: { challenge: new Uint8Array(128), timeout: 15000 } }); + o.forEach((n, i) => o[i] = null); + }); +</script> +</body> +</html> diff --git a/dom/webauthn/tests/mochitest.toml b/dom/webauthn/tests/mochitest.toml new file mode 100644 index 0000000000..2082d39a58 --- /dev/null +++ b/dom/webauthn/tests/mochitest.toml @@ -0,0 +1,118 @@ +[DEFAULT] +support-files = [ + "cbor.js", + "u2futil.js", + "pkijs/*", + "get_assertion_dead_object.html", + "empty.html", +] +scheme = "https" +prefs = [ + "dom.security.featurePolicy.webidl.enabled=true", + "security.webauth.webauthn=true", + "security.webauth.webauthn_enable_softtoken=true", + "security.webauth.webauthn_enable_usbtoken=false", + "security.webauthn.ctap2=true", + "security.webauthn.enable_conditional_mediation=true", + "security.webauthn.enable_macos_passkeys=false", +] + +["test_webauthn_abort_signal.html"] +fail-if = ["xorigin"] +skip-if = [ + "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.) + "os == 'android'", # Test sets security.webauth.webauthn_enable_usbtoken to true, which isn't applicable to android +] + +["test_webauthn_attestation_conveyance.html"] +fail-if = ["xorigin"] # NotAllowedError +skip-if = [ + "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_authenticator_selection.html"] +fail-if = ["xorigin"] # NotAllowedError +skip-if = [ + "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_authenticator_transports.html"] +fail-if = ["xorigin"] # NotAllowedError +skip-if = [ + "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_crossorigin_featurepolicy.html"] +fail-if = ["xorigin"] # Cross-origin use of WebAuthn requires a feature policy. + +["test_webauthn_ctap2_omitted_credential_id.html"] +fail-if = ["xorigin"] # NotAllowedError + +["test_webauthn_get_assertion.html"] +fail-if = ["xorigin"] # NotAllowedError +skip-if = [ + "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.) + "os == 'android'", # Test sets security.webauth.webauthn_enable_usbtoken to true, which isn't applicable to android +] + +["test_webauthn_get_assertion_dead_object.html"] +skip-if = [ + "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_isplatformauthenticatoravailable.html"] + +["test_webauthn_loopback.html"] +skip-if = [ + "xorigin", # Hangs, JavaScript error: https://example.org/tests/SimpleTest/SimpleTest.js, line 76: DataCloneError: The object could not be cloned. + "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_make_credential.html"] +fail-if = ["xorigin"] # NotAllowedError +skip-if = [ + "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_no_token.html"] +skip-if = [ + "xorigin", # JavaScript error: https://example.org/tests/SimpleTest/SimpleTest.js, line 76: DataCloneError: The object could not be cloned. + "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.) + "os == 'android'", # Test disables all tokens, which is an unsupported configuration on android. +] + +["test_webauthn_sameorigin.html"] +fail-if = ["xorigin"] # NotAllowedError +skip-if = [ + "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_sameoriginwithancestors.html"] +skip-if = [ + "xorigin", # this test has its own cross-origin setup + "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 = [ + "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_webdriver_virtual_authenticator.html"] +fail-if = ["xorigin"] # Cross-origin use of WebAuthn requires a feature policy. diff --git a/dom/webauthn/tests/pkijs/LICENSE b/dom/webauthn/tests/pkijs/LICENSE new file mode 100644 index 0000000000..4f71696a7d --- /dev/null +++ b/dom/webauthn/tests/pkijs/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2014, GMO GlobalSign +Copyright (c) 2015, Peculiar Ventures +All rights reserved. + +Author 2014-2015, Yury Strozhevsky + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/dom/webauthn/tests/pkijs/README b/dom/webauthn/tests/pkijs/README new file mode 100644 index 0000000000..9213c9d438 --- /dev/null +++ b/dom/webauthn/tests/pkijs/README @@ -0,0 +1 @@ +PKIjs and ASN1js are from https://pkijs.org/ and https://asn1js.org/.
\ No newline at end of file diff --git a/dom/webauthn/tests/pkijs/asn1.js b/dom/webauthn/tests/pkijs/asn1.js new file mode 100644 index 0000000000..ddee052407 --- /dev/null +++ b/dom/webauthn/tests/pkijs/asn1.js @@ -0,0 +1,5466 @@ +/* + * Copyright (c) 2014, GMO GlobalSign + * Copyright (c) 2015, Peculiar Ventures + * All rights reserved. + * + * Author 2014-2015, Yury Strozhevsky <www.strozhevsky.com>. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + */ +( +function(in_window) +{ + //************************************************************************************** + // #region Declaration of global variables + //************************************************************************************** + // #region "org" namespace + if(typeof in_window.org === "undefined") + in_window.org = {}; + else + { + if(typeof in_window.org !== "object") + throw new Error("Name org already exists and it's not an object"); + } + // #endregion + + // #region "org.pkijs" namespace + if(typeof in_window.org.pkijs === "undefined") + in_window.org.pkijs = {}; + else + { + if(typeof in_window.org.pkijs !== "object") + throw new Error("Name org.pkijs already exists and it's not an object" + " but " + (typeof in_window.org.pkijs)); + } + // #endregion + + // #region "org.pkijs.asn1" namespace + if(typeof in_window.org.pkijs.asn1 === "undefined") + in_window.org.pkijs.asn1 = {}; + else + { + if(typeof in_window.org.pkijs.asn1 !== "object") + throw new Error("Name org.pkijs.asn1 already exists and it's not an object" + " but " + (typeof in_window.org.pkijs.asn1)); + } + // #endregion + + // #region "local" namespace + var local = {}; + // #endregion + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Aux-functions + //************************************************************************************** + function util_frombase(input_buffer, input_base) + { + /// <summary>Convert number from 2^base to 2^10</summary> + /// <param name="input_buffer" type="Uint8Array">Array of bytes representing the number to convert</param> + /// <param name="input_base" type="Number">The base of initial number</param> + + var result = 0; + + for(var i = (input_buffer.length - 1); i >= 0; i-- ) + result += input_buffer[(input_buffer.length - 1) - i] * Math.pow(2, input_base * i); + + return result; + } + //************************************************************************************** + function util_tobase(value, base, reserved) + { + /// <summary>Convert number from 2^10 to 2^base</summary> + /// <param name="value" type="Number">The number to convert</param> + /// <param name="base" type="Number">The base for 2^base</param> + /// <param name="reserved" type="Number">Pre-defined number of bytes in output array (-1 = limited by function itself)</param> + + reserved = reserved || (-1); + + var result = 0; + var biggest = Math.pow(2, base); + + for(var i = 1; i < 8; i++) + { + if(value < biggest) + { + var ret_buf; + + if( reserved < 0 ) + { + ret_buf = new ArrayBuffer(i); + result = i; + } + else + { + if(reserved < i) + return (new ArrayBuffer(0)); + + ret_buf = new ArrayBuffer(reserved); + + result = reserved; + } + + var ret_view = new Uint8Array(ret_buf); + + for(var j = ( i - 1 ); j >= 0; j-- ) + { + var basis = Math.pow(2, j * base); + + ret_view[ result - j - 1 ] = Math.floor( value / basis ); + value -= ( ret_view[ result - j - 1 ] ) * basis; + } + + return ret_buf; + } + + biggest *= Math.pow(2, base); + } + } + //************************************************************************************** + function util_encode_tc(value) + { + /// <summary>Encode integer value to "two complement" format</summary> + /// <param name="value" type="Number">Value to encode</param> + + var mod_value = (value < 0) ? (value * (-1)) : value; + var big_int = 128; + + for(var i = 1; i < 8; i++) + { + if( mod_value <= big_int ) + { + if( value < 0 ) + { + var small_int = big_int - mod_value; + + var ret_buf = util_tobase( small_int, 8, i ); + var ret_view = new Uint8Array(ret_buf); + + ret_view[ 0 ] |= 0x80; + + return ret_buf; + } + else + { + var ret_buf = util_tobase( mod_value, 8, i ); + var ret_view = new Uint8Array(ret_buf); + + if( ret_view[ 0 ] & 0x80 ) + { + var temp_buf = util_copybuf(ret_buf); + var temp_view = new Uint8Array(temp_buf); + + ret_buf = new ArrayBuffer( ret_buf.byteLength + 1 ); + ret_view = new Uint8Array(ret_buf); + + for(var k = 0; k < temp_buf.byteLength; k++) + ret_view[k + 1] = temp_view[k]; + + ret_view[0] = 0x00; + } + + return ret_buf; + } + } + + big_int *= Math.pow(2, 8); + } + + return (new ArrayBuffer(0)); + } + //************************************************************************************** + function util_decode_tc() + { + /// <summary>Decoding of "two complement" values</summary> + /// <remarks>The function must be called in scope of instance of "hex_block" class ("value_hex" and "warnings" properties must be present)</remarks> + + var buf = new Uint8Array(this.value_hex); + + if(this.value_hex.byteLength >= 2) + { + var condition_1 = (buf[0] == 0xFF) && (buf[1] & 0x80); + var condition_2 = (buf[0] == 0x00) && ((buf[1] & 0x80) == 0x00); + + if(condition_1 || condition_2) + this.warnings.push("Needlessly long format"); + } + + // #region Create big part of the integer + var big_int_buffer = new ArrayBuffer(this.value_hex.byteLength); + var big_int_view = new Uint8Array(big_int_buffer); + for(var i = 0; i < this.value_hex.byteLength; i++) + big_int_view[i] = 0; + + big_int_view[0] = (buf[0] & 0x80); // mask only the biggest bit + + var big_int = util_frombase(big_int_view, 8); + // #endregion + + // #region Create small part of the integer + var small_int_buffer = new ArrayBuffer(this.value_hex.byteLength); + var small_int_view = new Uint8Array(small_int_buffer); + for(var j = 0; j < this.value_hex.byteLength; j++) + small_int_view[j] = buf[j]; + + small_int_view[0] &= 0x7F; // mask biggest bit + + var small_int = util_frombase(small_int_view, 8); + // #endregion + + return (small_int - big_int); + } + //************************************************************************************** + function util_copybuf(input_buffer) + { + /// <summary>Creating a copy of input ArrayBuffer</summary> + /// <param name="input_buffer" type="ArrayBuffer">ArrayBuffer for coping</param> + + if(check_buffer_params(input_buffer, 0, input_buffer.byteLength) === false) + return (new ArrayBuffer(0)); + + var input_view = new Uint8Array(input_buffer); + + var ret_buf = new ArrayBuffer(input_buffer.byteLength); + var ret_view = new Uint8Array(ret_buf); + + for(var i = 0; i < input_buffer.byteLength; i++) + ret_view[i] = input_view[i]; + + return ret_buf; + } + //************************************************************************************** + function util_copybuf_offset(input_buffer, input_offset, input_length) + { + /// <summary>Creating a copy of input ArrayBuffer</summary> + /// <param name="input_buffer" type="ArrayBuffer">ArrayBuffer for coping</param> + + if(check_buffer_params(input_buffer, input_offset, input_length) === false) + return (new ArrayBuffer(0)); + + var input_view = new Uint8Array(input_buffer, input_offset, input_length); + + var ret_buf = new ArrayBuffer(input_length); + var ret_view = new Uint8Array(ret_buf); + + for(var i = 0; i < input_length; i++) + ret_view[i] = input_view[i]; + + return ret_buf; + } + //************************************************************************************** + function util_concatbuf(input_buf1, input_buf2) + { + /// <summary>Concatenate two ArrayBuffers</summary> + /// <param name="input_buf1" type="ArrayBuffer">First ArrayBuffer (first part of concatenated array)</param> + /// <param name="input_buf2" type="ArrayBuffer">Second ArrayBuffer (second part of concatenated array)</param> + + var input_view1 = new Uint8Array(input_buf1); + var input_view2 = new Uint8Array(input_buf2); + + var ret_buf = new ArrayBuffer(input_buf1.byteLength + input_buf2.byteLength); + var ret_view = new Uint8Array(ret_buf); + + for(var i = 0; i < input_buf1.byteLength; i++) + ret_view[i] = input_view1[i]; + + for(var j = 0; j < input_buf2.byteLength; j++) + ret_view[input_buf1.byteLength + j] = input_view2[j]; + + return ret_buf; + } + //************************************************************************************** + function check_buffer_params(input_buffer, input_offset, input_length) + { + if((input_buffer instanceof ArrayBuffer) === false) + { + this.error = "Wrong parameter: input_buffer must be \"ArrayBuffer\""; + return false; + } + + if(input_buffer.byteLength === 0) + { + this.error = "Wrong parameter: input_buffer has zero length"; + return false; + } + + if(input_offset < 0) + { + this.error = "Wrong parameter: input_offset less than zero"; + return false; + } + + if(input_length < 0) + { + this.error = "Wrong parameter: input_length less than zero"; + return false; + } + + if((input_buffer.byteLength - input_offset - input_length) < 0) + { + this.error = "End of input reached before message was fully decoded (inconsistent offset and length values)"; + return false; + } + + return true; + } + //************************************************************************************** + function to_hex_codes(input_buffer, input_offset, input_lenght) + { + if(check_buffer_params(input_buffer, input_offset, input_lenght) === false) + return ""; + + var result = ""; + + var int_buffer = new Uint8Array(input_buffer, input_offset, input_lenght); + + for(var i = 0; i < int_buffer.length; i++) + { + var str = int_buffer[i].toString(16).toUpperCase(); + result = result + ((str.length === 1) ? " 0" : " ") + str; + } + + return result; + } + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of base block class + //************************************************************************************** + local.base_block = + function() + { + /// <summary>General class of all ASN.1 blocks</summary> + + if(arguments[0] instanceof Object) + { + this.block_length = in_window.org.pkijs.getValue(arguments[0], "block_length", 0); + this.error = in_window.org.pkijs.getValue(arguments[0], "error", new String()); + this.warnings = in_window.org.pkijs.getValue(arguments[0], "warnings", new Array()); + if("value_before_decode" in arguments[0]) + this.value_before_decode = util_copybuf(arguments[0].value_before_decode); + else + this.value_before_decode = new ArrayBuffer(0); + } + else + { + this.block_length = 0; + this.error = new String(); + this.warnings = new Array(); + /// <field>Copy of the value of incoming ArrayBuffer done before decoding</field> + this.value_before_decode = new ArrayBuffer(0); + } + }; + //************************************************************************************** + local.base_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "base_block"; + }; + //************************************************************************************** + local.base_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + return { + block_name: local.base_block.prototype.block_name.call(this), + block_length: this.block_length, + error: this.error, + warnings: this.warnings, + value_before_decode: in_window.org.pkijs.bufferToHexCodes(this.value_before_decode, 0, this.value_before_decode.byteLength) + }; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of hex block class + //************************************************************************************** + local.hex_block = + function() + { + /// <summary>Descendant of "base_block" with internal ArrayBuffer. Need to have it in case it is not possible to store ASN.1 value in native formats</summary> + + local.base_block.call(this, arguments[0]); + + if(arguments[0] instanceof Object) + { + this.is_hex_only = in_window.org.pkijs.getValue(arguments[0], "is_hex_only", false); + if("value_hex" in arguments[0]) + this.value_hex = util_copybuf(arguments[0].value_hex); + else + this.value_hex = new ArrayBuffer(0); + } + else + { + this.is_hex_only = false; + this.value_hex = new ArrayBuffer(0); + } + }; + //************************************************************************************** + local.hex_block.prototype = new local.base_block(); + local.hex_block.constructor = local.hex_block; + //************************************************************************************** + local.hex_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "hex_block"; + }; + //************************************************************************************** + local.hex_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + // #region Basic check for parameters + if(check_buffer_params.call(this, input_buffer, input_offset, input_length) === false) + return (-1); + // #endregion + + // #region Getting Uint8Array from ArrayBuffer + var int_buffer = new Uint8Array(input_buffer, input_offset, input_length); + // #endregion + + // #region Initial checks + if(int_buffer.length == 0) + { + this.warnings.push("Zero buffer length"); + return input_offset; + } + // #endregion + + // #region Copy input buffer to internal buffer + this.value_hex = new ArrayBuffer(input_length); + var view = new Uint8Array(this.value_hex); + + for(var i = 0; i < int_buffer.length; i++) + view[i] = int_buffer[i]; + // #endregion + + this.block_length = input_length; + + return (input_offset + input_length); + }; + //************************************************************************************** + local.hex_block.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + if(typeof size_only === "undefined") + size_only = false; + + if(this.is_hex_only !== true) + { + this.error = "Flag \"is_hex_only\" is not set, abort"; + return (new ArrayBuffer(0)); + } + + var ret_buf = new ArrayBuffer(this.value_hex.byteLength); + + if(size_only === true) + return ret_buf; + + var ret_view = new Uint8Array(ret_buf); + var cur_view = new Uint8Array(this.value_hex); + + for(var i = 0; i < cur_view.length; i++) + ret_view[i] = cur_view[i]; + + return ret_buf; + }; + //************************************************************************************** + local.hex_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.base_block.prototype.toJSON.call(this); + + _object.block_name = local.hex_block.prototype.block_name.call(this); + _object.is_hex_only = this.is_hex_only; + _object.value_hex = in_window.org.pkijs.bufferToHexCodes(this.value_hex, 0, this.value_hex.byteLength); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of identification block class + //************************************************************************************** + local.identification_block = + function() + { + /// <summary>Base class of ASN.1 "identification block"</summary> + + local.hex_block.call(this, arguments[0]); + + this.tag_class = (-1); + this.tag_number = (-1); + this.is_constructed = false; + + if(arguments[0] instanceof Object) + { + if("id_block" in arguments[0]) + { + // #region Properties from hex_block class + this.is_hex_only = in_window.org.pkijs.getValue(arguments[0].id_block, "is_hex_only", false); + this.value_hex = in_window.org.pkijs.getValue(arguments[0].id_block, "value_hex", new ArrayBuffer(0)); + // #endregion + + this.tag_class = in_window.org.pkijs.getValue(arguments[0].id_block, "tag_class", (-1)); + this.tag_number = in_window.org.pkijs.getValue(arguments[0].id_block, "tag_number", (-1)); + this.is_constructed = in_window.org.pkijs.getValue(arguments[0].id_block, "is_constructed", false); + } + } + }; + //************************************************************************************** + local.identification_block.prototype = new local.hex_block(); + local.identification_block.constructor = local.identification_block; + //************************************************************************************** + local.identification_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "identification_block"; + }; + //************************************************************************************** + local.identification_block.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + if(typeof size_only === "undefined") + size_only = false; + + var first_octet = 0; + + switch(this.tag_class) + { + case 1: + first_octet |= 0x00; // UNIVERSAL + break; + case 2: + first_octet |= 0x40; // APPLICATION + break; + case 3: + first_octet |= 0x80; // CONTEXT-SPECIFIC + break; + case 4: + first_octet |= 0xC0; // PRIVATE + break; + default: + this.error = "Unknown tag class"; + return (new ArrayBuffer(0)); + } + + if(this.is_constructed) + first_octet |= 0x20; + + if((this.tag_number < 31) && (!this.is_hex_only)) + { + var ret_buf = new ArrayBuffer(1); + var ret_view = new Uint8Array(ret_buf); + + if(!size_only) + { + var number = this.tag_number; + number &= 0x1F; + first_octet |= number; + + ret_view[0] = first_octet; + } + + return ret_buf; + } + else + { + if(this.is_hex_only === false) + { + var encoded_buf = util_tobase(this.tag_number, 7); + var encoded_view = new Uint8Array(encoded_buf); + var size = encoded_buf.byteLength; + + var ret_buf = new ArrayBuffer(size + 1); + var ret_view = new Uint8Array(ret_buf); + + ret_view[0] = (first_octet | 0x1F); + + if(!size_only) + { + for(var i = 0; i < (size - 1) ; i++) + ret_view[i + 1] = encoded_view[i] | 0x80; + + ret_view[size] = encoded_view[size - 1]; + } + + return ret_buf; + } + else + { + var ret_buf = new ArrayBuffer(this.value_hex.byteLength + 1); + var ret_view = new Uint8Array(ret_buf); + + ret_view[0] = (first_octet | 0x1F); + + if(size_only === false) + { + var cur_view = new Uint8Array(this.value_hex); + + for(var i = 0; i < (cur_view.length - 1); i++) + ret_view[i + 1] = cur_view[i] | 0x80; + + ret_view[this.value_hex.byteLength] = cur_view[cur_view.length - 1]; + } + + return ret_buf; + } + } + }; + //************************************************************************************** + local.identification_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + // #region Basic check for parameters + if(check_buffer_params.call(this, input_buffer, input_offset, input_length) === false) + return (-1); + // #endregion + + // #region Getting Uint8Array from ArrayBuffer + var int_buffer = new Uint8Array(input_buffer, input_offset, input_length); + // #endregion + + // #region Initial checks + if(int_buffer.length == 0) + { + this.error = "Zero buffer length"; + return (-1); + } + // #endregion + + // #region Find tag class + var tag_class_mask = int_buffer[0] & 0xC0; + + switch(tag_class_mask) + { + case 0x00: + this.tag_class = (1); // UNIVERSAL + break; + case 0x40: + this.tag_class = (2); // APPLICATION + break; + case 0x80: + this.tag_class = (3); // CONTEXT-SPECIFIC + break; + case 0xC0: + this.tag_class = (4); // PRIVATE + break; + default: + this.error = "Unknown tag class"; + return ( -1 ); + } + // #endregion + + // #region Find it's constructed or not + this.is_constructed = (int_buffer[0] & 0x20) == 0x20; + // #endregion + + // #region Find tag number + this.is_hex_only = false; + + var tag_number_mask = int_buffer[0] & 0x1F; + + // #region Simple case (tag number < 31) + if(tag_number_mask != 0x1F) + { + this.tag_number = (tag_number_mask); + this.block_length = 1; + } + // #endregion + // #region Tag number bigger or equal to 31 + else + { + var count = 1; + + this.value_hex = new ArrayBuffer(255); + var tag_number_buffer_max_length = 255; + var int_tag_number_buffer = new Uint8Array(this.value_hex); + + while(int_buffer[count] & 0x80) + { + int_tag_number_buffer[count - 1] = int_buffer[count] & 0x7F; + count++; + + if(count >= int_buffer.length) + { + this.error = "End of input reached before message was fully decoded"; + return (-1); + } + + // #region In case if tag number length is greater than 255 bytes (rare but possible case) + if(count == tag_number_buffer_max_length) + { + tag_number_buffer_max_length += 255; + + var temp_buffer = new ArrayBuffer(tag_number_buffer_max_length); + var temp_buffer_view = new Uint8Array(temp_buffer); + + for(var i = 0; i < int_tag_number_buffer.length; i++) + temp_buffer_view[i] = int_tag_number_buffer[i]; + + this.value_hex = new ArrayBuffer(tag_number_buffer_max_length); + int_tag_number_buffer = new Uint8Array(this.value_hex); + } + // #endregion + } + + this.block_length = (count + 1); + int_tag_number_buffer[count - 1] = int_buffer[count] & 0x7F; // Write last byte to buffer + + // #region Cut buffer + var temp_buffer = new ArrayBuffer(count); + var temp_buffer_view = new Uint8Array(temp_buffer); + for(var i = 0; i < count; i++) + temp_buffer_view[i] = int_tag_number_buffer[i]; + + this.value_hex = new ArrayBuffer(count); + int_tag_number_buffer = new Uint8Array(this.value_hex); + int_tag_number_buffer.set(temp_buffer_view); + // #endregion + + // #region Try to convert long tag number to short form + if(this.block_length <= 9) + this.tag_number = util_frombase(int_tag_number_buffer, 7); + else + { + this.is_hex_only = true; + this.warnings.push("Tag too long, represented as hex-coded"); + } + // #endregion + } + // #endregion + // #endregion + + // #region Check if constructed encoding was using for primitive type + if(((this.tag_class == 1)) && + (this.is_constructed)) + { + switch(this.tag_number) + { + case 1: // BOOLEAN + case 2: // REAL + case 5: // NULL + case 6: // OBJECT IDENTIFIER + case 9: // REAL + case 14: // TIME + case 23: + case 24: + case 31: + case 32: + case 33: + case 34: + this.error = "Constructed encoding used for primitive type"; + return (-1); + default: + ; + } + } + // #endregion + + return ( input_offset + this.block_length ); // Return current offset in input buffer + }; + //************************************************************************************** + local.identification_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.hex_block.prototype.toJSON.call(this); + + _object.block_name = local.identification_block.prototype.block_name.call(this); + _object.tag_class = this.tag_class; + _object.tag_number = this.tag_number; + _object.is_constructed = this.is_constructed; + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of length block class + //************************************************************************************** + local.length_block = + function() + { + /// <summary>Base class of ASN.1 "length block"</summary> + + local.base_block.call(this, arguments[0]); + + this.is_indefinite_form = false; + this.long_form_used = false; + this.length = (0); + + if(arguments[0] instanceof Object) + { + if("len_block" in arguments[0]) + { + this.is_indefinite_form = in_window.org.pkijs.getValue(arguments[0].len_block, "is_indefinite_form", false); + this.long_form_used = in_window.org.pkijs.getValue(arguments[0].len_block, "long_form_used", false); + this.length = in_window.org.pkijs.getValue(arguments[0].len_block, "length", 0); + } + } + }; + //************************************************************************************** + local.length_block.prototype = new local.base_block(); + local.length_block.constructor = local.length_block; + //************************************************************************************** + local.length_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "length_block"; + }; + //************************************************************************************** + local.length_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + // #region Basic check for parameters + if(check_buffer_params.call(this, input_buffer, input_offset, input_length) === false) + return (-1); + // #endregion + + // #region Getting Uint8Array from ArrayBuffer + var int_buffer = new Uint8Array(input_buffer, input_offset, input_length); + // #endregion + + // #region Initial checks + if(int_buffer.length == 0) + { + this.error = "Zero buffer length"; + return (-1); + } + + if(int_buffer[0] == 0xFF) + { + this.error = "Length block 0xFF is reserved by standard"; + return (-1); + } + // #endregion + + // #region Check for length form type + this.is_indefinite_form = int_buffer[0] == 0x80; + // #endregion + + // #region Stop working in case of indefinite length form + if(this.is_indefinite_form == true) + { + this.block_length = 1; + return (input_offset + this.block_length); + } + // #endregion + + // #region Check is long form of length encoding using + this.long_form_used = !!(int_buffer[0] & 0x80); + // #endregion + + // #region Stop working in case of short form of length value + if(this.long_form_used == false) + { + this.length = (int_buffer[0]); + this.block_length = 1; + return (input_offset + this.block_length); + } + // #endregion + + // #region Calculate length value in case of long form + var count = int_buffer[0] & 0x7F; + + if(count > 8) // Too big length value + { + this.error = "Too big integer"; + return (-1); + } + + if((count + 1) > int_buffer.length) + { + this.error = "End of input reached before message was fully decoded"; + return (-1); + } + + var length_buffer_view = new Uint8Array(count); + + for(var i = 0; i < count; i++) + length_buffer_view[i] = int_buffer[i + 1]; + + if(length_buffer_view[count - 1] == 0x00) + this.warnings.push("Needlessly long encoded length"); + + this.length = util_frombase(length_buffer_view, 8); + + if(this.long_form_used && (this.length <= 127)) + this.warnings.push("Unneccesary usage of long length form"); + + this.block_length = count + 1; + // #endregion + + return (input_offset + this.block_length); // Return current offset in input buffer + }; + //************************************************************************************** + local.length_block.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + if(typeof size_only === "undefined") + size_only = false; + + if(this.length > 127) + this.long_form_used = true; + + if(this.is_indefinite_form) + { + var ret_buf = new ArrayBuffer(1); + + if(size_only === false) + { + var ret_view = new Uint8Array(ret_buf); + ret_view[0] = 0x80; + } + + return ret_buf; + } + + if(this.long_form_used === true) + { + var encoded_buf = util_tobase(this.length, 8); + + if(encoded_buf.byteLength > 127) + { + this.error = "Too big length"; + return (new ArrayBuffer(0)); + } + + var ret_buf = new ArrayBuffer(encoded_buf.byteLength + 1); + + if(size_only === true) + return ret_buf; + + var encoded_view = new Uint8Array(encoded_buf); + var ret_view = new Uint8Array(ret_buf); + + ret_view[0] = encoded_buf.byteLength | 0x80; + + for(var i = 0; i < encoded_buf.byteLength; i++) + ret_view[i + 1] = encoded_view[i]; + + return ret_buf; + } + else + { + var ret_buf = new ArrayBuffer(1); + + if(size_only === false) + { + var ret_view = new Uint8Array(ret_buf); + + ret_view[0] = this.length; + } + + return ret_buf; + } + + return (new ArrayBuffer(0)); + }; + //************************************************************************************** + local.length_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.base_block.prototype.toJSON.call(this); + + _object.block_name = local.length_block.prototype.block_name.call(this); + _object.is_indefinite_form = this.is_indefinite_form; + _object.long_form_used = this.long_form_used; + _object.length = this.length; + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of value block class + //************************************************************************************** + local.value_block = + function() + { + /// <summary>Generic class of ASN.1 "value block"</summary> + local.base_block.call(this, arguments[0]); + }; + //************************************************************************************** + local.value_block.prototype = new local.base_block(); + local.value_block.constructor = local.value_block; + //************************************************************************************** + local.value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "value_block"; + }; + //************************************************************************************** + local.value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.base_block.prototype.toJSON.call(this); + + _object.block_name = local.value_block.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of basic ASN.1 block class + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_block = + function() + { + /// <summary>Base class of ASN.1 block (identification block + length block + value block)</summary> + + local.base_block.call(this, arguments[0]); + + if(arguments[0] instanceof Object) + { + this.name = in_window.org.pkijs.getValue(arguments[0], "name", ""); + this.optional = in_window.org.pkijs.getValue(arguments[0], "optional", false); + + if("primitive_schema" in arguments[0]) + this.primitive_schema = arguments[0].primitive_schema; + } + + this.id_block = new local.identification_block(arguments[0]); + this.len_block = new local.length_block(arguments[0]); + this.value_block = new local.value_block(arguments[0]); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_block.prototype = new local.base_block(); + in_window.org.pkijs.asn1.ASN1_block.constructor = in_window.org.pkijs.asn1.ASN1_block; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "ASN1_block"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + var result_offset = this.value_block.fromBER(input_buffer, input_offset, (this.len_block.is_indefinite_form == true) ? input_length : this.len_block.length); + if(result_offset == (-1)) + { + this.error = this.value_block.error; + return result_offset; + } + + if(this.id_block.error.length == 0) + this.block_length += this.id_block.block_length; + + if(this.len_block.error.length == 0) + this.block_length += this.len_block.block_length; + + if(this.value_block.error.length == 0) + this.block_length += this.value_block.block_length; + + return result_offset; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_block.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + if(typeof size_only === "undefined") + size_only = false; + + var ret_buf; + + var id_block_buf = this.id_block.toBER(size_only); + var value_block_size_buf = this.value_block.toBER(true); + + this.len_block.length = value_block_size_buf.byteLength; + var len_block_buf = this.len_block.toBER(size_only); + + ret_buf = util_concatbuf(id_block_buf, len_block_buf); + + var value_block_buf; + + if(size_only === false) + value_block_buf = this.value_block.toBER(size_only); + else + value_block_buf = new ArrayBuffer(this.len_block.length); + + ret_buf = util_concatbuf(ret_buf, value_block_buf); + + if(this.len_block.is_indefinite_form === true) + { + var indef_buf = new ArrayBuffer(2); + + if(size_only === false) + { + var indef_view = new Uint8Array(indef_buf); + + indef_view[0] = 0x00; + indef_view[1] = 0x00; + } + + ret_buf = util_concatbuf(ret_buf, indef_buf); + } + + return ret_buf; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.base_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.ASN1_block.prototype.block_name.call(this); + _object.id_block = this.id_block.toJSON(); + _object.len_block = this.len_block.toJSON(); + _object.value_block = this.value_block.toJSON(); + + if("name" in this) + _object.name = this.name; + if("optional" in this) + _object.optional = this.optional; + if("primitive_schema" in this) + _object.primitive_schema = this.primitive_schema.toJSON(); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of basic block for all PRIMITIVE types + //************************************************************************************** + local.ASN1_PRIMITIVE_value_block = + function() + { + /// <summary>Base class of ASN.1 value block for primitive values (non-constructive encoding)</summary> + + local.value_block.call(this, arguments[0]); + + if(arguments[0] instanceof Object) + { + // #region Variables from "hex_block" class + if("value_hex" in arguments[0]) + this.value_hex = util_copybuf(arguments[0].value_hex); + else + this.value_hex = new ArrayBuffer(0); + + this.is_hex_only = in_window.org.pkijs.getValue(arguments[0], "is_hex_only", true); + // #endregion + } + else + { + // #region Variables from "hex_block" class + this.value_hex = new ArrayBuffer(0); + this.is_hex_only = true; + // #endregion + } + }; + //************************************************************************************** + local.ASN1_PRIMITIVE_value_block.prototype = new local.value_block(); + local.ASN1_PRIMITIVE_value_block.constructor = local.ASN1_PRIMITIVE_value_block; + //************************************************************************************** + local.ASN1_PRIMITIVE_value_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + // #region Basic check for parameters + if(check_buffer_params.call(this, input_buffer, input_offset, input_length) === false) + return (-1); + // #endregion + + // #region Getting Uint8Array from ArrayBuffer + var int_buffer = new Uint8Array(input_buffer, input_offset, input_length); + // #endregion + + // #region Initial checks + if(int_buffer.length == 0) + { + this.warnings.push("Zero buffer length"); + return input_offset; + } + // #endregion + + // #region Copy input buffer into internal buffer + this.value_hex = new ArrayBuffer(int_buffer.length); + var value_hex_view = new Uint8Array(this.value_hex); + + for(var i = 0; i < int_buffer.length; i++) + value_hex_view[i] = int_buffer[i]; + // #endregion + + this.block_length = input_length; + + return (input_offset + input_length); + }; + //************************************************************************************** + local.ASN1_PRIMITIVE_value_block.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + return util_copybuf(this.value_hex); + }; + //************************************************************************************** + local.ASN1_PRIMITIVE_value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "ASN1_PRIMITIVE_value_block"; + }; + //************************************************************************************** + local.ASN1_PRIMITIVE_value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.value_block.prototype.toJSON.call(this); + + _object.block_name = local.ASN1_PRIMITIVE_value_block.prototype.block_name.call(this); + _object.value_hex = in_window.org.pkijs.bufferToHexCodes(this.value_hex, 0, this.value_hex.byteLength); + _object.is_hex_only = this.is_hex_only; + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_PRIMITIVE = + function() + { + /// <summary>Base class of ASN.1 block for primitive values (non-constructive encoding)</summary> + + in_window.org.pkijs.asn1.ASN1_block.call(this, arguments[0]); + + this.id_block.is_constructed = false; + this.value_block = new local.ASN1_PRIMITIVE_value_block(arguments[0]); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_PRIMITIVE.prototype = new in_window.org.pkijs.asn1.ASN1_block(); + in_window.org.pkijs.asn1.ASN1_PRIMITIVE.constructor = in_window.org.pkijs.asn1.ASN1_PRIMITIVE; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_PRIMITIVE.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "PRIMITIVE"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_PRIMITIVE.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.ASN1_PRIMITIVE.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of basic block for all CONSTRUCTED types + //************************************************************************************** + local.ASN1_CONSTRUCTED_value_block = + function() + { + /// <summary>Base class of ASN.1 value block for constructive values (constructive encoding)</summary> + + local.value_block.call(this, arguments[0]); + + if(arguments[0] instanceof Object) + { + this.value = in_window.org.pkijs.getValue(arguments[0], "value", new Array()); + this.is_indefinite_form = in_window.org.pkijs.getValue(arguments[0], "is_indefinite_form", false); + } + else + { + this.value = new Array(); + this.is_indefinite_form = false; + } + }; + //************************************************************************************** + local.ASN1_CONSTRUCTED_value_block.prototype = new local.value_block(); + local.ASN1_CONSTRUCTED_value_block.constructor = local.ASN1_CONSTRUCTED_value_block; + //************************************************************************************** + local.ASN1_CONSTRUCTED_value_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + // #region Store initial offset and length + var initial_offset = input_offset; + var initial_length = input_length; + // #endregion + + // #region Basic check for parameters + if(check_buffer_params.call(this, input_buffer, input_offset, input_length) === false) + return (-1); + // #endregion + + // #region Getting Uint8Array from ArrayBuffer + var int_buffer = new Uint8Array(input_buffer, input_offset, input_length); + // #endregion + + // #region Initial checks + if(int_buffer.length == 0) + { + this.warnings.push("Zero buffer length"); + return input_offset; + } + // #endregion + + // #region Aux function + function check_len(_indefinite_length, _length) + { + if(_indefinite_length == true) + return 1; + + return _length; + } + // #endregion + + var current_offset = input_offset; + + while(check_len(this.is_indefinite_form, input_length) > 0) + { + var return_object = fromBER_raw(input_buffer, current_offset, input_length); + if(return_object.offset == (-1)) + { + this.error = return_object.result.error; + this.warnings.concat(return_object.result.warnings); + return (-1); + } + + current_offset = return_object.offset; + + this.block_length += return_object.result.block_length; + input_length -= return_object.result.block_length; + + this.value.push(return_object.result); + + if((this.is_indefinite_form == true) && (return_object.result.block_name() == in_window.org.pkijs.asn1.EOC.prototype.block_name())) + break; + } + + if(this.is_indefinite_form == true) + { + if(this.value[this.value.length - 1].block_name() == in_window.org.pkijs.asn1.EOC.prototype.block_name()) + this.value.pop(); + else + this.warnings.push("No EOC block encoded"); + } + + // #region Copy "input_buffer" to "value_before_decode" + this.value_before_decode = util_copybuf_offset(input_buffer, initial_offset, initial_length); + // #endregion + + return current_offset; + }; + //************************************************************************************** + local.ASN1_CONSTRUCTED_value_block.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + if(typeof size_only === "undefined") + size_only = false; + + var ret_buf = new ArrayBuffer(0); + + for(var i = 0; i < this.value.length; i++) + { + var value_buf = this.value[i].toBER(size_only); + ret_buf = util_concatbuf(ret_buf, value_buf); + } + + return ret_buf; + }; + //************************************************************************************** + local.ASN1_CONSTRUCTED_value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "ASN1_CONSTRUCTED_value_block"; + }; + //************************************************************************************** + local.ASN1_CONSTRUCTED_value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.value_block.prototype.toJSON.call(this); + + _object.block_name = local.ASN1_CONSTRUCTED_value_block.prototype.block_name.call(this); + _object.is_indefinite_form = this.is_indefinite_form; + _object.value = new Array(); + for(var i = 0; i < this.value.length; i++) + _object.value.push(this.value[i].toJSON()); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_CONSTRUCTED = + function() + { + /// <summary>Base class of ASN.1 block for constructive values (constructive encoding)</summary> + + in_window.org.pkijs.asn1.ASN1_block.call(this, arguments[0]); + + this.id_block.is_constructed = true; + this.value_block = new local.ASN1_CONSTRUCTED_value_block(arguments[0]); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_CONSTRUCTED.prototype = new in_window.org.pkijs.asn1.ASN1_block(); + in_window.org.pkijs.asn1.ASN1_CONSTRUCTED.constructor = in_window.org.pkijs.asn1.ASN1_CONSTRUCTED; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_CONSTRUCTED.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "CONSTRUCTED"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_CONSTRUCTED.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + this.value_block.is_indefinite_form = this.len_block.is_indefinite_form; + + var result_offset = this.value_block.fromBER(input_buffer, input_offset, (this.len_block.is_indefinite_form == true) ? input_length : this.len_block.length); + if(result_offset == (-1)) + { + this.error = this.value_block.error; + return result_offset; + } + + if(this.id_block.error.length == 0) + this.block_length += this.id_block.block_length; + + if(this.len_block.error.length == 0) + this.block_length += this.len_block.block_length; + + if(this.value_block.error.length == 0) + this.block_length += this.value_block.block_length; + + return result_offset; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.ASN1_CONSTRUCTED.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.ASN1_CONSTRUCTED.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of ASN.1 EOC type class + //************************************************************************************** + local.EOC_value_block = + function() + { + local.value_block.call(this, arguments[0]); + }; + //************************************************************************************** + local.EOC_value_block.prototype = new local.value_block(); + local.EOC_value_block.constructor = local.EOC_value_block; + //************************************************************************************** + local.EOC_value_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + // #region There is no "value block" for EOC type and we need to return the same offset + return input_offset; + // #endregion + }; + //************************************************************************************** + local.EOC_value_block.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + return (new ArrayBuffer(0)); + }; + //************************************************************************************** + local.EOC_value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "EOC_value_block"; + }; + //************************************************************************************** + local.EOC_value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.value_block.prototype.toJSON.call(this); + + _object.block_name = local.EOC_value_block.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.EOC = + function() + { + in_window.org.pkijs.asn1.ASN1_block.call(this, arguments[0]); + + this.value_block = new local.EOC_value_block(); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 0; // EOC + }; + //************************************************************************************** + in_window.org.pkijs.asn1.EOC.prototype = new in_window.org.pkijs.asn1.ASN1_block(); + in_window.org.pkijs.asn1.EOC.constructor = local.EOC_value_block; + //************************************************************************************** + in_window.org.pkijs.asn1.EOC.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "END_OF_CONTENT"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.EOC.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.EOC.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of ASN.1 BOOLEAN type class + //************************************************************************************** + local.BOOLEAN_value_block = + function() + { + local.value_block.call(this, arguments[0]); + + if(arguments[0] instanceof Object) + { + this.value = in_window.org.pkijs.getValue(arguments[0], "value", false); + + // #region Variables from hex_block class + this.is_hex_only = in_window.org.pkijs.getValue(arguments[0], "is_hex_only", false); + if("value_hex" in arguments[0]) + this.value_hex = util_copybuf(arguments[0].value_hex); + else + { + this.value_hex = new ArrayBuffer(1); + if(this.value === true) + { + var view = new Uint8Array(this.value_hex); + view[0] = 0xFF; + } + } + // #endregion + } + else + { + this.value = false; + + // #region Variables from hex_block class + this.is_hex_only = false; + this.value_hex = new ArrayBuffer(1); + // #endregion + } + }; + //************************************************************************************** + local.BOOLEAN_value_block.prototype = new local.value_block(); + local.BOOLEAN_value_block.constructor = local.BOOLEAN_value_block; + //************************************************************************************** + local.BOOLEAN_value_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + // #region Basic check for parameters + if(check_buffer_params.call(this, input_buffer, input_offset, input_length) === false) + return (-1); + // #endregion + + // #region Getting Uint8Array from ArrayBuffer + var int_buffer = new Uint8Array(input_buffer, input_offset, input_length); + // #endregion + + if(input_length > 1) + this.warnings.push("BOOLEAN value encoded in more then 1 octet"); + + this.value = int_buffer[0] != 0x00; + + this.is_hex_only = true; + + // #region Copy input buffer to internal array + this.value_hex = new ArrayBuffer(int_buffer.length); + var view = new Uint8Array(this.value_hex); + + for(var i = 0; i < int_buffer.length; i++) + view[i] = int_buffer[i]; + // #endregion + + this.block_length = input_length; + + return (input_offset + input_length); + }; + //************************************************************************************** + local.BOOLEAN_value_block.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + if(typeof size_only === "undefined") + size_only = false; + + return this.value_hex; + }; + //************************************************************************************** + local.BOOLEAN_value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "BOOLEAN_value_block"; + }; + //************************************************************************************** + local.BOOLEAN_value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.value_block.prototype.toJSON.call(this); + + _object.block_name = local.BOOLEAN_value_block.prototype.block_name.call(this); + _object.value = this.value; + _object.is_hex_only = this.is_hex_only; + _object.value_hex = in_window.org.pkijs.bufferToHexCodes(this.value_hex, 0, this.value_hex.byteLength); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.BOOLEAN = + function() + { + in_window.org.pkijs.asn1.ASN1_block.call(this, arguments[0]); + + this.value_block = new local.BOOLEAN_value_block(arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 1; // BOOLEAN + }; + //************************************************************************************** + in_window.org.pkijs.asn1.BOOLEAN.prototype = new in_window.org.pkijs.asn1.ASN1_block(); + in_window.org.pkijs.asn1.BOOLEAN.constructor = local.BOOLEAN_value_block; + //************************************************************************************** + in_window.org.pkijs.asn1.BOOLEAN.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "BOOLEAN"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.BOOLEAN.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.BOOLEAN.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of ASN.1 SEQUENCE and SET type classes + //************************************************************************************** + in_window.org.pkijs.asn1.SEQUENCE = + function() + { + in_window.org.pkijs.asn1.ASN1_CONSTRUCTED.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 16; // SEQUENCE + }; + //************************************************************************************** + in_window.org.pkijs.asn1.SEQUENCE.prototype = new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED(); + in_window.org.pkijs.asn1.SEQUENCE.constructor = in_window.org.pkijs.asn1.SEQUENCE; + //************************************************************************************** + in_window.org.pkijs.asn1.SEQUENCE.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "SEQUENCE"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.SEQUENCE.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_CONSTRUCTED.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.SEQUENCE.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.SET = + function() + { + in_window.org.pkijs.asn1.ASN1_CONSTRUCTED.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 17; // SET + }; + //************************************************************************************** + in_window.org.pkijs.asn1.SET.prototype = new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED(); + in_window.org.pkijs.asn1.SET.constructor = in_window.org.pkijs.asn1.SET; + //************************************************************************************** + in_window.org.pkijs.asn1.SET.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "SET"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.SET.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_CONSTRUCTED.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.SET.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of ASN.1 NULL type class + //************************************************************************************** + in_window.org.pkijs.asn1.NULL = + function() + { + in_window.org.pkijs.asn1.ASN1_block.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 5; // NULL + }; + //************************************************************************************** + in_window.org.pkijs.asn1.NULL.prototype = new in_window.org.pkijs.asn1.ASN1_block(); + in_window.org.pkijs.asn1.NULL.constructor = in_window.org.pkijs.asn1.NULL; + //************************************************************************************** + in_window.org.pkijs.asn1.NULL.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "NULL"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.NULL.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + if(this.len_block.length > 0) + this.warnings.push("Non-zero length of value block for NULL type"); + + if(this.id_block.error.length === 0) + this.block_length += this.id_block.block_length; + + if(this.len_block.error.length === 0) + this.block_length += this.len_block.block_length; + + this.block_length += input_length; + + return (input_offset + input_length); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.NULL.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + if(typeof size_only === "undefined") + size_only = false; + + var ret_buf = new ArrayBuffer(2); + + if(size_only === true) + return ret_buf; + + var ret_view = new Uint8Array(ret_buf); + ret_view[0] = 0x05; + ret_view[1] = 0x00; + + return ret_buf; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.NULL.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.NULL.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of ASN.1 OCTETSTRING type class + //************************************************************************************** + local.OCTETSTRING_value_block = + function() + { + /// <param name="input_value_hex" type="ArrayBuffer"></param> + /// <param name="input_value" type="Array"></param> + /// <param name="input_constructed" type="Boolean"></param> + /// <remarks>Value for the OCTETSTRING may be as hex, as well as a constructed value.</remarks> + /// <remarks>Constructed values consists of other OCTETSTRINGs</remarks> + + local.ASN1_CONSTRUCTED_value_block.call(this, arguments[0]); + + if(arguments[0] instanceof Object) + { + this.is_constructed = in_window.org.pkijs.getValue(arguments[0], "is_constructed", false); + + // #region Variables from hex_block type + this.is_hex_only = in_window.org.pkijs.getValue(arguments[0], "is_hex_only", false); + if("value_hex" in arguments[0]) + this.value_hex = util_copybuf(arguments[0].value_hex); + else + this.value_hex = new ArrayBuffer(0); + // #endregion + } + else + { + this.is_constructed = false; + + // #region Variables from hex_block type + this.is_hex_only = false; + this.value_hex = new ArrayBuffer(0); + // #endregion + } + }; + //************************************************************************************** + local.OCTETSTRING_value_block.prototype = new local.ASN1_CONSTRUCTED_value_block(); + local.OCTETSTRING_value_block.constructor = local.OCTETSTRING_value_block; + //************************************************************************************** + local.OCTETSTRING_value_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + var result_offset = 0; + + if(this.is_constructed == true) + { + this.is_hex_only = false; + + result_offset = local.ASN1_CONSTRUCTED_value_block.prototype.fromBER.call(this, input_buffer, input_offset, input_length); + if(result_offset == (-1)) + return result_offset; + + for(var i = 0; i < this.value.length; i++) + { + var current_block_name = this.value[i].block_name(); + + if(current_block_name == in_window.org.pkijs.asn1.EOC.prototype.block_name()) + { + if(this.is_indefinite_form == true) + break; + else + { + this.error = "EOC is unexpected, OCTET STRING may consists of OCTET STRINGs only"; + return (-1); + } + } + + if(current_block_name != in_window.org.pkijs.asn1.OCTETSTRING.prototype.block_name()) + { + this.error = "OCTET STRING may consists of OCTET STRINGs only"; + return (-1); + } + } + } + else + { + this.is_hex_only = true; + + result_offset = local.hex_block.prototype.fromBER.call(this, input_buffer, input_offset, input_length); + this.block_length = input_length; + } + + return result_offset; + }; + //************************************************************************************** + local.OCTETSTRING_value_block.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + if(typeof size_only === "undefined") + size_only = false; + + if(this.is_constructed === true) + return local.ASN1_CONSTRUCTED_value_block.prototype.toBER.call(this, size_only); + else + { + var ret_buf = new ArrayBuffer(this.value_hex.byteLength); + + if(size_only === true) + return ret_buf; + + if(this.value_hex.byteLength == 0) + return ret_buf; + + ret_buf = util_copybuf(this.value_hex); + + return ret_buf; + } + + return (new ArrayBuffer(0)); + }; + //************************************************************************************** + local.OCTETSTRING_value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "OCTETSTRING_value_block"; + }; + //************************************************************************************** + local.OCTETSTRING_value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.ASN1_CONSTRUCTED_value_block.prototype.toJSON.call(this); + + _object.block_name = local.OCTETSTRING_value_block.prototype.block_name.call(this); + _object.is_constructed = this.is_constructed; + _object.is_hex_only = this.is_hex_only; + _object.value_hex = in_window.org.pkijs.bufferToHexCodes(this.value_hex, 0, this.value_hex.byteLength); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.OCTETSTRING = + function() + { + in_window.org.pkijs.asn1.ASN1_block.call(this, arguments[0]); + + this.value_block = new local.OCTETSTRING_value_block(arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 4; // OCTETSTRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.OCTETSTRING.prototype = new in_window.org.pkijs.asn1.ASN1_block(); + in_window.org.pkijs.asn1.OCTETSTRING.constructor = in_window.org.pkijs.asn1.OCTETSTRING; + //************************************************************************************** + in_window.org.pkijs.asn1.OCTETSTRING.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + this.value_block.is_constructed = this.id_block.is_constructed; + this.value_block.is_indefinite_form = this.len_block.is_indefinite_form; + + // #region Ability to encode empty OCTET STRING + if(input_length == 0) + { + if(this.id_block.error.length == 0) + this.block_length += this.id_block.block_length; + + if(this.len_block.error.length == 0) + this.block_length += this.len_block.block_length; + + return input_offset; + } + // #endregion + + return in_window.org.pkijs.asn1.ASN1_block.prototype.fromBER.call(this, input_buffer, input_offset, input_length); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.OCTETSTRING.prototype.block_name = + function() + { + return "OCTETSTRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.OCTETSTRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.OCTETSTRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.OCTETSTRING.prototype.isEqual = + function(octetString) + { + /// <summaryChecking that two OCTETSTRINGs are equal></summary> + /// <param name="octetString" type="in_window.org.pkijs.asn1.OCTETSTRING">The OCTETSTRING to compare with</param> + + // #region Check input type + if((octetString instanceof in_window.org.pkijs.asn1.OCTETSTRING) == false) + return false; + // #endregion + + // #region Compare two JSON strings + if(JSON.stringify(this) != JSON.stringify(octetString)) + return false; + // #endregion + + return true; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of ASN.1 BITSTRING type class + //************************************************************************************** + local.BITSTRING_value_block = + function() + { + local.ASN1_CONSTRUCTED_value_block.call(this, arguments[0]); + + if(arguments[0] instanceof Object) + { + this.unused_bits = in_window.org.pkijs.getValue(arguments[0], "unused_bits", 0); + this.is_constructed = in_window.org.pkijs.getValue(arguments[0], "is_constructed", false); + + // #region Variables from hex_block type + this.is_hex_only = in_window.org.pkijs.getValue(arguments[0], "is_hex_only", false); + + if("value_hex" in arguments[0]) + this.value_hex = util_copybuf(arguments[0].value_hex); + else + this.value_hex = new ArrayBuffer(0); + + this.block_length = this.value_hex.byteLength; + // #endregion + } + else + { + this.unused_bits = 0; + this.is_constructed = false; + + // #region Variables from hex_block type + this.is_hex_only = false; + this.value_hex = new ArrayBuffer(0); + // #endregion + } + }; + //************************************************************************************** + local.BITSTRING_value_block.prototype = new local.ASN1_CONSTRUCTED_value_block(); + local.BITSTRING_value_block.constructor = local.BITSTRING_value_block; + //************************************************************************************** + local.BITSTRING_value_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + // #region Ability to decode zero-length BITSTRING value + if(input_length == 0) + return input_offset; + // #endregion + + var result_offset = (-1); + + // #region If the BISTRING supposed to be a constructed value + if(this.is_constructed == true) + { + result_offset = local.ASN1_CONSTRUCTED_value_block.prototype.fromBER.call(this, input_buffer, input_offset, input_length); + if(result_offset == (-1)) + return result_offset; + + for(var i = 0; i < this.value.length; i++) + { + var current_block_name = this.value[i].block_name(); + + if(current_block_name == in_window.org.pkijs.asn1.EOC.prototype.block_name()) + { + if(this.is_indefinite_form == true) + break; + else + { + this.error = "EOC is unexpected, BIT STRING may consists of BIT STRINGs only"; + return (-1); + } + } + + if(current_block_name != in_window.org.pkijs.asn1.BITSTRING.prototype.block_name()) + { + this.error = "BIT STRING may consists of BIT STRINGs only"; + return (-1); + } + + if((this.unused_bits > 0) && (this.value[i].unused_bits > 0)) + { + this.error = "Usign of \"unused bits\" inside constructive BIT STRING allowed for least one only"; + return (-1); + } + else + { + this.unused_bits = this.value[i].unused_bits; + if(this.unused_bits > 7) + { + this.error = "Unused bits for BITSTRING must be in range 0-7"; + return (-1); + } + } + } + + return result_offset; + } + // #endregion + // #region If the BITSTRING supposed to be a primitive value + else + { + // #region Basic check for parameters + if(check_buffer_params.call(this, input_buffer, input_offset, input_length) === false) + return (-1); + // #endregion + + var int_buffer = new Uint8Array(input_buffer, input_offset, input_length); + + this.unused_bits = int_buffer[0]; + if(this.unused_bits > 7) + { + this.error = "Unused bits for BITSTRING must be in range 0-7"; + return (-1); + } + + // #region Copy input buffer to internal buffer + this.value_hex = new ArrayBuffer(int_buffer.length - 1); + var view = new Uint8Array(this.value_hex); + for(var i = 0; i < (input_length - 1) ; i++) + view[i] = int_buffer[i + 1]; + // #endregion + + this.block_length = int_buffer.length; + + return (input_offset + input_length); + } + // #endregion + }; + //************************************************************************************** + local.BITSTRING_value_block.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + if(typeof size_only === "undefined") + size_only = false; + + if(this.is_constructed === true) + return local.ASN1_CONSTRUCTED_value_block.prototype.toBER.call(this, size_only); + else + { + if(size_only === true) + return (new ArrayBuffer(this.value_hex.byteLength + 1)); + + if(this.value_hex.byteLength == 0) + return (new ArrayBuffer(0)); + + var cur_view = new Uint8Array(this.value_hex); + + var ret_buf = new ArrayBuffer(this.value_hex.byteLength + 1); + var ret_view = new Uint8Array(ret_buf); + + ret_view[0] = this.unused_bits; + + for(var i = 0; i < this.value_hex.byteLength; i++) + ret_view[i + 1] = cur_view[i]; + + return ret_buf; + } + + return (new ArrayBuffer(0)); + }; + //************************************************************************************** + local.BITSTRING_value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "BITSTRING_value_block"; + }; + //************************************************************************************** + local.BITSTRING_value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.ASN1_CONSTRUCTED_value_block.prototype.toJSON.call(this); + + _object.block_name = local.BITSTRING_value_block.prototype.block_name.call(this); + _object.unused_bits = this.unused_bits; + _object.is_constructed = this.is_constructed; + _object.is_hex_only = this.is_hex_only; + _object.value_hex = in_window.org.pkijs.bufferToHexCodes(this.value_hex, 0, this.value_hex.byteLength); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.BITSTRING = + function() + { + in_window.org.pkijs.asn1.ASN1_block.call(this, arguments[0]); + + this.value_block = new local.BITSTRING_value_block(arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 3; // BITSTRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.BITSTRING.prototype = new in_window.org.pkijs.asn1.ASN1_block(); + in_window.org.pkijs.asn1.BITSTRING.constructor = in_window.org.pkijs.asn1.BITSTRING; + //************************************************************************************** + in_window.org.pkijs.asn1.BITSTRING.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "BITSTRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.BITSTRING.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + // #region Ability to encode empty BITSTRING + if(input_length == 0) + return input_offset; + // #endregion + + this.value_block.is_constructed = this.id_block.is_constructed; + this.value_block.is_indefinite_form = this.len_block.is_indefinite_form; + + return in_window.org.pkijs.asn1.ASN1_block.prototype.fromBER.call(this, input_buffer, input_offset, input_length); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.BITSTRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.BITSTRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of ASN.1 INTEGER type class + //************************************************************************************** + local.INTEGER_value_block = + function() + { + local.value_block.call(this, arguments[0]); + + if(arguments[0] instanceof Object) + { + this.value_dec = in_window.org.pkijs.getValue(arguments[0], "value", 0); + + // #region Variables from hex_block type + this.is_hex_only = in_window.org.pkijs.getValue(arguments[0], "is_hex_only", false); + if("value_hex" in arguments[0]) + { + this.value_hex = util_copybuf(arguments[0].value_hex); + + if(this.value_hex.byteLength >= 4) // Dummy's protection + this.is_hex_only = true; + else + this.value_dec = util_decode_tc.call(this); + } + else + this.value_hex = util_encode_tc(this.value_dec); + // #endregion + } + else + { + this.value_dec = 0; + + // #region Variables from hex_block type + this.is_hex_only = false; + this.value_hex = new ArrayBuffer(0); + // #endregion + } + }; + //************************************************************************************** + local.INTEGER_value_block.prototype = new local.value_block(); + local.INTEGER_value_block.constructor = local.INTEGER_value_block; + //************************************************************************************** + local.INTEGER_value_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + var result_offset = local.hex_block.prototype.fromBER.call(this, input_buffer, input_offset, input_length); + if(result_offset == (-1)) + return result_offset; + + if(this.value_hex.byteLength > 4) // In JavaScript we can effectively work with 32-bit integers only + { + this.warnings.push("Too big INTEGER for decoding, hex only"); + this.is_hex_only = true; + } + else + this.value_dec = util_decode_tc.call(this); + + this.block_length = input_length; + + return (input_offset + input_length); + }; + //************************************************************************************** + local.INTEGER_value_block.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + if(typeof size_only === "undefined") + size_only = false; + + if(this.is_hex_only === false) + { + var encoded_buf = util_encode_tc(this.value_dec); + if(encoded_buf.byteLength == 0) + { + this.error = "Error during encoding INTEGER value"; + return (new ArrayBuffer(0)); + } + + return util_copybuf(encoded_buf); + } + else + return util_copybuf(this.value_hex); + + return (new ArrayBuffer(0)); + }; + //************************************************************************************** + local.INTEGER_value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "INTEGER_value_block"; + }; + //************************************************************************************** + local.INTEGER_value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.value_block.prototype.toJSON.call(this); + + _object.block_name = local.INTEGER_value_block.prototype.block_name.call(this); + _object.value_dec = this.value_dec; + _object.is_hex_only = this.is_hex_only; + _object.value_hex = in_window.org.pkijs.bufferToHexCodes(this.value_hex, 0, this.value_hex.byteLength); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.INTEGER = + function() + { + in_window.org.pkijs.asn1.ASN1_block.call(this, arguments[0]); + + this.value_block = new local.INTEGER_value_block(arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 2; // INTEGER + }; + //************************************************************************************** + in_window.org.pkijs.asn1.INTEGER.prototype = new in_window.org.pkijs.asn1.ASN1_block(); + in_window.org.pkijs.asn1.INTEGER.constructor = in_window.org.pkijs.asn1.INTEGER; + //************************************************************************************** + in_window.org.pkijs.asn1.INTEGER.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "INTEGER"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.INTEGER.prototype.isEqual = + function() + { + /// <summary>Compare two INTEGER object, or INTEGER and ArrayBuffer objects</summary> + /// <returns type="Boolean"></returns> + + if(arguments[0] instanceof in_window.org.pkijs.asn1.INTEGER) + { + if(this.value_block.is_hex_only && arguments[0].value_block.is_hex_only) // Compare two ArrayBuffers + return in_window.org.pkijs.isEqual_buffer(this.value_block.value_hex, arguments[0].value_block.value_hex); + else + { + if(this.value_block.is_hex_only === arguments[0].value_block.is_hex_only) + return (this.value_block.value_dec == arguments[0].value_block.value_dec); + else + return false; + } + } + else + { + if(arguments[0] instanceof ArrayBuffer) + return in_window.org.pkijs.isEqual_buffer(this.value_block.value_hex, arguments[0]); + else + return false; + } + + return false; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.INTEGER.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.INTEGER.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of ASN.1 ENUMERATED type class + //************************************************************************************** + in_window.org.pkijs.asn1.ENUMERATED = + function() + { + in_window.org.pkijs.asn1.INTEGER.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 10; // ENUMERATED + }; + //************************************************************************************** + in_window.org.pkijs.asn1.ENUMERATED.prototype = new in_window.org.pkijs.asn1.INTEGER(); + in_window.org.pkijs.asn1.ENUMERATED.constructor = in_window.org.pkijs.asn1.ENUMERATED; + //************************************************************************************** + in_window.org.pkijs.asn1.ENUMERATED.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "ENUMERATED"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.ENUMERATED.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.INTEGER.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.ENUMERATED.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of ASN.1 OBJECT IDENTIFIER type class + //************************************************************************************** + local.SID_value_block = + function() + { + local.hex_block.call(this, arguments[0]); + + if(arguments[0] instanceof Object) + { + this.value_dec = in_window.org.pkijs.getValue(arguments[0], "value_dec", -1); + this.is_first_sid = in_window.org.pkijs.getValue(arguments[0], "is_first_sid", false); + } + else + { + this.value_dec = (-1); + this.is_first_sid = false; + } + }; + //************************************************************************************** + local.SID_value_block.prototype = new local.hex_block(); + local.SID_value_block.constructor = local.SID_value_block; + //************************************************************************************** + local.SID_value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "sid_block"; + }; + //************************************************************************************** + local.SID_value_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + if(input_length == 0) + return input_offset; + + // #region Basic check for parameters + if(check_buffer_params.call(this, input_buffer, input_offset, input_length) === false) + return (-1); + // #endregion + + var int_buffer = new Uint8Array(input_buffer, input_offset, input_length); + + this.value_hex = new ArrayBuffer(input_length); + var view = new Uint8Array(this.value_hex); + + for(var i = 0; i < input_length; i++) + { + view[i] = int_buffer[i] & 0x7F; + + this.block_length++; + + if((int_buffer[i] & 0x80) == 0x00) + break; + } + + // #region Ajust size of value_hex buffer + var temp_value_hex = new ArrayBuffer(this.block_length); + var temp_view = new Uint8Array(temp_value_hex); + + for(var i = 0; i < this.block_length; i++) + temp_view[i] = view[i]; + + this.value_hex = util_copybuf(temp_value_hex); + view = new Uint8Array(this.value_hex); + // #endregion + + if((int_buffer[this.block_length - 1] & 0x80) != 0x00) + { + this.error = "End of input reached before message was fully decoded"; + return (-1); + } + + if(view[0] == 0x00) + this.warnings.push("Needlessly long format of SID encoding"); + + if(this.block_length <= 8) + this.value_dec = util_frombase(view, 7); + else + { + this.is_hex_only = true; + this.warnings.push("Too big SID for decoding, hex only"); + } + + return (input_offset + this.block_length); + }; + //************************************************************************************** + local.SID_value_block.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + if(typeof size_only === "undefined") + size_only = false; + + if(this.is_hex_only) + { + if(size_only === true) + return (new ArrayBuffer(this.value_hex.byteLength)); + + var cur_view = new Uint8Array(this.value_hex); + + var ret_buf = new ArrayBuffer(this.block_length); + var ret_view = new Uint8Array(ret_buf); + + for(var i = 0; i < (this.block_length - 1) ; i++) + ret_view[i] = cur_view[i] | 0x80; + + ret_view[this.block_length - 1] = cur_view[this.block_length - 1]; + + return ret_buf; + } + else + { + var encoded_buf = util_tobase(this.value_dec, 7); + if(encoded_buf.byteLength === 0) + { + this.error = "Error during encoding SID value"; + return (new ArrayBuffer(0)); + } + + var ret_buf = new ArrayBuffer(encoded_buf.byteLength); + + if(size_only === false) + { + var encoded_view = new Uint8Array(encoded_buf); + var ret_view = new Uint8Array(ret_buf); + + for(var i = 0; i < (encoded_buf.byteLength - 1) ; i++) + ret_view[i] = encoded_view[i] | 0x80; + + ret_view[encoded_buf.byteLength - 1] = encoded_view[encoded_buf.byteLength - 1]; + } + + return ret_buf; + } + }; + //************************************************************************************** + local.SID_value_block.prototype.toString = + function() + { + var result = ""; + + if(this.is_hex_only === true) + result = to_hex_codes(this.value_hex); + else + { + if(this.is_first_sid) + { + var sid_value = this.value_dec; + + if(this.value_dec <= 39) + result = "0."; + else + { + if(this.value_dec <= 79) + { + result = "1."; + sid_value -= 40; + } + else + { + result = "2."; + sid_value -= 80; + } + } + + result = result + sid_value.toString(); + } + else + result = this.value_dec.toString(); + } + + return result; + }; + //************************************************************************************** + local.SID_value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.hex_block.prototype.toJSON.call(this); + + _object.block_name = local.SID_value_block.prototype.block_name.call(this); + _object.value_dec = this.value_dec; + _object.is_first_sid = this.is_first_sid; + + return _object; + }; + //************************************************************************************** + local.OID_value_block = + function() + { + local.value_block.call(this, arguments[0]); + + this.value = new Array(); + + if(arguments[0] instanceof Object) + this.fromString(in_window.org.pkijs.getValue(arguments[0], "value", "")); + }; + //************************************************************************************** + local.OID_value_block.prototype = new local.value_block(); + local.OID_value_block.constructor = local.OID_value_block; + //************************************************************************************** + local.OID_value_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + var result_offset = input_offset; + + while(input_length > 0) + { + var sid_block = new local.SID_value_block(); + result_offset = sid_block.fromBER(input_buffer, result_offset, input_length); + if(result_offset == (-1)) + { + this.block_length = 0; + this.error = sid_block.error; + return result_offset; + } + + if(this.value.length == 0) + sid_block.is_first_sid = true; + + this.block_length += sid_block.block_length; + input_length -= sid_block.block_length; + + this.value.push(sid_block); + } + + return result_offset; + }; + //************************************************************************************** + local.OID_value_block.prototype.toBER = + function(size_only) + { + /// <summary>Encoding of current ASN.1 block into ASN.1 encoded array (BER rules)</summary> + /// <param name="size_only" type="Boolean">Flag that we need only a size of encoding, not a real array of bytes</param> + + if(typeof size_only === "undefined") + size_only = false; + + var ret_buf = new ArrayBuffer(0); + + for(var i = 0; i < this.value.length; i++) + { + var value_buf = this.value[i].toBER(size_only); + if(value_buf.byteLength === 0) + { + this.error = this.value[i].error; + return (new ArrayBuffer(0)); + } + + ret_buf = util_concatbuf(ret_buf, value_buf); + } + + return ret_buf; + }; + //************************************************************************************** + local.OID_value_block.prototype.fromString = + function(str) + { + this.value = new Array(); // Clear existing SID values + + var pos1 = 0; + var pos2 = 0; + + var sid = ""; + + var flag = false; + + do + { + pos2 = str.indexOf('.', pos1); + if(pos2 === (-1)) + sid = str.substr(pos1); + else + sid = str.substr(pos1, pos2 - pos1); + + pos1 = pos2 + 1; + + if(flag) + { + var sid_block = this.value[0]; + + var plus = 0; + + switch(sid_block.value_dec) + { + case 0: + break; + case 1: + plus = 40; + break; + case 2: + plus = 80; + break; + default: + this.value = new Array(); // clear SID array + return false; // ??? + } + + var parsedSID = parseInt(sid, 10); + if(isNaN(parsedSID)) + return true; + + sid_block.value_dec = parsedSID + plus; + + flag = false; + } + else + { + var sid_block = new local.SID_value_block(); + sid_block.value_dec = parseInt(sid, 10); + if(isNaN(sid_block.value_dec)) + return true; + + if(this.value.length === 0) + { + sid_block.is_first_sid = true; + flag = true; + } + + this.value.push(sid_block); + } + + } while(pos2 !== (-1)); + + return true; + }; + //************************************************************************************** + local.OID_value_block.prototype.toString = + function() + { + var result = ""; + var is_hex_only = false; + + for(var i = 0; i < this.value.length; i++) + { + is_hex_only = this.value[i].is_hex_only; + + var sid_str = this.value[i].toString(); + + if(i !== 0) + result = result + "."; + + if(is_hex_only) + { + sid_str = "{" + sid_str + "}"; + + if(this.value[i].is_first_sid) + result = "2.{" + sid_str + " - 80}"; + else + result = result + sid_str; + } + else + result = result + sid_str; + } + + return result; + }; + //************************************************************************************** + local.OID_value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "OID_value_block"; + }; + //************************************************************************************** + local.OID_value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.value_block.prototype.toJSON.call(this); + + _object.block_name = local.OID_value_block.prototype.block_name.call(this); + _object.value = local.OID_value_block.prototype.toString.call(this); + _object.sid_array = new Array(); + for(var i = 0; i < this.value.length; i++) + _object.sid_array.push(this.value[i].toJSON()); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.OID = + function() + { + in_window.org.pkijs.asn1.ASN1_block.call(this, arguments[0]); + + this.value_block = new local.OID_value_block(arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 6; // OBJECT IDENTIFIER + }; + //************************************************************************************** + in_window.org.pkijs.asn1.OID.prototype = new in_window.org.pkijs.asn1.ASN1_block(); + in_window.org.pkijs.asn1.OID.constructor = in_window.org.pkijs.asn1.OID; + //************************************************************************************** + in_window.org.pkijs.asn1.OID.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "OID"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.OID.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.OID.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of all string's classes + //************************************************************************************** + local.UTF8STRING_value_block = + function() + { + local.hex_block.call(this, arguments[0]); + + this.is_hex_only = true; + this.value = ""; // String representation of decoded ArrayBuffer + }; + //************************************************************************************** + local.UTF8STRING_value_block.prototype = new local.hex_block(); + local.UTF8STRING_value_block.constructor = local.UTF8STRING_value_block; + //************************************************************************************** + local.UTF8STRING_value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "UTF8STRING_value_block"; + }; + //************************************************************************************** + local.UTF8STRING_value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.hex_block.prototype.toJSON.call(this); + + _object.block_name = local.UTF8STRING_value_block.prototype.block_name.call(this); + _object.value = this.value; + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTF8STRING = + function() + { + in_window.org.pkijs.asn1.ASN1_block.call(this, arguments[0]); + + this.value_block = new local.UTF8STRING_value_block(); + + if(arguments[0] instanceof Object) + { + if("value" in arguments[0]) + in_window.org.pkijs.asn1.UTF8STRING.prototype.fromString.call(this,arguments[0].value); + } + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 12; // UTF8STRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTF8STRING.prototype = new in_window.org.pkijs.asn1.ASN1_block(); + in_window.org.pkijs.asn1.UTF8STRING.constructor = in_window.org.pkijs.asn1.UTF8STRING; + //************************************************************************************** + in_window.org.pkijs.asn1.UTF8STRING.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "UTF8STRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTF8STRING.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + var result_offset = this.value_block.fromBER(input_buffer, input_offset, (this.len_block.is_indefinite_form == true) ? input_length : this.len_block.length); + if(result_offset == (-1)) + { + this.error = this.value_block.error; + return result_offset; + } + + in_window.org.pkijs.asn1.UTF8STRING.prototype.fromBuffer.call(this, this.value_block.value_hex); + + if(this.id_block.error.length == 0) + this.block_length += this.id_block.block_length; + + if(this.len_block.error.length == 0) + this.block_length += this.len_block.block_length; + + if(this.value_block.error.length == 0) + this.block_length += this.value_block.block_length; + + return result_offset; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTF8STRING.prototype.fromBuffer = + function(input_buffer) + { + /// <param name="input_buffer" type="ArrayBuffer">Array with encoded string</param> + this.value_block.value = String.fromCharCode.apply(null, new Uint8Array(input_buffer)); + + try + { + this.value_block.value = decodeURIComponent(escape(this.value_block.value)); + } + catch(ex) + { + this.warnings.push("Error during \"decodeURIComponent\": " + ex + ", using raw string"); + } + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTF8STRING.prototype.fromString = + function(input_string) + { + /// <param name="input_string" type="String">String with UNIVERSALSTRING value</param> + + var str = unescape(encodeURIComponent(input_string)); + var str_len = str.length; + + this.value_block.value_hex = new ArrayBuffer(str_len); + var view = new Uint8Array(this.value_block.value_hex); + + for(var i = 0; i < str_len; i++) + view[i] = str.charCodeAt(i); + + this.value_block.value = input_string; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTF8STRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.UTF8STRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + local.BMPSTRING_value_block = + function() + { + local.hex_block.call(this, arguments[0]); + + this.is_hex_only = true; + this.value = ""; + }; + //************************************************************************************** + local.BMPSTRING_value_block.prototype = new local.hex_block(); + local.BMPSTRING_value_block.constructor = local.BMPSTRING_value_block; + //************************************************************************************** + local.BMPSTRING_value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "BMPSTRING_value_block"; + }; + //************************************************************************************** + local.BMPSTRING_value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.hex_block.prototype.toJSON.call(this); + + _object.block_name = local.BMPSTRING_value_block.prototype.block_name.call(this); + _object.value = this.value; + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.BMPSTRING = + function() + { + in_window.org.pkijs.asn1.ASN1_block.call(this, arguments[0]); + + this.value_block = new local.BMPSTRING_value_block(); + + if(arguments[0] instanceof Object) + { + if("value" in arguments[0]) + in_window.org.pkijs.asn1.BMPSTRING.prototype.fromString.call(this, arguments[0].value); + } + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 30; // BMPSTRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.BMPSTRING.prototype = new in_window.org.pkijs.asn1.ASN1_block(); + in_window.org.pkijs.asn1.BMPSTRING.constructor = in_window.org.pkijs.asn1.BMPSTRING; + //************************************************************************************** + in_window.org.pkijs.asn1.BMPSTRING.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "BMPSTRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.BMPSTRING.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + var result_offset = this.value_block.fromBER(input_buffer, input_offset, (this.len_block.is_indefinite_form == true) ? input_length : this.len_block.length); + if(result_offset == (-1)) + { + this.error = this.value_block.error; + return result_offset; + } + + in_window.org.pkijs.asn1.BMPSTRING.prototype.fromBuffer.call(this, this.value_block.value_hex); + + if(this.id_block.error.length == 0) + this.block_length += this.id_block.block_length; + + if(this.len_block.error.length == 0) + this.block_length += this.len_block.block_length; + + if(this.value_block.error.length == 0) + this.block_length += this.value_block.block_length; + + return result_offset; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.BMPSTRING.prototype.fromBuffer = + function(input_buffer) + { + /// <param name="input_buffer" type="ArrayBuffer">Array with encoded string</param> + + var copy_buffer = in_window.org.pkijs.copyBuffer(input_buffer); + + var value_view = new Uint8Array(copy_buffer); + + for(var i = 0; i < value_view.length; i = i + 2) + { + var temp = value_view[i]; + + value_view[i] = value_view[i + 1]; + value_view[i + 1] = temp; + } + + this.value_block.value = String.fromCharCode.apply(null, new Uint16Array(copy_buffer)); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.BMPSTRING.prototype.fromString = + function(input_string) + { + /// <param name="input_string" type="String">String with UNIVERSALSTRING value</param> + + var str_length = input_string.length; + + this.value_block.value_hex = new ArrayBuffer(str_length * 2); + var value_hex_view = new Uint8Array(this.value_block.value_hex); + + for(var i = 0; i < str_length; i++) + { + var code_buf = util_tobase(input_string.charCodeAt(i), 8); + var code_view = new Uint8Array(code_buf); + if(code_view.length > 2) + continue; + + var dif = 2 - code_view.length; + + for(var j = (code_view.length - 1) ; j >= 0; j--) + value_hex_view[i * 2 + j + dif] = code_view[j]; + } + + this.value_block.value = input_string; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.BMPSTRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.BMPSTRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + local.UNIVERSALSTRING_value_block = + function() + { + local.hex_block.call(this, arguments[0]); + + this.is_hex_only = true; + this.value = ""; + }; + //************************************************************************************** + local.UNIVERSALSTRING_value_block.prototype = new local.hex_block(); + local.UNIVERSALSTRING_value_block.constructor = local.UNIVERSALSTRING_value_block; + //************************************************************************************** + local.UNIVERSALSTRING_value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "UNIVERSALSTRING_value_block"; + }; + //************************************************************************************** + local.UNIVERSALSTRING_value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.hex_block.prototype.toJSON.call(this); + + _object.block_name = local.UNIVERSALSTRING_value_block.prototype.block_name.call(this); + _object.value = this.value; + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UNIVERSALSTRING = + function() + { + in_window.org.pkijs.asn1.ASN1_block.call(this, arguments[0]); + + this.value_block = new local.UNIVERSALSTRING_value_block(); + + if(arguments[0] instanceof Object) + { + if("value" in arguments[0]) + in_window.org.pkijs.asn1.UNIVERSALSTRING.prototype.fromString.call(this, arguments[0].value); + } + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 28; // UNIVERSALSTRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UNIVERSALSTRING.prototype = new in_window.org.pkijs.asn1.ASN1_block(); + in_window.org.pkijs.asn1.UNIVERSALSTRING.constructor = in_window.org.pkijs.asn1.UNIVERSALSTRING; + //************************************************************************************** + in_window.org.pkijs.asn1.UNIVERSALSTRING.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "UNIVERSALSTRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UNIVERSALSTRING.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + var result_offset = this.value_block.fromBER(input_buffer, input_offset, (this.len_block.is_indefinite_form == true) ? input_length : this.len_block.length); + if(result_offset == (-1)) + { + this.error = this.value_block.error; + return result_offset; + } + + in_window.org.pkijs.asn1.UNIVERSALSTRING.prototype.fromBuffer.call(this, this.value_block.value_hex); + + if(this.id_block.error.length == 0) + this.block_length += this.id_block.block_length; + + if(this.len_block.error.length == 0) + this.block_length += this.len_block.block_length; + + if(this.value_block.error.length == 0) + this.block_length += this.value_block.block_length; + + return result_offset; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UNIVERSALSTRING.prototype.fromBuffer = + function(input_buffer) + { + /// <param name="input_buffer" type="ArrayBuffer">Array with encoded string</param> + + var copy_buffer = in_window.org.pkijs.copyBuffer(input_buffer); + + var value_view = new Uint8Array(copy_buffer); + + for(var i = 0; i < value_view.length; i = i + 4) + { + value_view[i] = value_view[i + 3]; + value_view[i + 1] = value_view[i + 2]; + value_view[i + 2] = 0x00; + value_view[i + 3] = 0x00; + } + + this.value_block.value = String.fromCharCode.apply(null, new Uint32Array(copy_buffer)); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UNIVERSALSTRING.prototype.fromString = + function(input_string) + { + /// <param name="input_string" type="String">String with UNIVERSALSTRING value</param> + + var str_length = input_string.length; + + this.value_block.value_hex = new ArrayBuffer(str_length * 4); + var value_hex_view = new Uint8Array(this.value_block.value_hex); + + for(var i = 0; i < str_length; i++) + { + var code_buf = util_tobase(input_string.charCodeAt(i), 8); + var code_view = new Uint8Array(code_buf); + if(code_view.length > 4) + continue; + + var dif = 4 - code_view.length; + + for(var j = (code_view.length - 1) ; j >= 0; j--) + value_hex_view[i*4 + j + dif] = code_view[j]; + } + + this.value_block.value = input_string; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UNIVERSALSTRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.UNIVERSALSTRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + local.SIMPLESTRING_value_block = + function() + { + local.hex_block.call(this, arguments[0]); + + /// <field type="String">Native string representation</field> + this.value = ""; + this.is_hex_only = true; + }; + //************************************************************************************** + local.SIMPLESTRING_value_block.prototype = new local.hex_block(); + local.SIMPLESTRING_value_block.constructor = local.SIMPLESTRING_value_block; + //************************************************************************************** + local.SIMPLESTRING_value_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "SIMPLESTRING_value_block"; + }; + //************************************************************************************** + local.SIMPLESTRING_value_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.hex_block.prototype.toJSON.call(this); + + _object.block_name = local.SIMPLESTRING_value_block.prototype.block_name.call(this); + _object.value = this.value; + + return _object; + }; + //************************************************************************************** + local.SIMPLESTRING_block = + function() + { + in_window.org.pkijs.asn1.ASN1_block.call(this, arguments[0]); + + this.value_block = new local.SIMPLESTRING_value_block(); + + if(arguments[0] instanceof Object) + { + if("value" in arguments[0]) + local.SIMPLESTRING_block.prototype.fromString.call(this, arguments[0].value); + } + }; + //************************************************************************************** + local.SIMPLESTRING_block.prototype = new in_window.org.pkijs.asn1.ASN1_block(); + local.SIMPLESTRING_block.constructor = local.SIMPLESTRING_block; + //************************************************************************************** + local.SIMPLESTRING_block.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "SIMPLESTRING"; + }; + //************************************************************************************** + local.SIMPLESTRING_block.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + var result_offset = this.value_block.fromBER(input_buffer, input_offset, (this.len_block.is_indefinite_form == true) ? input_length : this.len_block.length); + if(result_offset == (-1)) + { + this.error = this.value_block.error; + return result_offset; + } + + local.SIMPLESTRING_block.prototype.fromBuffer.call(this, this.value_block.value_hex); + + if(this.id_block.error.length == 0) + this.block_length += this.id_block.block_length; + + if(this.len_block.error.length == 0) + this.block_length += this.len_block.block_length; + + if(this.value_block.error.length == 0) + this.block_length += this.value_block.block_length; + + return result_offset; + }; + //************************************************************************************** + local.SIMPLESTRING_block.prototype.fromBuffer = + function(input_buffer) + { + /// <param name="input_buffer" type="ArrayBuffer">Array with encoded string</param> + + this.value_block.value = String.fromCharCode.apply(null, new Uint8Array(input_buffer)); + }; + //************************************************************************************** + local.SIMPLESTRING_block.prototype.fromString = + function(input_string) + { + /// <param name="input_string" type="String">String with UNIVERSALSTRING value</param> + var str_len = input_string.length; + + this.value_block.value_hex = new ArrayBuffer(str_len); + var view = new Uint8Array(this.value_block.value_hex); + + for(var i = 0; i < str_len; i++) + view[i] = input_string.charCodeAt(i); + + this.value_block.value = input_string; + }; + //************************************************************************************** + local.SIMPLESTRING_block.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.ASN1_block.prototype.toJSON.call(this); + + _object.block_name = local.SIMPLESTRING_block.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.NUMERICSTRING = + function() + { + local.SIMPLESTRING_block.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 18; // NUMERICSTRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.NUMERICSTRING.prototype = new local.SIMPLESTRING_block(); + in_window.org.pkijs.asn1.NUMERICSTRING.constructor = in_window.org.pkijs.asn1.NUMERICSTRING; + //************************************************************************************** + in_window.org.pkijs.asn1.NUMERICSTRING.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "NUMERICSTRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.NUMERICSTRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.SIMPLESTRING_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.NUMERICSTRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.PRINTABLESTRING = + function() + { + local.SIMPLESTRING_block.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 19; // PRINTABLESTRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.PRINTABLESTRING.prototype = new local.SIMPLESTRING_block(); + in_window.org.pkijs.asn1.PRINTABLESTRING.constructor = in_window.org.pkijs.asn1.PRINTABLESTRING; + //************************************************************************************** + in_window.org.pkijs.asn1.PRINTABLESTRING.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "PRINTABLESTRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.PRINTABLESTRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.SIMPLESTRING_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.PRINTABLESTRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.TELETEXSTRING = + function() + { + local.SIMPLESTRING_block.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 20; // TELETEXSTRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.TELETEXSTRING.prototype = new local.SIMPLESTRING_block(); + in_window.org.pkijs.asn1.TELETEXSTRING.constructor = in_window.org.pkijs.asn1.TELETEXSTRING; + //************************************************************************************** + in_window.org.pkijs.asn1.TELETEXSTRING.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "TELETEXSTRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.TELETEXSTRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.SIMPLESTRING_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.TELETEXSTRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.VIDEOTEXSTRING = + function() + { + local.SIMPLESTRING_block.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 21; // VIDEOTEXSTRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.VIDEOTEXSTRING.prototype = new local.SIMPLESTRING_block(); + in_window.org.pkijs.asn1.VIDEOTEXSTRING.constructor = in_window.org.pkijs.asn1.VIDEOTEXSTRING; + //************************************************************************************** + in_window.org.pkijs.asn1.VIDEOTEXSTRING.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "VIDEOTEXSTRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.VIDEOTEXSTRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.SIMPLESTRING_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.VIDEOTEXSTRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.IA5STRING = + function() + { + local.SIMPLESTRING_block.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 22; // IA5STRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.IA5STRING.prototype = new local.SIMPLESTRING_block(); + in_window.org.pkijs.asn1.IA5STRING.constructor = in_window.org.pkijs.asn1.IA5STRING; + //************************************************************************************** + in_window.org.pkijs.asn1.IA5STRING.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "IA5STRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.IA5STRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.SIMPLESTRING_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.IA5STRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GRAPHICSTRING = + function() + { + local.SIMPLESTRING_block.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 25; // GRAPHICSTRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GRAPHICSTRING.prototype = new local.SIMPLESTRING_block(); + in_window.org.pkijs.asn1.GRAPHICSTRING.constructor = in_window.org.pkijs.asn1.GRAPHICSTRING; + //************************************************************************************** + in_window.org.pkijs.asn1.GRAPHICSTRING.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "GRAPHICSTRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GRAPHICSTRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.SIMPLESTRING_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.GRAPHICSTRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.VISIBLESTRING = + function() + { + local.SIMPLESTRING_block.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 26; // VISIBLESTRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.VISIBLESTRING.prototype = new local.SIMPLESTRING_block(); + in_window.org.pkijs.asn1.VISIBLESTRING.constructor = in_window.org.pkijs.asn1.VISIBLESTRING; + //************************************************************************************** + in_window.org.pkijs.asn1.VISIBLESTRING.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "VISIBLESTRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.VISIBLESTRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.SIMPLESTRING_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.VISIBLESTRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALSTRING = + function() + { + local.SIMPLESTRING_block.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 27; // GENERALSTRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALSTRING.prototype = new local.SIMPLESTRING_block(); + in_window.org.pkijs.asn1.GENERALSTRING.constructor = in_window.org.pkijs.asn1.GENERALSTRING; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALSTRING.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "GENERALSTRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALSTRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.SIMPLESTRING_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.GENERALSTRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.CHARACTERSTRING = + function() + { + local.SIMPLESTRING_block.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 29; // CHARACTERSTRING + }; + //************************************************************************************** + in_window.org.pkijs.asn1.CHARACTERSTRING.prototype = new local.SIMPLESTRING_block(); + in_window.org.pkijs.asn1.CHARACTERSTRING.constructor = in_window.org.pkijs.asn1.CHARACTERSTRING; + //************************************************************************************** + in_window.org.pkijs.asn1.CHARACTERSTRING.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "CHARACTERSTRING"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.CHARACTERSTRING.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = local.SIMPLESTRING_block.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.CHARACTERSTRING.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of all date and time classes + //************************************************************************************** + in_window.org.pkijs.asn1.UTCTIME = + function() + { + in_window.org.pkijs.asn1.VISIBLESTRING.call(this, arguments[0]); + + this.year = 0; + this.month = 0; + this.day = 0; + this.hour = 0; + this.minute = 0; + this.second = 0; + + // #region Create UTCTIME from ASN.1 UTC string value + if((arguments[0] instanceof Object) && ("value" in arguments[0])) + { + in_window.org.pkijs.asn1.UTCTIME.prototype.fromString.call(this, arguments[0].value); + + this.value_block.value_hex = new ArrayBuffer(arguments[0].value.length); + var view = new Uint8Array(this.value_block.value_hex); + + for(var i = 0; i < arguments[0].value.length; i++) + view[i] = arguments[0].value.charCodeAt(i); + } + // #endregion + // #region Create UTCTIME from JavaScript Date type + if((arguments[0] instanceof Object) && ("value_date" in arguments[0])) + { + in_window.org.pkijs.asn1.UTCTIME.prototype.fromDate.call(this, arguments[0].value_date); + this.value_block.value_hex = in_window.org.pkijs.asn1.UTCTIME.prototype.toBuffer.call(this); + } + // #endregion + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 23; // UTCTIME + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTCTIME.prototype = new in_window.org.pkijs.asn1.VISIBLESTRING(); + in_window.org.pkijs.asn1.UTCTIME.constructor = in_window.org.pkijs.asn1.UTCTIME; + //************************************************************************************** + in_window.org.pkijs.asn1.UTCTIME.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + var result_offset = this.value_block.fromBER(input_buffer, input_offset, (this.len_block.is_indefinite_form == true) ? input_length : this.len_block.length); + if(result_offset == (-1)) + { + this.error = this.value_block.error; + return result_offset; + } + + in_window.org.pkijs.asn1.UTCTIME.prototype.fromBuffer.call(this, this.value_block.value_hex); + + if(this.id_block.error.length == 0) + this.block_length += this.id_block.block_length; + + if(this.len_block.error.length == 0) + this.block_length += this.len_block.block_length; + + if(this.value_block.error.length == 0) + this.block_length += this.value_block.block_length; + + return result_offset; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTCTIME.prototype.fromBuffer = + function(input_buffer) + { + in_window.org.pkijs.asn1.UTCTIME.prototype.fromString.call(this, String.fromCharCode.apply(null, new Uint8Array(input_buffer))); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTCTIME.prototype.toBuffer = + function() + { + var str = in_window.org.pkijs.asn1.UTCTIME.prototype.toString.call(this); + + var buffer = new ArrayBuffer(str.length); + var view = new Uint8Array(buffer); + + for(var i = 0; i < str.length; i++) + view[i] = str.charCodeAt(i); + + return buffer; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTCTIME.prototype.fromDate = + function(input_date) + { + /// <summary>Create "UTCTime" ASN.1 type from JavaScript "Date" type</summary> + + this.year = input_date.getUTCFullYear(); + this.month = input_date.getUTCMonth() + 1; + this.day = input_date.getUTCDate(); + this.hour = input_date.getUTCHours(); + this.minute = input_date.getUTCMinutes(); + this.second = input_date.getUTCSeconds(); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTCTIME.prototype.toDate = + function() + { + return (new Date(Date.UTC(this.year, this.month - 1, this.day, this.hour, this.minute, this.second))); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTCTIME.prototype.fromString = + function(input_string) + { + /// <summary>Create "UTCTime" ASN.1 type from JavaScript "String" type</summary> + + // #region Parse input string + var parser = /(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z/ig; + var parser_array = parser.exec(input_string); + if(parser_array === null) + { + this.error = "Wrong input string for convertion"; + return; + } + // #endregion + + // #region Store parsed values + var year = parseInt(parser_array[1], 10); + if(year >= 50) + this.year = 1900 + year; + else + this.year = 2000 + year; + + this.month = parseInt(parser_array[2], 10); + this.day = parseInt(parser_array[3], 10); + this.hour = parseInt(parser_array[4], 10); + this.minute = parseInt(parser_array[5], 10); + this.second = parseInt(parser_array[6], 10); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTCTIME.prototype.toString = + function() + { + var output_array = new Array(7); + + output_array[0] = in_window.org.pkijs.padNumber(((this.year < 2000) ? (this.year - 1900) : (this.year - 2000)), 2); + output_array[1] = in_window.org.pkijs.padNumber(this.month, 2); + output_array[2] = in_window.org.pkijs.padNumber(this.day, 2); + output_array[3] = in_window.org.pkijs.padNumber(this.hour, 2); + output_array[4] = in_window.org.pkijs.padNumber(this.minute, 2); + output_array[5] = in_window.org.pkijs.padNumber(this.second, 2); + output_array[6] = "Z"; + + return output_array.join(''); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTCTIME.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "UTCTIME"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.UTCTIME.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.VISIBLESTRING.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.UTCTIME.prototype.block_name.call(this); + _object.year = this.year; + _object.month = this.month; + _object.day = this.day; + _object.hour = this.hour; + _object.minute = this.minute; + _object.second = this.second; + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALIZEDTIME = + function() + { + in_window.org.pkijs.asn1.VISIBLESTRING.call(this, arguments[0]); + + this.year = 0; + this.month = 0; + this.day = 0; + this.hour = 0; + this.minute = 0; + this.second = 0; + this.millisecond = 0; + + // #region Create GeneralizedTime from ASN.1 string value + if((arguments[0] instanceof Object) && ("value" in arguments[0])) + { + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.fromString.call(this, arguments[0].value); + + this.value_block.value_hex = new ArrayBuffer(arguments[0].value.length); + var view = new Uint8Array(this.value_block.value_hex); + + for(var i = 0; i < arguments[0].value.length; i++) + view[i] = arguments[0].value.charCodeAt(i); + } + // #endregion + // #region Create GeneralizedTime from JavaScript Date type + if((arguments[0] instanceof Object) && ("value_date" in arguments[0])) + { + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.fromDate.call(this, arguments[0].value_date); + this.value_block.value_hex = in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.toBuffer.call(this); + } + // #endregion + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 24; // GENERALIZEDTIME + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype = new in_window.org.pkijs.asn1.VISIBLESTRING(); + in_window.org.pkijs.asn1.GENERALIZEDTIME.constructor = in_window.org.pkijs.asn1.GENERALIZEDTIME; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.fromBER = + function(input_buffer, input_offset, input_length) + { + /// <summary>Base function for converting block from BER encoded array of bytes</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array</param> + /// <param name="input_offset" type="Number">Offset in ASN.1 BER encoded array where decoding should be started</param> + /// <param name="input_length" type="Number">Maximum length of array of bytes which can be using in this function</param> + + var result_offset = this.value_block.fromBER(input_buffer, input_offset, (this.len_block.is_indefinite_form == true) ? input_length : this.len_block.length); + if(result_offset == (-1)) + { + this.error = this.value_block.error; + return result_offset; + } + + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.fromBuffer.call(this, this.value_block.value_hex); + + if(this.id_block.error.length == 0) + this.block_length += this.id_block.block_length; + + if(this.len_block.error.length == 0) + this.block_length += this.len_block.block_length; + + if(this.value_block.error.length == 0) + this.block_length += this.value_block.block_length; + + return result_offset; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.fromBuffer = + function(input_buffer) + { + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.fromString.call(this, String.fromCharCode.apply(null, new Uint8Array(input_buffer))); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.toBuffer = + function() + { + var str = in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.toString.call(this); + + var buffer = new ArrayBuffer(str.length); + var view = new Uint8Array(buffer); + + for(var i = 0; i < str.length; i++) + view[i] = str.charCodeAt(i); + + return buffer; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.fromDate = + function(input_date) + { + /// <summary>Create "GeneralizedTime" ASN.1 type from JavaScript "Date" type</summary> + + this.year = input_date.getUTCFullYear(); + this.month = input_date.getUTCMonth(); + this.day = input_date.getUTCDate(); + this.hour = input_date.getUTCHours(); + this.minute = input_date.getUTCMinutes(); + this.second = input_date.getUTCSeconds(); + this.millisecond = input_date.getUTCMilliseconds(); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.toDate = + function() + { + return (new Date(Date.UTC(this.year, this.month - 1, this.day, this.hour, this.minute, this.second, this.millisecond))); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.fromString = + function(input_string) + { + /// <summary>Create "GeneralizedTime" ASN.1 type from JavaScript "String" type</summary> + + // #region Initial variables + var isUTC = false; + + var timeString = ""; + var dateTimeString = ""; + var fractionPart = 0; + + var parser; + + var hourDifference = 0; + var minuteDifference = 0; + // #endregion + + // #region Convert as UTC time + if(input_string[input_string.length - 1] == "Z") + { + timeString = input_string.substr(0, input_string.length - 1); + + isUTC = true; + } + // #endregion + // #region Convert as local time + else + { + var number = new Number(input_string[input_string.length - 1]); + + if(isNaN(number.valueOf())) + throw new Error("Wrong input string for convertion"); + + timeString = input_string; + } + // #endregion + + // #region Check that we do not have a "+" and "-" symbols inside UTC time + if(isUTC) + { + if(timeString.indexOf("+") != (-1)) + throw new Error("Wrong input string for convertion"); + + if(timeString.indexOf("-") != (-1)) + throw new Error("Wrong input string for convertion"); + } + // #endregion + // #region Get "UTC time difference" in case of local time + else + { + var multiplier = 1; + var differencePosition = timeString.indexOf("+"); + var differenceString = ""; + + if(differencePosition == (-1)) + { + differencePosition = timeString.indexOf("-"); + multiplier = (-1); + } + + if(differencePosition != (-1)) + { + differenceString = timeString.substr(differencePosition + 1); + timeString = timeString.substr(0, differencePosition); + + if((differenceString.length != 2) && (differenceString.length != 4)) + throw new Error("Wrong input string for convertion"); + + var number = new Number(differenceString.substr(0, 2)); + + if(isNaN(number.valueOf())) + throw new Error("Wrong input string for convertion"); + + hourDifference = multiplier * number; + + if(differenceString.length == 4) + { + number = new Number(differenceString.substr(2, 2)); + + if(isNaN(number.valueOf())) + throw new Error("Wrong input string for convertion"); + + minuteDifference = multiplier * number; + } + } + } + // #endregion + + // #region Get position of fraction point + var fractionPointPosition = timeString.indexOf("."); // Check for "full stop" symbol + if(fractionPointPosition == (-1)) + fractionPointPosition = timeString.indexOf(","); // Check for "comma" symbol + // #endregion + + // #region Get fraction part + if(fractionPointPosition != (-1)) + { + var fractionPartCheck = new Number("0" + timeString.substr(fractionPointPosition)); + + if(isNaN(fractionPartCheck.valueOf())) + throw new Error("Wrong input string for convertion"); + + fractionPart = fractionPartCheck.valueOf(); + + dateTimeString = timeString.substr(0, fractionPointPosition); + } + else + dateTimeString = timeString; + // #endregion + + // #region Parse internal date + switch(true) + { + case (dateTimeString.length == 8): // "YYYYMMDD" + parser = /(\d{4})(\d{2})(\d{2})/ig; + if(fractionPointPosition !== (-1)) + throw new Error("Wrong input string for convertion"); // Here we should not have a "fraction point" + break; + case (dateTimeString.length == 10): // "YYYYMMDDHH" + parser = /(\d{4})(\d{2})(\d{2})(\d{2})/ig; + + if(fractionPointPosition !== (-1)) + { + var fractionResult = 60 * fractionPart; + this.minute = Math.floor(fractionResult); + + fractionResult = 60 * (fractionResult - this.minute); + this.second = Math.floor(fractionResult); + + fractionResult = 1000 * (fractionResult - this.second); + this.millisecond = Math.floor(fractionResult); + } + break; + case (dateTimeString.length == 12): // "YYYYMMDDHHMM" + parser = /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})/ig; + + if(fractionPointPosition !== (-1)) + { + var fractionResult = 60 * fractionPart; + this.second = Math.floor(fractionResult); + + fractionResult = 1000 * (fractionResult - this.second); + this.millisecond = Math.floor(fractionResult); + } + break; + case (dateTimeString.length == 14): // "YYYYMMDDHHMMSS" + parser = /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/ig; + + if(fractionPointPosition !== (-1)) + { + var fractionResult = 1000 * fractionPart; + this.millisecond = Math.floor(fractionResult); + } + break; + default: + throw new Error("Wrong input string for convertion"); + } + // #endregion + + // #region Put parsed values at right places + var parser_array = parser.exec(dateTimeString); + if(parser_array == null) + throw new Error("Wrong input string for convertion"); + + for(var j = 1; j < parser_array.length; j++) + { + switch(j) + { + case 1: + this.year = parseInt(parser_array[j], 10); + break; + case 2: + this.month = parseInt(parser_array[j], 10) - 1; // In JavaScript we have month range as "0 - 11" + break; + case 3: + this.day = parseInt(parser_array[j], 10); + break; + case 4: + this.hour = parseInt(parser_array[j], 10) + hourDifference; + break; + case 5: + this.minute = parseInt(parser_array[j], 10) + minuteDifference; + break; + case 6: + this.second = parseInt(parser_array[j], 10); + break; + default: + throw new Error("Wrong input string for convertion"); + } + } + // #endregion + + // #region Get final date + if(isUTC == false) + { + var tempDate = new Date(this.year, this.month, this.day, this.hour, this.minute, this.second, this.millisecond); + + this.year = tempDate.getUTCFullYear(); + this.month = tempDate.getUTCMonth(); + this.day = tempDate.getUTCDay(); + this.hour = tempDate.getUTCHours(); + this.minute = tempDate.getUTCMinutes(); + this.second = tempDate.getUTCSeconds(); + this.millisecond = tempDate.getUTCMilliseconds(); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.toString = + function() + { + var output_array = new Array(); + + output_array.push(in_window.org.pkijs.padNumber(this.year, 4)); + output_array.push(in_window.org.pkijs.padNumber(this.month, 2)); + output_array.push(in_window.org.pkijs.padNumber(this.day, 2)); + output_array.push(in_window.org.pkijs.padNumber(this.hour, 2)); + output_array.push(in_window.org.pkijs.padNumber(this.minute, 2)); + output_array.push(in_window.org.pkijs.padNumber(this.second, 2)); + if(this.millisecond != 0) + { + output_array.push("."); + output_array.push(in_window.org.pkijs.padNumber(this.millisecond, 3)); + } + output_array.push("Z"); + + return output_array.join(''); + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "GENERALIZEDTIME"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.VISIBLESTRING.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.GENERALIZEDTIME.prototype.block_name.call(this); + _object.year = this.year; + _object.month = this.month; + _object.day = this.day; + _object.hour = this.hour; + _object.minute = this.minute; + _object.second = this.second; + _object.millisecond = this.millisecond; + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.DATE = + function() + { + in_window.org.pkijs.asn1.UTF8STRING.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 31; // DATE + }; + //************************************************************************************** + in_window.org.pkijs.asn1.DATE.prototype = new in_window.org.pkijs.asn1.UTF8STRING(); + in_window.org.pkijs.asn1.DATE.constructor = in_window.org.pkijs.asn1.DATE; + //************************************************************************************** + in_window.org.pkijs.asn1.DATE.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "DATE"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.DATE.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.UTF8STRING.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.DATE.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.TIMEOFDAY = + function() + { + in_window.org.pkijs.asn1.UTF8STRING.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 32; // TIMEOFDAY + }; + //************************************************************************************** + in_window.org.pkijs.asn1.TIMEOFDAY.prototype = new in_window.org.pkijs.asn1.UTF8STRING(); + in_window.org.pkijs.asn1.TIMEOFDAY.constructor = in_window.org.pkijs.asn1.TIMEOFDAY; + //************************************************************************************** + in_window.org.pkijs.asn1.TIMEOFDAY.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "TIMEOFDAY"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.TIMEOFDAY.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.UTF8STRING.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.TIMEOFDAY.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.DATETIME = + function() + { + in_window.org.pkijs.asn1.UTF8STRING.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 33; // DATETIME + }; + //************************************************************************************** + in_window.org.pkijs.asn1.DATETIME.prototype = new in_window.org.pkijs.asn1.UTF8STRING(); + in_window.org.pkijs.asn1.DATETIME.constructor = in_window.org.pkijs.asn1.DATETIME; + //************************************************************************************** + in_window.org.pkijs.asn1.DATETIME.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "DATETIME"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.DATETIME.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.UTF8STRING.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.DATETIME.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.DURATION = + function() + { + in_window.org.pkijs.asn1.UTF8STRING.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 34; // DURATION + }; + //************************************************************************************** + in_window.org.pkijs.asn1.DURATION.prototype = new in_window.org.pkijs.asn1.UTF8STRING(); + in_window.org.pkijs.asn1.DURATION.constructor = in_window.org.pkijs.asn1.DURATION; + //************************************************************************************** + in_window.org.pkijs.asn1.DURATION.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "DURATION"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.DURATION.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.UTF8STRING.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.DURATION.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.TIME = + function() + { + in_window.org.pkijs.asn1.UTF8STRING.call(this, arguments[0]); + + this.id_block.tag_class = 1; // UNIVERSAL + this.id_block.tag_number = 14; // TIME + }; + //************************************************************************************** + in_window.org.pkijs.asn1.TIME.prototype = new in_window.org.pkijs.asn1.UTF8STRING(); + in_window.org.pkijs.asn1.TIME.constructor = in_window.org.pkijs.asn1.TIME; + //************************************************************************************** + in_window.org.pkijs.asn1.TIME.prototype.block_name = + function() + { + /// <summary>Aux function, need to get a block name. Need to have it here for inhiritence</summary> + + return "TIME"; + }; + //************************************************************************************** + in_window.org.pkijs.asn1.TIME.prototype.toJSON = + function() + { + /// <summary>Convertion for the block to JSON object</summary> + + var _object = in_window.org.pkijs.asn1.UTF8STRING.prototype.toJSON.call(this); + + _object.block_name = in_window.org.pkijs.asn1.TIME.prototype.block_name.call(this); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of special ASN.1 schema type CHOICE + //************************************************************************************** + in_window.org.pkijs.asn1.CHOICE = + function() + { + if(arguments[0] instanceof Object) + { + this.value = in_window.org.pkijs.getValue(arguments[0], "value", new Array()); // Array of ASN.1 types for make a choice from + this.optional = in_window.org.pkijs.getValue(arguments[0], "optional", false); + } + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of special ASN.1 schema type ANY + //************************************************************************************** + in_window.org.pkijs.asn1.ANY = + function() + { + if(arguments[0] instanceof Object) + { + this.name = in_window.org.pkijs.getValue(arguments[0], "name", ""); + this.optional = in_window.org.pkijs.getValue(arguments[0], "optional", false); + } + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of special ASN.1 schema type REPEATED + //************************************************************************************** + in_window.org.pkijs.asn1.REPEATED = + function() + { + if(arguments[0] instanceof Object) + { + this.name = in_window.org.pkijs.getValue(arguments[0], "name", ""); + this.optional = in_window.org.pkijs.getValue(arguments[0], "optional", false); + this.value = in_window.org.pkijs.getValue(arguments[0], "value", new in_window.org.pkijs.asn1.ANY()); + this.local = in_window.org.pkijs.getValue(arguments[0], "local", false); // Could local or global array to store elements + } + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Major ASN.1 BER decoding function + //************************************************************************************** + function fromBER_raw(input_buffer, input_offset, input_length) + { + var incoming_offset = input_offset; // Need to store initial offset since "input_offset" is changing in the function + + // #region Local function changing a type for ASN.1 classes + function local_change_type(input_object, new_type) + { + if(input_object instanceof new_type) + return input_object; + + var new_object = new new_type(); + new_object.id_block = input_object.id_block; + new_object.len_block = input_object.len_block; + new_object.warnings = input_object.warnings; + new_object.value_before_decode = util_copybuf(input_object.value_before_decode); + + return new_object; + } + // #endregion + + // #region Create a basic ASN.1 type since we need to return errors and warnings from the function + var return_object = new in_window.org.pkijs.asn1.ASN1_block(); + // #endregion + + // #region Basic check for parameters + if(check_buffer_params(input_buffer, input_offset, input_length) === false) + { + return_object.error = "Wrong input parameters"; + return { + offset: (-1), + result: return_object + }; + } + // #endregion + + // #region Getting Uint8Array from ArrayBuffer + var int_buffer = new Uint8Array(input_buffer, input_offset, input_length); + // #endregion + + // #region Initial checks + if(int_buffer.length == 0) + { + this.error = "Zero buffer length"; + return { + offset: (-1), + result: return_object + }; + } + // #endregion + + // #region Decode indentifcation block of ASN.1 BER structure + var result_offset = return_object.id_block.fromBER(input_buffer, input_offset, input_length); + return_object.warnings.concat(return_object.id_block.warnings); + if(result_offset == (-1)) + { + return_object.error = return_object.id_block.error; + return { + offset: (-1), + result: return_object + }; + } + + input_offset = result_offset; + input_length -= return_object.id_block.block_length; + // #endregion + + // #region Decode length block of ASN.1 BER structure + result_offset = return_object.len_block.fromBER(input_buffer, input_offset, input_length); + return_object.warnings.concat(return_object.len_block.warnings); + if(result_offset == (-1)) + { + return_object.error = return_object.len_block.error; + return { + offset: (-1), + result: return_object + }; + } + + input_offset = result_offset; + input_length -= return_object.len_block.block_length; + // #endregion + + // #region Check for usign indefinite length form in encoding for primitive types + if((return_object.id_block.is_constructed == false) && + (return_object.len_block.is_indefinite_form == true)) + { + return_object.error = new String("Indefinite length form used for primitive encoding form"); + return { + offset: (-1), + result: return_object + }; + } + // #endregion + + // #region Switch ASN.1 block type + var new_asn1_type = in_window.org.pkijs.asn1.ASN1_block; + + switch(return_object.id_block.tag_class) + { + // #region UNIVERSAL + case 1: + // #region Check for reserved tag numbers + if((return_object.id_block.tag_number >= 37) && + (return_object.id_block.is_hex_only == false)) + { + return_object.error = "UNIVERSAL 37 and upper tags are reserved by ASN.1 standard"; + return { + offset: (-1), + result: return_object + }; + } + // #endregion + + switch(return_object.id_block.tag_number) + { + // #region EOC type + case 0: + // #region Check for EOC type + if((return_object.id_block.is_constructed == true) && + (return_object.len_block.length > 0)) + { + return_object.error = "Type [UNIVERSAL 0] is reserved"; + return { + offset: (-1), + result: return_object + }; + } + // #endregion + + new_asn1_type = in_window.org.pkijs.asn1.EOC; + + break; + // #endregion + // #region BOOLEAN type + case 1: + new_asn1_type = in_window.org.pkijs.asn1.BOOLEAN; + break; + // #endregion + // #region INTEGER type + case 2: + new_asn1_type = in_window.org.pkijs.asn1.INTEGER; + break; + // #endregion + // #region BITSTRING type + case 3: + new_asn1_type = in_window.org.pkijs.asn1.BITSTRING; + break; + // #endregion + // #region OCTETSTRING type + case 4: + new_asn1_type = in_window.org.pkijs.asn1.OCTETSTRING; + break; + // #endregion + // #region NULL type + case 5: + new_asn1_type = in_window.org.pkijs.asn1.NULL; + break; + // #endregion + // #region OBJECT IDENTIFIER type + case 6: + new_asn1_type = in_window.org.pkijs.asn1.OID; + break; + // #endregion + // #region ENUMERATED type + case 10: + new_asn1_type = in_window.org.pkijs.asn1.ENUMERATED; + break; + // #endregion + // #region UTF8STRING type + case 12: + new_asn1_type = in_window.org.pkijs.asn1.UTF8STRING; + break; + // #endregion + // #region TIME type + case 14: + new_asn1_type = in_window.org.pkijs.asn1.TIME; + break; + // #endregion + // #region ASN.1 reserved type + case 15: + return_object.error = "[UNIVERSAL 15] is reserved by ASN.1 standard"; + return { + offset: (-1), + result: return_object + }; + break; + // #endregion + // #region SEQUENCE type + case 16: + new_asn1_type = in_window.org.pkijs.asn1.SEQUENCE; + break; + // #endregion + // #region SET type + case 17: + new_asn1_type = in_window.org.pkijs.asn1.SET; + break; + // #endregion + // #region NUMERICSTRING type + case 18: + new_asn1_type = in_window.org.pkijs.asn1.NUMERICSTRING; + break; + // #endregion + // #region PRINTABLESTRING type + case 19: + new_asn1_type = in_window.org.pkijs.asn1.PRINTABLESTRING; + break; + // #endregion + // #region TELETEXSTRING type + case 20: + new_asn1_type = in_window.org.pkijs.asn1.TELETEXSTRING; + break; + // #endregion + // #region VIDEOTEXSTRING type + case 21: + new_asn1_type = in_window.org.pkijs.asn1.VIDEOTEXSTRING; + break; + // #endregion + // #region IA5STRING type + case 22: + new_asn1_type = in_window.org.pkijs.asn1.IA5STRING; + break; + // #endregion + // #region UTCTIME type + case 23: + new_asn1_type = in_window.org.pkijs.asn1.UTCTIME; + break; + // #endregion + // #region GENERALIZEDTIME type + case 24: + new_asn1_type = in_window.org.pkijs.asn1.GENERALIZEDTIME; + break; + // #endregion + // #region GRAPHICSTRING type + case 25: + new_asn1_type = in_window.org.pkijs.asn1.GRAPHICSTRING; + break; + // #endregion + // #region VISIBLESTRING type + case 26: + new_asn1_type = in_window.org.pkijs.asn1.VISIBLESTRING; + break; + // #endregion + // #region GENERALSTRING type + case 27: + new_asn1_type = in_window.org.pkijs.asn1.GENERALSTRING; + break; + // #endregion + // #region UNIVERSALSTRING type + case 28: + new_asn1_type = in_window.org.pkijs.asn1.UNIVERSALSTRING; + break; + // #endregion + // #region CHARACTERSTRING type + case 29: + new_asn1_type = in_window.org.pkijs.asn1.CHARACTERSTRING; + break; + // #endregion + // #region BMPSTRING type + case 30: + new_asn1_type = in_window.org.pkijs.asn1.BMPSTRING; + break; + // #endregion + // #region DATE type + case 31: + new_asn1_type = in_window.org.pkijs.asn1.DATE; + break; + // #endregion + // #region TIMEOFDAY type + case 32: + new_asn1_type = in_window.org.pkijs.asn1.TIMEOFDAY; + break; + // #endregion + // #region DATE-TIME type + case 33: + new_asn1_type = in_window.org.pkijs.asn1.DATETIME; + break; + // #endregion + // #region DURATION type + case 34: + new_asn1_type = in_window.org.pkijs.asn1.DURATION; + break; + // #endregion + // #region default + default: + { + var new_object; + + if(return_object.id_block.is_constructed == true) + new_object = new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED(); + else + new_object = new in_window.org.pkijs.asn1.ASN1_PRIMITIVE(); + + new_object.id_block = return_object.id_block; + new_object.len_block = return_object.len_block; + new_object.warnings = return_object.warnings; + + return_object = new_object; + + result_offset = return_object.fromBER(input_buffer, input_offset, input_length); + } + // #endregion + } + break; + // #endregion + // #region All other tag classes + case 2: // APPLICATION + case 3: // CONTEXT-SPECIFIC + case 4: // PRIVATE + default: + { + if(return_object.id_block.is_constructed == true) + new_asn1_type = in_window.org.pkijs.asn1.ASN1_CONSTRUCTED; + else + new_asn1_type = in_window.org.pkijs.asn1.ASN1_PRIMITIVE; + } + // #endregion + } + // #endregion + + // #region Change type and perform BER decoding + return_object = local_change_type(return_object, new_asn1_type); + result_offset = return_object.fromBER(input_buffer, input_offset, (return_object.len_block.is_indefinite_form == true) ? input_length : return_object.len_block.length); + // #endregion + + // #region Coping incoming buffer for entire ASN.1 block + return_object.value_before_decode = util_copybuf_offset(input_buffer, incoming_offset, return_object.block_length); + // #endregion + + return { + offset: result_offset, + result: return_object + }; + } + //************************************************************************************** + in_window.org.pkijs.fromBER = + function(input_buffer) + { + /// <summary>Major function for decoding ASN.1 BER array into internal library structuries</summary> + /// <param name="input_buffer" type="ArrayBuffer">ASN.1 BER encoded array of bytes</param> + + if(input_buffer.byteLength == 0) + { + var result = new in_window.org.pkijs.asn1.ASN1_block(); + result.error = "Input buffer has zero length"; + + return { + offset: (-1), + result: result + }; + } + + return fromBER_raw(input_buffer, 0, input_buffer.byteLength); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Major scheme verification function + //************************************************************************************** + in_window.org.pkijs.compareSchema = + function(root, input_asn1_data, input_asn1_schema) + { + // #region Special case for CHOICE schema element type + if(input_asn1_schema instanceof in_window.org.pkijs.asn1.CHOICE) + { + var choice_result = false; + + for(var j = 0; j < input_asn1_schema.value.length; j++) + { + var result = in_window.org.pkijs.compareSchema(root, input_asn1_data, input_asn1_schema.value[j]); + if(result.verified === true) + return { + verified: true, + result: root + }; + } + + if(choice_result === false) + { + var _result = { + verified: false, + result: { + error: "Wrong values for CHOICE type" + } + }; + + if(input_asn1_schema.hasOwnProperty('name')) + _result.name = input_asn1_schema.name; + + return _result; + } + } + // #endregion + + // #region Special case for ANY schema element type + if(input_asn1_schema instanceof in_window.org.pkijs.asn1.ANY) + { + // #region Add named component of ASN.1 schema + if(input_asn1_schema.hasOwnProperty('name')) + root[input_asn1_schema.name] = input_asn1_data; + // #endregion + + return { + verified: true, + result: root + }; + } + // #endregion + + // #region Initial check + if((root instanceof Object) === false) + return { + verified: false, + result: { error: "Wrong root object" } + }; + + if((input_asn1_data instanceof Object) === false) + return { + verified: false, + result: { error: "Wrong ASN.1 data" } + }; + + if((input_asn1_schema instanceof Object) === false) + return { + verified: false, + result: { error: "Wrong ASN.1 schema" } + }; + + if(('id_block' in input_asn1_schema) === false) + return { + verified: false, + result: { error: "Wrong ASN.1 schema" } + }; + // #endregion + + // #region Comparing id_block properties in ASN.1 data and ASN.1 schema + // #region Encode and decode ASN.1 schema id_block + /// <remarks>This encoding/decoding is neccessary because could be an errors in schema definition</remarks> + if(('fromBER' in input_asn1_schema.id_block) === false) + return { + verified: false, + result: { error: "Wrong ASN.1 schema" } + }; + + if(('toBER' in input_asn1_schema.id_block) === false) + return { + verified: false, + result: { error: "Wrong ASN.1 schema" } + }; + + var encoded_id = input_asn1_schema.id_block.toBER(false); + if(encoded_id.byteLength === 0) + return { + verified: false, + result: { error: "Error encoding id_block for ASN.1 schema" } + }; + + var decoded_offset = input_asn1_schema.id_block.fromBER(encoded_id, 0, encoded_id.byteLength); + if(decoded_offset === (-1)) + return { + verified: false, + result: { error: "Error decoding id_block for ASN.1 schema" } + }; + // #endregion + + // #region tag_class + if(input_asn1_schema.id_block.hasOwnProperty('tag_class') === false) + return { + verified: false, + result: { error: "Wrong ASN.1 schema" } + }; + + if(input_asn1_schema.id_block.tag_class !== input_asn1_data.id_block.tag_class) + return { + verified: false, + result: root + }; + // #endregion + // #region tag_number + if(input_asn1_schema.id_block.hasOwnProperty('tag_number') === false) + return { + verified: false, + result: { error: "Wrong ASN.1 schema" } + }; + + if(input_asn1_schema.id_block.tag_number !== input_asn1_data.id_block.tag_number) + return { + verified: false, + result: root + }; + // #endregion + // #region is_constructed + if(input_asn1_schema.id_block.hasOwnProperty('is_constructed') === false) + return { + verified: false, + result: { error: "Wrong ASN.1 schema" } + }; + + if(input_asn1_schema.id_block.is_constructed !== input_asn1_data.id_block.is_constructed) + return { + verified: false, + result: root + }; + // #endregion + // #region is_hex_only + if(('is_hex_only' in input_asn1_schema.id_block) === false) // Since 'is_hex_only' is an inhirited property + return { + verified: false, + result: { error: "Wrong ASN.1 schema" } + }; + + if(input_asn1_schema.id_block.is_hex_only !== input_asn1_data.id_block.is_hex_only) + return { + verified: false, + result: root + }; + // #endregion + // #region value_hex + if(input_asn1_schema.id_block.is_hex_only === true) + { + if(('value_hex' in input_asn1_schema.id_block) === false) // Since 'value_hex' is an inhirited property + return { + verified: false, + result: { error: "Wrong ASN.1 schema" } + }; + + var schema_view = new Uint8Array(input_asn1_schema.id_block.value_hex); + var asn1_view = new Uint8Array(input_asn1_data.id_block.value_hex); + + if(schema_view.length !== asn1_view.length) + return { + verified: false, + result: root + }; + + for(var i = 0; i < schema_view.length; i++) + { + if(schema_view[i] !== asn1_view[1]) + return { + verified: false, + result: root + }; + } + } + // #endregion + // #endregion + + // #region Add named component of ASN.1 schema + if(input_asn1_schema.hasOwnProperty('name')) + { + input_asn1_schema.name = input_asn1_schema.name.replace(/^\s+|\s+$/g, ''); + if(input_asn1_schema.name !== "") + root[input_asn1_schema.name] = input_asn1_data; + } + // #endregion + + // #region Getting next ASN.1 block for comparition + if(input_asn1_schema.id_block.is_constructed === true) + { + var admission = 0; + var result = { verified: false }; + + var max_length = input_asn1_schema.value_block.value.length; + + if(max_length > 0) + { + if(input_asn1_schema.value_block.value[0] instanceof in_window.org.pkijs.asn1.REPEATED) + max_length = input_asn1_data.value_block.value.length; + } + + // #region Special case when constructive value has no elements + if(max_length === 0) + return { + verified: true, + result: root + }; + // #endregion + + // #region Special case when "input_asn1_data" has no values and "input_asn1_schema" has all optional values + if((input_asn1_data.value_block.value.length === 0) && + (input_asn1_schema.value_block.value.length !== 0)) + { + var _optional = true; + + for(var i = 0; i < input_asn1_schema.value_block.value.length; i++) + _optional = _optional && (input_asn1_schema.value_block.value[i].optional || false); + + if(_optional === true) + { + return { + verified: true, + result: root + }; + } + else + { + // #region Delete early added name of block + if(input_asn1_schema.hasOwnProperty('name')) + { + input_asn1_schema.name = input_asn1_schema.name.replace(/^\s+|\s+$/g, ''); + if(input_asn1_schema.name !== "") + delete root[input_asn1_schema.name]; + } + // #endregion + + root.error = "Inconsistent object length"; + + return { + verified: false, + result: root + }; + } + } + // #endregion + + for(var i = 0; i < max_length; i++) + { + // #region Special case when there is an "optional" element of ASN.1 schema at the end + if((i - admission) >= input_asn1_data.value_block.value.length) + { + if(input_asn1_schema.value_block.value[i].optional === false) + { + var _result = { + verified: false, + result: root + }; + + root.error = "Inconsistent length between ASN.1 data and schema"; + + // #region Delete early added name of block + if(input_asn1_schema.hasOwnProperty('name')) + { + input_asn1_schema.name = input_asn1_schema.name.replace(/^\s+|\s+$/g, ''); + if(input_asn1_schema.name !== "") + { + delete root[input_asn1_schema.name]; + _result.name = input_asn1_schema.name; + } + } + // #endregion + + return _result; + } + } + // #endregion + else + { + // #region Special case for REPEATED type of ASN.1 schema element + if(input_asn1_schema.value_block.value[0] instanceof in_window.org.pkijs.asn1.REPEATED) + { + result = in_window.org.pkijs.compareSchema(root, input_asn1_data.value_block.value[i], input_asn1_schema.value_block.value[0].value); + if(result.verified === false) + { + if(input_asn1_schema.value_block.value[0].optional === true) + admission++; + else + { + // #region Delete early added name of block + if(input_asn1_schema.hasOwnProperty('name')) + { + input_asn1_schema.name = input_asn1_schema.name.replace(/^\s+|\s+$/g, ''); + if(input_asn1_schema.name !== "") + delete root[input_asn1_schema.name]; + } + // #endregion + + return result; + } + } + + if(("name" in input_asn1_schema.value_block.value[0]) && (input_asn1_schema.value_block.value[0].name.length > 0)) + { + var array_root = {}; + + if(("local" in input_asn1_schema.value_block.value[0]) && (input_asn1_schema.value_block.value[0].local === true)) + array_root = input_asn1_data; + else + array_root = root; + + if(typeof array_root[input_asn1_schema.value_block.value[0].name] === "undefined") + array_root[input_asn1_schema.value_block.value[0].name] = new Array(); + + array_root[input_asn1_schema.value_block.value[0].name].push(input_asn1_data.value_block.value[i]); + } + } + // #endregion + else + { + result = in_window.org.pkijs.compareSchema(root, input_asn1_data.value_block.value[i - admission], input_asn1_schema.value_block.value[i]); + if(result.verified === false) + { + if(input_asn1_schema.value_block.value[i].optional === true) + admission++; + else + { + // #region Delete early added name of block + if(input_asn1_schema.hasOwnProperty('name')) + { + input_asn1_schema.name = input_asn1_schema.name.replace(/^\s+|\s+$/g, ''); + if(input_asn1_schema.name !== "") + delete root[input_asn1_schema.name]; + } + // #endregion + + return result; + } + } + } + } + } + + if(result.verified === false) // The situation may take place if last element is "optional" and verification failed + { + var _result = { + verified: false, + result: root + }; + + // #region Delete early added name of block + if(input_asn1_schema.hasOwnProperty('name')) + { + input_asn1_schema.name = input_asn1_schema.name.replace(/^\s+|\s+$/g, ''); + if(input_asn1_schema.name !== "") + { + delete root[input_asn1_schema.name]; + _result.name = input_asn1_schema.name; + } + } + // #endregion + + return _result; + } + + return { + verified: true, + result: root + }; + } + // #endregion + // #region Ability to parse internal value for primitive-encoded value (value of OCTETSTRING, for example) + else + { + if( ("primitive_schema" in input_asn1_schema) && + ("value_hex" in input_asn1_data.value_block) ) + { + // #region Decoding of raw ASN.1 data + var asn1 = in_window.org.pkijs.fromBER(input_asn1_data.value_block.value_hex); + if(asn1.offset === (-1)) + { + var _result = { + verified: false, + result: asn1.result + }; + + // #region Delete early added name of block + if(input_asn1_schema.hasOwnProperty('name')) + { + input_asn1_schema.name = input_asn1_schema.name.replace(/^\s+|\s+$/g, ''); + if(input_asn1_schema.name !== "") + { + delete root[input_asn1_schema.name]; + _result.name = input_asn1_schema.name; + } + } + // #endregion + + return _result; + } + // #endregion + + return in_window.org.pkijs.compareSchema(root, asn1.result, input_asn1_schema.primitive_schema); + } + else + return { + verified: true, + result: root + }; + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.verifySchema = + function(input_buffer, input_schema) + { + // #region Initial check + if((input_schema instanceof Object) === false) + return { + verified: false, + result: { error: "Wrong ASN.1 schema type" } + }; + // #endregion + + // #region Decoding of raw ASN.1 data + var asn1 = in_window.org.pkijs.fromBER(input_buffer); + if(asn1.offset === (-1)) + return { + verified: false, + result: asn1.result + }; + // #endregion + + // #region Compare ASN.1 struct with input schema + return in_window.org.pkijs.compareSchema(asn1.result, asn1.result, input_schema); + // #endregion + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Major function converting JSON to ASN.1 objects + //************************************************************************************** + in_window.org.pkijs.fromJSON = + function(json) + { + /// <summary>Converting from JSON to ASN.1 objects</summary> + /// <param name="json" type="String|Object">JSON string or object to convert to ASN.1 objects</param> + }; + //************************************************************************************** + // #endregion + //************************************************************************************** +} +)(typeof exports !== "undefined" ? exports : window);
\ No newline at end of file diff --git a/dom/webauthn/tests/pkijs/common.js b/dom/webauthn/tests/pkijs/common.js new file mode 100644 index 0000000000..1bc9eda7f8 --- /dev/null +++ b/dom/webauthn/tests/pkijs/common.js @@ -0,0 +1,1542 @@ +/* + * Copyright (c) 2014, GMO GlobalSign + * Copyright (c) 2015, Peculiar Ventures + * All rights reserved. + * + * Author 2014-2015, Yury Strozhevsky <www.strozhevsky.com>. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + */ +( +function(in_window) +{ + //************************************************************************************** + // #region Declaration of global variables + //************************************************************************************** + // #region "org" namespace + if(typeof in_window.org === "undefined") + in_window.org = {}; + else + { + if(typeof in_window.org !== "object") + throw new Error("Name org already exists and it's not an object"); + } + // #endregion + + // #region "org.pkijs" namespace + if(typeof in_window.org.pkijs === "undefined") + in_window.org.pkijs = {}; + else + { + if(typeof in_window.org.pkijs !== "object") + throw new Error("Name org.pkijs already exists and it's not an object" + " but " + (typeof in_window.org.pkijs)); + } + // #endregion + + // #region "local" namespace + var local = {}; + // #endregion + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Settings for "crypto engine" + //************************************************************************************** + local.engine = { + name: "none", + crypto: null, + subtle: null + }; + + if(typeof window != "undefined") + { + if("crypto" in window) + { + var engineName = "webcrypto"; + var cryptoObject = window.crypto; + var subtleObject = null; + + // Apple Safari support + if("webkitSubtle" in window.crypto) + subtleObject = window.crypto.webkitSubtle; + + if("subtle" in window.crypto) + subtleObject = window.crypto.subtle; + + local.engine = { + name: engineName, + crypto: cryptoObject, + subtle: subtleObject + }; + } + } + //************************************************************************************** + in_window.org.pkijs.setEngine = + function(name, crypto, subtle) + { + /// <summary>Setting the global "crypto engine" parameters</summary> + /// <param name="name" type="String">Auxiliary name for "crypto engine"</param> + /// <param name="crypto" type="Object">Object handling all root cryptographic requests (in fact currently it must handle only "getRandomValues")</param> + /// <param name="subtle" type="Object">Object handling all main cryptographic requests</param> + + local.engine = { + name: name, + crypto: crypto, + subtle: subtle + }; + }; + //************************************************************************************** + in_window.org.pkijs.getEngine = + function() + { + return local.engine; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Declaration of common functions + //************************************************************************************** + in_window.org.pkijs.emptyObject = + function() + { + this.toJSON = function() + { + return {}; + }; + this.toSchema = function() + { + return {}; + }; + }; + //************************************************************************************** + in_window.org.pkijs.getNames = + function(arg) + { + /// <summary>Get correct "names" array for all "schema" objects</summary> + + var names = {}; + + if(arg instanceof Object) + names = (arg.names || {}); + + return names; + }; + //************************************************************************************** + in_window.org.pkijs.inheriteObjectFields = + function(from) + { + for(var i in from.prototype) + { + if(typeof from.prototype[i] === "function") + continue; + + this[i] = from.prototype[i]; + } + }; + //************************************************************************************** + in_window.org.pkijs.getUTCDate = + function(date) + { + /// <summary>Making UTC date from local date</summary> + /// <param name="date" type="Date">Date to convert from</param> + + var current_date = date; + return new Date(current_date.getTime() + (current_date.getTimezoneOffset() * 60000)); + }; + //************************************************************************************** + in_window.org.pkijs.padNumber = + function(input_number, full_length) + { + var str = input_number.toString(10); + var dif = full_length - str.length; + + var padding = new Array(dif); + for(var i = 0; i < dif; i++) + padding[i] = '0'; + + var padding_string = padding.join(''); + + return padding_string.concat(str); + }; + //************************************************************************************** + in_window.org.pkijs.getValue = + function(args, item, default_value) + { + if(item in args) + return args[item]; + else + return default_value; + }; + //************************************************************************************** + in_window.org.pkijs.isEqual_view = + function(input_view1, input_view2) + { + /// <summary>Compare two Uint8Arrays</summary> + /// <param name="input_view1" type="Uint8Array">First Uint8Array for comparision</param> + /// <param name="input_view2" type="Uint8Array">Second Uint8Array for comparision</param> + + if(input_view1.length !== input_view2.length) + return false; + + for(var i = 0; i < input_view1.length; i++) + { + if(input_view1[i] != input_view2[i]) + return false; + } + + return true; + }; + //************************************************************************************** + in_window.org.pkijs.isEqual_buffer = + function(input_buffer1, input_buffer2) + { + /// <summary>Compare two array buffers</summary> + /// <param name="input_buffer1" type="ArrayBuffer">First ArrayBuffer for comparision</param> + /// <param name="input_buffer2" type="ArrayBuffer">Second ArrayBuffer for comparision</param> + + if(input_buffer1.byteLength != input_buffer2.byteLength) + return false; + + var view1 = new Uint8Array(input_buffer1); + var view2 = new Uint8Array(input_buffer2); + + return in_window.org.pkijs.isEqual_view(view1, view2); + }; + //************************************************************************************** + in_window.org.pkijs.concat_buffers = + function(input_buf1, input_buf2) + { + /// <summary>Concatenate two ArrayBuffers</summary> + /// <param name="input_buf1" type="ArrayBuffer">First ArrayBuffer (first part of concatenated array)</param> + /// <param name="input_buf2" type="ArrayBuffer">Second ArrayBuffer (second part of concatenated array)</param> + + var input_view1 = new Uint8Array(input_buf1); + var input_view2 = new Uint8Array(input_buf2); + + var ret_buf = new ArrayBuffer(input_buf1.byteLength + input_buf2.byteLength); + var ret_view = new Uint8Array(ret_buf); + + for(var i = 0; i < input_buf1.byteLength; i++) + ret_view[i] = input_view1[i]; + + for(var j = 0; j < input_buf2.byteLength; j++) + ret_view[input_buf1.byteLength + j] = input_view2[j]; + + return ret_buf; + }; + //************************************************************************************** + in_window.org.pkijs.copyBuffer = + function(input_buffer) + { + var result = new ArrayBuffer(input_buffer.byteLength); + + var resultView = new Uint8Array(result); + var inputView = new Uint8Array(input_buffer); + + for(var i = 0; i < inputView.length; i++) + resultView[i] = inputView[i]; + + return result; + }; + //************************************************************************************** + in_window.org.pkijs.getCrypto = + function() + { + var crypto_temp; + + if(local.engine.subtle !== null) + crypto_temp = local.engine.subtle; + + return crypto_temp; + }; + //************************************************************************************** + in_window.org.pkijs.stringPrep = + function(input_string) + { + /// <summary>String preparation function. In a future here will be realization of algorithm from RFC4518.</summary> + /// <param name="input_string" type="String">JavaScript string. As soon as for each ASN.1 string type we have a specific transformation function here we will work with pure JavaScript string</param> + /// <returns type="String">Formated string</returns> + + var result = input_string.replace(/^\s+|\s+$/g, ""); // Trim input string + result = result.replace(/\s+/g, " "); // Change all sequence of SPACE down to SPACE char + result = result.toLowerCase(); + + return result; + }; + //************************************************************************************** + in_window.org.pkijs.bufferToHexCodes = + function(input_buffer, input_offset, input_lenght) + { + var result = ""; + + var int_buffer = new Uint8Array(input_buffer, input_offset, input_lenght); + + for(var i = 0; i < int_buffer.length; i++) + { + var str = int_buffer[i].toString(16).toUpperCase(); + result = result + ((str.length === 1) ? "0" : "") + str; + } + + return result; + }; + //************************************************************************************** + in_window.org.pkijs.bufferFromHexCodes = + function(hexString) + { + /// <summary>Create an ArrayBuffer from string having hexdecimal codes</summary> + /// <param name="hexString" type="String">String to create ArrayBuffer from</param> + + // #region Initial variables + var stringLength = hexString.length; + + var resultBuffer = new ArrayBuffer(stringLength >> 1); + var resultView = new Uint8Array(resultBuffer); + + var hex_map = {}; + + hex_map['0'] = 0x00; + hex_map['1'] = 0x01; + hex_map['2'] = 0x02; + hex_map['3'] = 0x03; + hex_map['4'] = 0x04; + hex_map['5'] = 0x05; + hex_map['6'] = 0x06; + hex_map['7'] = 0x07; + hex_map['8'] = 0x08; + hex_map['9'] = 0x09; + hex_map['A'] = 0x0A; + hex_map['a'] = 0x0A; + hex_map['B'] = 0x0B; + hex_map['b'] = 0x0B; + hex_map['C'] = 0x0C; + hex_map['c'] = 0x0C; + hex_map['D'] = 0x0D; + hex_map['d'] = 0x0D; + hex_map['E'] = 0x0E; + hex_map['e'] = 0x0E; + hex_map['F'] = 0x0F; + hex_map['f'] = 0x0F; + + var j = 0; + var temp = 0x00; + // #endregion + + // #region Convert char-by-char + for(var i = 0; i < stringLength; i++) + { + if(!(i % 2)) + temp = hex_map[hexString.charAt(i)] << 4; + else + { + temp |= hex_map[hexString.charAt(i)]; + + resultView[j] = temp; + j++; + } + } + // #endregion + + return resultBuffer; + }; + //************************************************************************************** + in_window.org.pkijs.getRandomValues = + function(view) + { + /// <param name="view" type="Uint8Array">New array which gives a length for random value</param> + + if(local.engine.crypto !== null) + return local.engine.crypto.getRandomValues(view); + else + throw new Error("No support for Web Cryptography API"); + }; + //************************************************************************************** + in_window.org.pkijs.getAlgorithmParameters = + function(algorithmName, operation) + { + /// <param name="algorithmName" type="String">Algorithm name to get common parameters for</param> + /// <param name="operation" type="String">Kind of operation: "sign", "encrypt", "generatekey", "importkey", "exportkey", "verify"</param> + + var result = { + algorithm: {}, + usages: [] + }; + + switch(algorithmName.toUpperCase()) + { + case "RSASSA-PKCS1-V1_5": + switch(operation.toLowerCase()) + { + case "generatekey": + result = { + algorithm: { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { + name: "SHA-256" + } + }, + usages: ["sign", "verify"] + }; + break; + case "verify": + case "sign": + case "importkey": + result = { + algorithm: { + name: "RSASSA-PKCS1-v1_5", + hash: { + name: "SHA-256" + } + }, + usages: ["verify"] // For importKey("pkcs8") usage must be "sign" only + }; + break; + case "exportkey": + default: + return { + algorithm: { + name: "RSASSA-PKCS1-v1_5" + }, + usages: [] + }; + } + break; + case "RSA-PSS": + switch(operation.toLowerCase()) + { + case "sign": + case "verify": + result = { + algorithm: { + name: "RSA-PSS", + hash: { + name: "SHA-1" + }, + saltLength: 20 + }, + usages: ["sign", "verify"] + }; + break; + case "generatekey": + result = { + algorithm: { + name: "RSA-PSS", + modulusLength: 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { + name: "SHA-1" + } + }, + usages: ["sign", "verify"] + }; + break; + case "importkey": + result = { + algorithm: { + name: "RSA-PSS", + hash: { + name: "SHA-1" + } + }, + usages: ["verify"] // For importKey("pkcs8") usage must be "sign" only + }; + break; + case "exportkey": + default: + return { + algorithm: { + name: "RSA-PSS" + }, + usages: [] + }; + } + break; + case "RSA-OAEP": + switch(operation.toLowerCase()) + { + case "encrypt": + case "decrypt": + result = { + algorithm: { + name: "RSA-OAEP" + }, + usages: ["encrypt", "decrypt"] + }; + break; + break; + case "generatekey": + result = { + algorithm: { + name: "RSA-OAEP", + modulusLength: 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { + name: "SHA-256" + } + }, + usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] + }; + break; + case "importkey": + result = { + algorithm: { + name: "RSA-OAEP", + hash: { + name: "SHA-256" + } + }, + usages: ["encrypt"] // encrypt for "spki" and decrypt for "pkcs8" + }; + break; + case "exportkey": + default: + return { + algorithm: { + name: "RSA-OAEP" + }, + usages: [] + }; + } + break; + case "ECDSA": + switch(operation.toLowerCase()) + { + case "generatekey": + result = { + algorithm: { + name: "ECDSA", + namedCurve: "P-256" + }, + usages: ["sign", "verify"] + }; + break; + case "importkey": + result = { + algorithm: { + name: "ECDSA", + namedCurve: "P-256" + }, + usages: ["verify"] // "sign" for "pkcs8" + }; + break; + case "verify": + case "sign": + result = { + algorithm: { + name: "ECDSA", + hash: { + name: "SHA-256" + } + }, + usages: ["sign"] + }; + break; + default: + return { + algorithm: { + name: "ECDSA" + }, + usages: [] + }; + } + break; + case "ECDH": + switch(operation.toLowerCase()) + { + case "exportkey": + case "importkey": + case "generatekey": + result = { + algorithm: { + name: "ECDH", + namedCurve: "P-256" + }, + usages: ["deriveKey", "deriveBits"] + }; + break; + case "derivekey": + case "derivebits": + result = { + algorithm: { + name: "ECDH", + namedCurve: "P-256", + public: [] // Must be a "publicKey" + }, + usages: ["encrypt", "decrypt"] + }; + break; + default: + return { + algorithm: { + name: "ECDH" + }, + usages: [] + }; + } + break; + case "AES-CTR": + switch(operation.toLowerCase()) + { + case "importkey": + case "exportkey": + case "generatekey": + result = { + algorithm: { + name: "AES-CTR", + length: 256 + }, + usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] + }; + break; + case "decrypt": + case "encrypt": + result = { + algorithm: { + name: "AES-CTR", + counter: new Uint8Array(16), + length: 10 + }, + usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] + }; + break; + default: + return { + algorithm: { + name: "AES-CTR" + }, + usages: [] + }; + } + break; + case "AES-CBC": + switch(operation.toLowerCase()) + { + case "importkey": + case "exportkey": + case "generatekey": + result = { + algorithm: { + name: "AES-CBC", + length: 256 + }, + usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] + }; + break; + case "decrypt": + case "encrypt": + result = { + algorithm: { + name: "AES-CBC", + iv: in_window.org.pkijs.getRandomValues(new Uint8Array(16)) // For "decrypt" the value should be replaced with value got on "encrypt" step + }, + usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] + }; + break; + default: + return { + algorithm: { + name: "AES-CBC" + }, + usages: [] + }; + } + break; + case "AES-GCM": + switch(operation.toLowerCase()) + { + case "importkey": + case "exportkey": + case "generatekey": + result = { + algorithm: { + name: "AES-GCM", + length: 256 + }, + usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] + }; + break; + case "decrypt": + case "encrypt": + result = { + algorithm: { + name: "AES-GCM", + iv: in_window.org.pkijs.getRandomValues(new Uint8Array(16)) // For "decrypt" the value should be replaced with value got on "encrypt" step + }, + usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] + }; + break; + default: + return { + algorithm: { + name: "AES-GCM" + }, + usages: [] + }; + } + break; + case "AES-KW": + switch(operation.toLowerCase()) + { + case "importkey": + case "exportkey": + case "generatekey": + case "wrapkey": + case "unwrapkey": + result = { + algorithm: { + name: "AES-KW", + length: 256 + }, + usages: ["wrapKey", "unwrapKey"] + }; + break; + default: + return { + algorithm: { + name: "AES-KW" + }, + usages: [] + }; + } + break; + case "HMAC": + switch(operation.toLowerCase()) + { + case "sign": + case "verify": + result = { + algorithm: { + name: "HMAC" + }, + usages: ["sign", "verify"] + }; + break; + case "importkey": + case "exportkey": + case "generatekey": + result = { + algorithm: { + name: "HMAC", + length: 32, + hash: { + name: "SHA-256" + } + }, + usages: ["sign", "verify"] + }; + break; + default: + return { + algorithm: { + name: "HMAC" + }, + usages: [] + }; + } + break; + case "HKDF": + switch(operation.toLowerCase()) + { + case "derivekey": + result = { + algorithm: { + name: "HKDF", + hash: "SHA-256", + salt: new Uint8Array(), + info: new Uint8Array() + }, + usages: ["encrypt", "decrypt"] + }; + break; + default: + return { + algorithm: { + name: "HKDF" + }, + usages: [] + }; + } + break; + case "PBKDF2": + switch(operation.toLowerCase()) + { + case "derivekey": + result = { + algorithm: { + name: "PBKDF2", + hash: { name: "SHA-256" }, + salt: new Uint8Array(), + iterations: 1000 + }, + usages: ["encrypt", "decrypt"] + }; + break; + default: + return { + algorithm: { + name: "PBKDF2" + }, + usages: [] + }; + } + break; + default: + } + + return result; + }; + //************************************************************************************** + in_window.org.pkijs.getOIDByAlgorithm = + function(algorithm) + { + /// <summary>Get OID for each specific WebCrypto algorithm</summary> + /// <param name="algorithm" type="Object">WebCrypto algorithm</param> + + var result = ""; + + switch(algorithm.name.toUpperCase()) + { + case "RSASSA-PKCS1-V1_5": + switch(algorithm.hash.name.toUpperCase()) + { + case "SHA-1": + result = "1.2.840.113549.1.1.5"; + break; + case "SHA-256": + result = "1.2.840.113549.1.1.11"; + break; + case "SHA-384": + result = "1.2.840.113549.1.1.12"; + break; + case "SHA-512": + result = "1.2.840.113549.1.1.13"; + break; + default: + } + break; + case "RSA-PSS": + result = "1.2.840.113549.1.1.10"; + break; + case "RSA-OAEP": + result = "1.2.840.113549.1.1.7"; + break; + case "ECDSA": + switch(algorithm.hash.name.toUpperCase()) + { + case "SHA-1": + result = "1.2.840.10045.4.1"; + break; + case "SHA-256": + result = "1.2.840.10045.4.3.2"; + break; + case "SHA-384": + result = "1.2.840.10045.4.3.3"; + break; + case "SHA-512": + result = "1.2.840.10045.4.3.4"; + break; + default: + } + break; + case "ECDH": + switch(algorithm.kdf.toUpperCase()) // Non-standard addition - hash algorithm of KDF function + { + case "SHA-1": + result = "1.3.133.16.840.63.0.2"; // dhSinglePass-stdDH-sha1kdf-scheme + break; + case "SHA-256": + result = "1.3.132.1.11.1"; // dhSinglePass-stdDH-sha256kdf-scheme + break; + case "SHA-384": + result = "1.3.132.1.11.2"; // dhSinglePass-stdDH-sha384kdf-scheme + break; + case "SHA-512": + result = "1.3.132.1.11.3"; // dhSinglePass-stdDH-sha512kdf-scheme + break; + default: + } + break; + case "AES-CTR": + break; + case "AES-CBC": + switch(algorithm.length) + { + case 128: + result = "2.16.840.1.101.3.4.1.2"; + break; + case 192: + result = "2.16.840.1.101.3.4.1.22"; + break; + case 256: + result = "2.16.840.1.101.3.4.1.42"; + break; + default: + } + break; + case "AES-CMAC": + break; + case "AES-GCM": + switch(algorithm.length) + { + case 128: + result = "2.16.840.1.101.3.4.1.6"; + break; + case 192: + result = "2.16.840.1.101.3.4.1.26"; + break; + case 256: + result = "2.16.840.1.101.3.4.1.46"; + break; + default: + } + break; + case "AES-CFB": + switch(algorithm.length) + { + case 128: + result = "2.16.840.1.101.3.4.1.4"; + break; + case 192: + result = "2.16.840.1.101.3.4.1.24"; + break; + case 256: + result = "2.16.840.1.101.3.4.1.44"; + break; + default: + } + break; + case "AES-KW": + switch(algorithm.length) + { + case 128: + result = "2.16.840.1.101.3.4.1.5"; + break; + case 192: + result = "2.16.840.1.101.3.4.1.25"; + break; + case 256: + result = "2.16.840.1.101.3.4.1.45"; + break; + default: + } + break; + case "HMAC": + switch(algorithm.hash.name.toUpperCase()) + { + case "SHA-1": + result = "1.2.840.113549.2.7"; + break; + case "SHA-256": + result = "1.2.840.113549.2.9"; + break; + case "SHA-384": + result = "1.2.840.113549.2.10"; + break; + case "SHA-512": + result = "1.2.840.113549.2.11"; + break; + default: + } + break; + case "DH": + result = "1.2.840.113549.1.9.16.3.5"; + break; + case "SHA-1": + result = "1.3.14.3.2.26"; + break; + case "SHA-256": + result = "2.16.840.1.101.3.4.2.1"; + break; + case "SHA-384": + result = "2.16.840.1.101.3.4.2.2"; + break; + case "SHA-512": + result = "2.16.840.1.101.3.4.2.3"; + break; + case "CONCAT": + break; + case "HKDF": + break; + case "PBKDF2": + result = "1.2.840.113549.1.5.12"; + break; + // #region Special case - OIDs for ECC curves + case "P-256": + result = "1.2.840.10045.3.1.7"; + break; + case "P-384": + result = "1.3.132.0.34"; + break; + case "P-521": + result = "1.3.132.0.35"; + break; + // #endregion + + default: + } + + return result; + }; + //************************************************************************************** + in_window.org.pkijs.getAlgorithmByOID = + function(oid) + { + /// <summary>Get WebCrypto algorithm by wel-known OID</summary> + /// <param name="oid" type="String">Wel-known OID to search for</param> + + var result = {}; + + switch(oid) + { + case "1.2.840.113549.1.1.5": + result = { + name: "RSASSA-PKCS1-v1_5", + hash: { + name: "SHA-1" + } + }; + break; + case "1.2.840.113549.1.1.11": + result = { + name: "RSASSA-PKCS1-v1_5", + hash: { + name: "SHA-256" + } + }; + break; + case "1.2.840.113549.1.1.12": + result = { + name: "RSASSA-PKCS1-v1_5", + hash: { + name: "SHA-384" + } + }; + break; + case "1.2.840.113549.1.1.13": + result = { + name: "RSASSA-PKCS1-v1_5", + hash: { + name: "SHA-512" + } + }; + break; + case "1.2.840.113549.1.1.10": + result = { + name: "RSA-PSS" + }; + break; + case "1.2.840.113549.1.1.7": + result = { + name: "RSA-OAEP" + }; + break; + case "1.2.840.10045.4.1": + result = { + name: "ECDSA", + hash: { + name: "SHA-1" + } + }; + break; + case "1.2.840.10045.4.3.2": + result = { + name: "ECDSA", + hash: { + name: "SHA-256" + } + }; + break; + case "1.2.840.10045.4.3.3": + result = { + name: "ECDSA", + hash: { + name: "SHA-384" + } + }; + break; + case "1.2.840.10045.4.3.4": + result = { + name: "ECDSA", + hash: { + name: "SHA-512" + } + }; + break; + case "1.3.133.16.840.63.0.2": + result = { + name: "ECDH", + kdf: "SHA-1" + }; + break; + case "1.3.132.1.11.1": + result = { + name: "ECDH", + kdf: "SHA-256" + }; + break; + case "1.3.132.1.11.2": + result = { + name: "ECDH", + kdf: "SHA-384" + }; + break; + case "1.3.132.1.11.3": + result = { + name: "ECDH", + kdf: "SHA-512" + }; + break; + case "2.16.840.1.101.3.4.1.2": + result = { + name: "AES-CBC", + length: 128 + }; + break; + case "2.16.840.1.101.3.4.1.22": + result = { + name: "AES-CBC", + length: 192 + }; + break; + case "2.16.840.1.101.3.4.1.42": + result = { + name: "AES-CBC", + length: 256 + }; + break; + case "2.16.840.1.101.3.4.1.6": + result = { + name: "AES-GCM", + length: 128 + }; + break; + case "2.16.840.1.101.3.4.1.26": + result = { + name: "AES-GCM", + length: 192 + }; + break; + case "2.16.840.1.101.3.4.1.46": + result = { + name: "AES-GCM", + length: 256 + }; + break; + case "2.16.840.1.101.3.4.1.4": + result = { + name: "AES-CFB", + length: 128 + }; + break; + case "2.16.840.1.101.3.4.1.24": + result = { + name: "AES-CFB", + length: 192 + }; + break; + case "2.16.840.1.101.3.4.1.44": + result = { + name: "AES-CFB", + length: 256 + }; + break; + case "2.16.840.1.101.3.4.1.5": + result = { + name: "AES-KW", + length: 128 + }; + break; + case "2.16.840.1.101.3.4.1.25": + result = { + name: "AES-KW", + length: 192 + }; + break; + case "2.16.840.1.101.3.4.1.45": + result = { + name: "AES-KW", + length: 256 + }; + break; + case "1.2.840.113549.2.7": + result = { + name: "HMAC", + hash: { + name: "SHA-1" + } + }; + break; + case "1.2.840.113549.2.9": + result = { + name: "HMAC", + hash: { + name: "SHA-256" + } + }; + break; + case "1.2.840.113549.2.10": + result = { + name: "HMAC", + hash: { + name: "SHA-384" + } + }; + break; + case "1.2.840.113549.2.11": + result = { + name: "HMAC", + hash: { + name: "SHA-512" + } + }; + break; + case "1.2.840.113549.1.9.16.3.5": + result = { + name: "DH" + }; + break; + case "1.3.14.3.2.26": + result = { + name: "SHA-1" + }; + break; + case "2.16.840.1.101.3.4.2.1": + result = { + name: "SHA-256" + }; + break; + case "2.16.840.1.101.3.4.2.2": + result = { + name: "SHA-384" + }; + break; + case "2.16.840.1.101.3.4.2.3": + result = { + name: "SHA-512" + }; + break; + case "1.2.840.113549.1.5.12": + result = { + name: "PBKDF2" + }; + break; + // #region Special case - OIDs for ECC curves + case "1.2.840.10045.3.1.7": + result = { + name: "P-256" + }; + break; + case "1.3.132.0.34": + result = { + name: "P-384" + }; + break; + case "1.3.132.0.35": + result = { + name: "P-521" + }; + break; + // #endregion + + default: + } + + return result; + }; + //************************************************************************************** + in_window.org.pkijs.getHashAlgorithm = + function(signatureAlgorithm) + { + /// <summary>Getting hash algorithm by signature algorithm</summary> + /// <param name="signatureAlgorithm" type="in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER">Signature algorithm</param> + + var result = ""; + + switch(signatureAlgorithm.algorithm_id) + { + case "1.2.840.10045.4.1": // ecdsa-with-SHA1 + case "1.2.840.113549.1.1.5": + result = "SHA-1"; + break; + case "1.2.840.10045.4.3.2": // ecdsa-with-SHA256 + case "1.2.840.113549.1.1.11": + result = "SHA-256"; + break; + case "1.2.840.10045.4.3.3": // ecdsa-with-SHA384 + case "1.2.840.113549.1.1.12": + result = "SHA-384"; + break; + case "1.2.840.10045.4.3.4": // ecdsa-with-SHA512 + case "1.2.840.113549.1.1.13": + result = "SHA-512"; + break; + case "1.2.840.113549.1.1.10": // RSA-PSS + { + var params; + + try + { + params = new in_window.org.pkijs.simpl.x509.RSASSA_PSS_params({ schema: signatureAlgorithm.algorithm_params }); + if("hashAlgorithm" in params) + { + var algorithm = in_window.org.pkijs.getAlgorithmByOID(params.hashAlgorithm.algorithm_id); + if(("name" in algorithm) === false) + return ""; + + result = algorithm.name; + } + else + result = "SHA-1"; + } + catch(ex) + { + } + } + break; + default: + } + + return result; + }; + //************************************************************************************** + in_window.org.pkijs.createCMSECDSASignature = + function(signatureBuffer) + { + /// <summary>Create CMS ECDSA signature from WebCrypto ECDSA signature</summary> + /// <param name="signatureBuffer" type="ArrayBuffer">WebCrypto result of "sign" function</param> + + // #region Initial check for correct length + if((signatureBuffer.byteLength % 2) != 0) + return new ArrayBuffer(0); + // #endregion + + // #region Initial variables + var i = 0; + var length = signatureBuffer.byteLength / 2; // There are two equal parts inside incoming ArrayBuffer + + var signatureView = new Uint8Array(signatureBuffer); + + var r_buffer = new ArrayBuffer(length); + var r_view = new Uint8Array(r_buffer); + var r_corrected_buffer; + var r_corrected_view; + + var s_buffer = new ArrayBuffer(length); + var s_view = new Uint8Array(s_buffer); + var s_corrected_buffer; + var s_corrected_view; + // #endregion + + // #region Get "r" part of ECDSA signature + for(; i < length; i++) + r_view[i] = signatureView[i]; + + if(r_view[0] & 0x80) + { + r_corrected_buffer = new ArrayBuffer(length + 1); + r_corrected_view = new Uint8Array(r_corrected_buffer); + + r_corrected_view[0] = 0x00; + + for(var j = 0; j < length; j++) + r_corrected_view[j + 1] = r_view[j]; + } + else + { + r_corrected_buffer = r_buffer; + r_corrected_view = r_view; + } + // #endregion + + // #region Get "s" part of ECDSA signature + for(; i < signatureBuffer.byteLength; i++) + s_view[i - length] = signatureView[i]; + + + if(s_view[0] & 0x80) + { + s_corrected_buffer = new ArrayBuffer(length + 1); + s_corrected_view = new Uint8Array(s_corrected_buffer); + + s_corrected_view[0] = 0x00; + + for(var j = 0; j < length; j++) + s_corrected_view[j + 1] = s_view[j]; + } + else + { + s_corrected_buffer = s_buffer; + s_corrected_view = s_view; + } + // #endregion + + // #region Create ASN.1 structure of CMS ECDSA signature + var r_integer = new in_window.org.pkijs.asn1.INTEGER(); + r_integer.value_block.is_hex_only = true; + r_integer.value_block.value_hex = in_window.org.pkijs.copyBuffer(r_corrected_buffer); + + var s_integer = new in_window.org.pkijs.asn1.INTEGER(); + s_integer.value_block.is_hex_only = true; + s_integer.value_block.value_hex = in_window.org.pkijs.copyBuffer(s_corrected_buffer); + + var asn1 = new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + r_integer, + s_integer + ] + }); + // #endregion + + return asn1.toBER(false); + }; + //************************************************************************************** + in_window.org.pkijs.createECDSASignatureFromCMS = + function(cmsSignature) + { + /// <summary>Create a single ArrayBuffer from CMS ECDSA signature</summary> + /// <param name="cmsSignature" type="in_window.org.pkijs.asn1.SEQUENCE">ASN.1 SEQUENCE contains CMS ECDSA signature</param> + + // #region Initial variables + var length = 0; + + var r_start = 0; + var s_start = 0; + + var r_length = cmsSignature.value_block.value[0].value_block.value_hex.byteLength; + var s_length = cmsSignature.value_block.value[1].value_block.value_hex.byteLength; + // #endregion + + // #region Get length of final "ArrayBuffer" + var r_view = new Uint8Array(cmsSignature.value_block.value[0].value_block.value_hex); + if((r_view[0] === 0x00) && (r_view[1] & 0x80)) + { + length = r_length - 1; + r_start = 1; + } + else + length = r_length; + + var s_view = new Uint8Array(cmsSignature.value_block.value[1].value_block.value_hex); + if((s_view[0] === 0x00) && (s_view[1] & 0x80)) + { + length += s_length - 1; + s_start = 1; + } + else + length += s_length; + // #endregion + + // #region Copy values from CMS ECDSA signature + var result = new ArrayBuffer(length); + var result_view = new Uint8Array(result); + + for(var i = r_start; i < r_length; i++) + result_view[i - r_start] = r_view[i]; + + for(var i = s_start; i < s_length; i++) + result_view[i - s_start + r_length - r_start] = s_view[i]; + // #endregion + + return result; + }; + //************************************************************************************** + in_window.org.pkijs.getEncryptionAlgorithm = + function(algorithm) + { + /// <summary>Get encryption algorithm OID by WebCrypto algorithm's object</summary> + /// <param name="algorithm" type="WebCryptoAlgorithm">WebCrypto algorithm object</param> + + var result = ""; + + switch(algorithm.name.toUpperCase()) + { + case "AES-CBC": + switch(algorithm.length) + { + case 128: + result = "2.16.840.1.101.3.4.1.2"; + break; + case 192: + result = "2.16.840.1.101.3.4.1.22"; + break; + case 256: + result = "2.16.840.1.101.3.4.1.42"; + break; + default: + } + break; + case "AES-GCM": + switch(algorithm.length) + { + case 128: + result = "2.16.840.1.101.3.4.1.6"; + break; + case 192: + result = "2.16.840.1.101.3.4.1.26"; + break; + case 256: + result = "2.16.840.1.101.3.4.1.46"; + break; + default: + } + break; + default: + } + + return result; + }; + //************************************************************************************** + in_window.org.pkijs.getAlgorithmByEncryptionOID = + function(oid) + { + /// <summary>Get encryption algorithm name by OID</summary> + /// <param name="oid" type="String">OID of encryption algorithm</param> + + var result = ""; + + switch(oid) + { + case "2.16.840.1.101.3.4.1.2": + case "2.16.840.1.101.3.4.1.22": + case "2.16.840.1.101.3.4.1.42": + result = "AES-CBC"; + break; + case "2.16.840.1.101.3.4.1.6": + case "2.16.840.1.101.3.4.1.26": + case "2.16.840.1.101.3.4.1.46": + result = "AES-GCM"; + break; + default: + } + + return result; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** +} +)(typeof exports !== "undefined" ? exports : window);
\ No newline at end of file diff --git a/dom/webauthn/tests/pkijs/x509_schema.js b/dom/webauthn/tests/pkijs/x509_schema.js new file mode 100644 index 0000000000..15b0929bb5 --- /dev/null +++ b/dom/webauthn/tests/pkijs/x509_schema.js @@ -0,0 +1,1889 @@ +/* + * Copyright (c) 2014, GMO GlobalSign + * Copyright (c) 2015, Peculiar Ventures + * All rights reserved. + * + * Author 2014-2015, Yury Strozhevsky <www.strozhevsky.com>. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + */ +( +function(in_window) +{ + //************************************************************************************** + // #region Declaration of global variables + //************************************************************************************** + // #region "org" namespace + if(typeof in_window.org === "undefined") + in_window.org = {}; + else + { + if(typeof in_window.org !== "object") + throw new Error("Name org already exists and it's not an object"); + } + // #endregion + + // #region "org.pkijs" namespace + if(typeof in_window.org.pkijs === "undefined") + in_window.org.pkijs = {}; + else + { + if(typeof in_window.org.pkijs !== "object") + throw new Error("Name org.pkijs already exists and it's not an object" + " but " + (typeof in_window.org.pkijs)); + } + // #endregion + + // #region "org.pkijs.schema" namespace + if(typeof in_window.org.pkijs.schema === "undefined") + in_window.org.pkijs.schema = {}; + else + { + if(typeof in_window.org.pkijs.schema !== "object") + throw new Error("Name org.pkijs.schema already exists and it's not an object" + " but " + (typeof in_window.org.pkijs.schema)); + } + // #endregion + + // #region "org.pkijs.schema.x509" namespace + if(typeof in_window.org.pkijs.schema.x509 === "undefined") + in_window.org.pkijs.schema.x509 = {}; + else + { + if(typeof in_window.org.pkijs.schema.x509 !== "object") + throw new Error("Name org.pkijs.schema.x509 already exists and it's not an object" + " but " + (typeof in_window.org.pkijs.schema.x509)); + } + // #endregion + + // #region "local" namespace + var local = {}; + // #endregion + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "Time" type + //************************************************************************************** + in_window.org.pkijs.schema.TIME = + function(input_names, input_optional) + { + var names = in_window.org.pkijs.getNames(arguments[0]); + var optional = (input_optional || false); + + return (new in_window.org.pkijs.asn1.CHOICE({ + optional: optional, + value: [ + new in_window.org.pkijs.asn1.UTCTIME({ name: (names.utcTimeName || "") }), + new in_window.org.pkijs.asn1.GENERALIZEDTIME({ name: (names.generalTimeName || "") }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for X.509 v3 certificate (RFC5280) + //************************************************************************************** + local.tbsCertificate = + function() + { + //TBSCertificate ::= SEQUENCE { + // version [0] EXPLICIT Version DEFAULT v1, + // serialNumber CertificateSerialNumber, + // signature AlgorithmIdentifier, + // issuer Name, + // validity Validity, + // subject Name, + // subjectPublicKeyInfo SubjectPublicKeyInfo, + // issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, + // -- If present, version MUST be v2 or v3 + // subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, + // -- If present, version MUST be v2 or v3 + // extensions [3] EXPLICIT Extensions OPTIONAL + // -- If present, version MUST be v3 + //} + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || "tbsCertificate"), + value: [ + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [ + new in_window.org.pkijs.asn1.INTEGER({ name: (names.tbsCertificate_version || "tbsCertificate.version") }) // EXPLICIT integer value + ] + }), + new in_window.org.pkijs.asn1.INTEGER({ name: (names.tbsCertificate_serialNumber || "tbsCertificate.serialNumber") }), + in_window.org.pkijs.schema.ALGORITHM_IDENTIFIER(names.signature || { + names: { + block_name: "tbsCertificate.signature" + } + }), + in_window.org.pkijs.schema.RDN(names.issuer || { + names: { + block_name: "tbsCertificate.issuer" + } + }), + new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.tbsCertificate_validity || "tbsCertificate.validity"), + value: [ + in_window.org.pkijs.schema.TIME(names.not_before || { + names: { + utcTimeName: "tbsCertificate.notBefore", + generalTimeName: "tbsCertificate.notBefore" + } + }), + in_window.org.pkijs.schema.TIME(names.not_after || { + names: { + utcTimeName: "tbsCertificate.notAfter", + generalTimeName: "tbsCertificate.notAfter" + } + }) + ] + }), + in_window.org.pkijs.schema.RDN(names.subject || { + names: { + block_name: "tbsCertificate.subject" + } + }), + in_window.org.pkijs.schema.PUBLIC_KEY_INFO(names.subjectPublicKeyInfo || { + names: { + block_name: "tbsCertificate.subjectPublicKeyInfo" + } + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.tbsCertificate_issuerUniqueID ||"tbsCertificate.issuerUniqueID"), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + } + }), // IMPLICIT bistring value + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.tbsCertificate_subjectUniqueID ||"tbsCertificate.subjectUniqueID"), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 2 // [2] + } + }), // IMPLICIT bistring value + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 3 // [3] + }, + value: [in_window.org.pkijs.schema.EXTENSIONS(names.extensions || { + names: { + block_name: "tbsCertificate.extensions" + } + })] + }) // EXPLICIT SEQUENCE value + ] + })); + }; + //************************************************************************************** + in_window.org.pkijs.schema.CERT = + function() + { + //Certificate ::= SEQUENCE { + // tbsCertificate TBSCertificate, + // signatureAlgorithm AlgorithmIdentifier, + // signatureValue BIT STRING } + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + local.tbsCertificate(names.tbsCertificate), + in_window.org.pkijs.schema.ALGORITHM_IDENTIFIER(names.signatureAlgorithm || { + names: { + block_name: "signatureAlgorithm" + } + }), + new in_window.org.pkijs.asn1.BITSTRING({ name: (names.signatureValue || "signatureValue") }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for X.509 CRL (Certificate Revocation List)(RFC5280) + //************************************************************************************** + local.tbsCertList = + function() + { + //TBSCertList ::= SEQUENCE { + // version Version OPTIONAL, + // -- if present, MUST be v2 + // signature AlgorithmIdentifier, + // issuer Name, + // thisUpdate Time, + // nextUpdate Time OPTIONAL, + // revokedCertificates SEQUENCE OF SEQUENCE { + // userCertificate CertificateSerialNumber, + // revocationDate Time, + // crlEntryExtensions Extensions OPTIONAL + // -- if present, version MUST be v2 + // } OPTIONAL, + // crlExtensions [0] EXPLICIT Extensions OPTIONAL + // -- if present, version MUST be v2 + //} + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || "tbsCertList"), + value: [ + new in_window.org.pkijs.asn1.INTEGER({ + optional: true, + name: (names.tbsCertList_version || "tbsCertList.version"), + value: 2 + }), // EXPLICIT integer value (v2) + in_window.org.pkijs.schema.ALGORITHM_IDENTIFIER(names.signature || { + names: { + block_name: "tbsCertList.signature" + } + }), + in_window.org.pkijs.schema.RDN(names.issuer || { + names: { + block_name: "tbsCertList.issuer" + } + }), + in_window.org.pkijs.schema.TIME(names.tbsCertList_thisUpdate || { + names: { + utcTimeName: "tbsCertList.thisUpdate", + generalTimeName: "tbsCertList.thisUpdate" + } + }), + in_window.org.pkijs.schema.TIME(names.tbsCertList_thisUpdate || { + names: { + utcTimeName: "tbsCertList.nextUpdate", + generalTimeName: "tbsCertList.nextUpdate" + } + }, true), + new in_window.org.pkijs.asn1.SEQUENCE({ + optional: true, + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.tbsCertList_revokedCertificates || "tbsCertList.revokedCertificates"), + value: new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + new in_window.org.pkijs.asn1.INTEGER(), + in_window.org.pkijs.schema.TIME(), + in_window.org.pkijs.schema.EXTENSIONS({}, true) + ] + }) + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [in_window.org.pkijs.schema.EXTENSIONS(names.crlExtensions || { + names: { + block_name: "tbsCertList.extensions" + } + })] + }) // EXPLICIT SEQUENCE value + ] + })); + }; + //************************************************************************************** + in_window.org.pkijs.schema.CRL = + function() + { + //CertificateList ::= SEQUENCE { + // tbsCertList TBSCertList, + // signatureAlgorithm AlgorithmIdentifier, + // signatureValue BIT STRING } + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || "CertificateList"), + value: [ + local.tbsCertList(arguments[0]), + in_window.org.pkijs.schema.ALGORITHM_IDENTIFIER(names.signatureAlgorithm || { + names: { + block_name: "signatureAlgorithm" + } + }), + new in_window.org.pkijs.asn1.BITSTRING({ name: (names.signatureValue || "signatureValue") }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for PKCS#10 certificate request + //************************************************************************************** + local.CertificationRequestInfo = + function() + { + //CertificationRequestInfo ::= SEQUENCE { + // version INTEGER { v1(0) } (v1,...), + // subject Name, + // subjectPKInfo SubjectPublicKeyInfo{{ PKInfoAlgorithms }}, + // attributes [0] Attributes{{ CRIAttributes }} + //} + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.CertificationRequestInfo || "CertificationRequestInfo"), + value: [ + new in_window.org.pkijs.asn1.INTEGER({ name: (names.CertificationRequestInfo_version || "CertificationRequestInfo.version") }), + new in_window.org.pkijs.schema.RDN(names.subject || { + names: { + block_name: "CertificationRequestInfo.subject" + } + }), + new in_window.org.pkijs.schema.PUBLIC_KEY_INFO({ + names: { + block_name: "CertificationRequestInfo.subjectPublicKeyInfo" + } + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + optional: true, // Because OpenSSL makes wrong "attributes" field + name: (names.CertificationRequestInfo_attributes || "CertificationRequestInfo.attributes"), + value: in_window.org.pkijs.schema.ATTRIBUTE(names.attributes || {}) + }) + ] + }) + ] + })); + }; + //************************************************************************************** + in_window.org.pkijs.schema.PKCS10 = + function() + { + //CertificationRequest ::= SEQUENCE { + // certificationRequestInfo CertificationRequestInfo, + // signatureAlgorithm AlgorithmIdentifier{{ SignatureAlgorithms }}, + // signature BIT STRING + //} + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + local.CertificationRequestInfo(names.certificationRequestInfo || {}), + new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.signatureAlgorithm || "signatureAlgorithm"), + value: [ + new in_window.org.pkijs.asn1.OID(), + new in_window.org.pkijs.asn1.ANY({ optional: true }) + ] + }), + new in_window.org.pkijs.asn1.BITSTRING({ name: (names.signatureValue || "signatureValue") }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for PKCS#8 private key bag + //************************************************************************************** + in_window.org.pkijs.schema.PKCS8 = + function() + { + //PrivateKeyInfo ::= SEQUENCE { + // version Version, + // privateKeyAlgorithm AlgorithmIdentifier {{PrivateKeyAlgorithms}}, + // privateKey PrivateKey, + // attributes [0] Attributes OPTIONAL } + // + //Version ::= INTEGER {v1(0)} (v1,...) + // + //PrivateKey ::= OCTET STRING + // + //Attributes ::= SET OF Attribute + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + new in_window.org.pkijs.asn1.INTEGER({ name: (names.version || "") }), + in_window.org.pkijs.schema.ALGORITHM_IDENTIFIER(names.privateKeyAlgorithm || ""), + new in_window.org.pkijs.asn1.OCTETSTRING({ name: (names.privateKey || "") }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.attributes || ""), + value: in_window.org.pkijs.schema.ATTRIBUTE() + }) + ] + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "GeneralName" type + //************************************************************************************** + local.BuiltInStandardAttributes = + function(optional_flag) + { + //BuiltInStandardAttributes ::= SEQUENCE { + // country-name CountryName OPTIONAL, + // administration-domain-name AdministrationDomainName OPTIONAL, + // network-address [0] IMPLICIT NetworkAddress OPTIONAL, + // terminal-identifier [1] IMPLICIT TerminalIdentifier OPTIONAL, + // private-domain-name [2] PrivateDomainName OPTIONAL, + // organization-name [3] IMPLICIT OrganizationName OPTIONAL, + // numeric-user-identifier [4] IMPLICIT NumericUserIdentifier OPTIONAL, + // personal-name [5] IMPLICIT PersonalName OPTIONAL, + // organizational-unit-names [6] IMPLICIT OrganizationalUnitNames OPTIONAL } + + if(typeof optional_flag === "undefined") + optional_flag = false; + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + optional: optional_flag, + value: [ + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 2, // APPLICATION-SPECIFIC + tag_number: 1 // [1] + }, + name: (names.country_name || ""), + value: [ + new in_window.org.pkijs.asn1.CHOICE({ + value: [ + new in_window.org.pkijs.asn1.NUMERICSTRING(), + new in_window.org.pkijs.asn1.PRINTABLESTRING() + ] + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 2, // APPLICATION-SPECIFIC + tag_number: 2 // [2] + }, + name: (names.administration_domain_name || ""), + value: [ + new in_window.org.pkijs.asn1.CHOICE({ + value: [ + new in_window.org.pkijs.asn1.NUMERICSTRING(), + new in_window.org.pkijs.asn1.PRINTABLESTRING() + ] + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + name: (names.network_address || ""), + is_hex_only: true + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + name: (names.terminal_identifier || ""), + is_hex_only: true + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 2 // [2] + }, + name: (names.private_domain_name || ""), + value: [ + new in_window.org.pkijs.asn1.CHOICE({ + value: [ + new in_window.org.pkijs.asn1.NUMERICSTRING(), + new in_window.org.pkijs.asn1.PRINTABLESTRING() + ] + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 3 // [3] + }, + name: (names.organization_name || ""), + is_hex_only: true + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + optional: true, + name: (names.numeric_user_identifier || ""), + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 4 // [4] + }, + is_hex_only: true + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + name: (names.personal_name || ""), + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 5 // [5] + }, + value: [ + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + is_hex_only: true + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + is_hex_only: true + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 2 // [2] + }, + is_hex_only: true + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 3 // [3] + }, + is_hex_only: true + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + name: (names.organizational_unit_names || ""), + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 6 // [6] + }, + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + value: new in_window.org.pkijs.asn1.PRINTABLESTRING() + }) + ] + }) + ] + })); + }; + //************************************************************************************** + local.BuiltInDomainDefinedAttributes = + function(optional_flag) + { + if(typeof optional_flag === "undefined") + optional_flag = false; + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + optional: optional_flag, + value: [ + new in_window.org.pkijs.asn1.PRINTABLESTRING(), + new in_window.org.pkijs.asn1.PRINTABLESTRING() + ] + })); + }; + //************************************************************************************** + local.ExtensionAttributes = + function(optional_flag) + { + if(typeof optional_flag === "undefined") + optional_flag = false; + + return (new in_window.org.pkijs.asn1.SET({ + optional: optional_flag, + value: [ + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + is_hex_only: true + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value: [new in_window.org.pkijs.asn1.ANY()] + }) + ] + })); + }; + //************************************************************************************** + in_window.org.pkijs.schema.GENERAL_NAME = + function() + { + /// <remarks>By passing "names" array as an argument you can name each element of "GENERAL NAME" choice</remarks> + + //GeneralName ::= CHOICE { + // otherName [0] OtherName, + // rfc822Name [1] IA5String, + // dNSName [2] IA5String, + // x400Address [3] ORAddress, + // directoryName [4] Name, + // ediPartyName [5] EDIPartyName, + // uniformResourceIdentifier [6] IA5String, + // iPAddress [7] OCTET STRING, + // registeredID [8] OBJECT IDENTIFIER } + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.CHOICE({ + value: [ + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.OID(), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [new in_window.org.pkijs.asn1.ANY()] + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.block_name || ""), + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + } + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.block_name || ""), + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 2 // [2] + } + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 3 // [3] + }, + name: (names.block_name || ""), + value: [ + local.BuiltInStandardAttributes(false), + local.BuiltInDomainDefinedAttributes(true), + local.ExtensionAttributes(true) + ] + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 4 // [4] + }, + name: (names.block_name || ""), + value: [in_window.org.pkijs.schema.RDN(names.directoryName || {})] + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 5 // [5] + }, + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [ + new in_window.org.pkijs.asn1.CHOICE({ + value: [ + new in_window.org.pkijs.asn1.TELETEXSTRING(), + new in_window.org.pkijs.asn1.PRINTABLESTRING(), + new in_window.org.pkijs.asn1.UNIVERSALSTRING(), + new in_window.org.pkijs.asn1.UTF8STRING(), + new in_window.org.pkijs.asn1.BMPSTRING() + ] + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value: [ + new in_window.org.pkijs.asn1.CHOICE({ + value: [ + new in_window.org.pkijs.asn1.TELETEXSTRING(), + new in_window.org.pkijs.asn1.PRINTABLESTRING(), + new in_window.org.pkijs.asn1.UNIVERSALSTRING(), + new in_window.org.pkijs.asn1.UTF8STRING(), + new in_window.org.pkijs.asn1.BMPSTRING() + ] + }) + ] + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.block_name || ""), + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 6 // [6] + } + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.block_name || ""), + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 7 // [7] + } + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.block_name || ""), + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 8 // [8] + } + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "AlgorithmIdentifier" type + //************************************************************************************** + in_window.org.pkijs.schema.ALGORITHM_IDENTIFIER = + function() + { + //AlgorithmIdentifier ::= SEQUENCE { + // algorithm OBJECT IDENTIFIER, + // parameters ANY DEFINED BY algorithm OPTIONAL } + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + optional: (names.optional || false), + value: [ + new in_window.org.pkijs.asn1.OID({ name: (names.algorithmIdentifier || "") }), + new in_window.org.pkijs.asn1.ANY({ name: (names.algorithmParams || ""), optional: true }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "RSAPublicKey" type (RFC3447) + //************************************************************************************** + in_window.org.pkijs.schema.x509.RSAPublicKey = + function() + { + //RSAPublicKey ::= SEQUENCE { + // modulus INTEGER, -- n + // publicExponent INTEGER -- e + //} + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.INTEGER({ name: (names.modulus || "") }), + new in_window.org.pkijs.asn1.INTEGER({ name: (names.publicExponent || "") }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "OtherPrimeInfo" type (RFC3447) + //************************************************************************************** + in_window.org.pkijs.schema.x509.OtherPrimeInfo = + function() + { + //OtherPrimeInfo ::= SEQUENCE { + // prime INTEGER, -- ri + // exponent INTEGER, -- di + // coefficient INTEGER -- ti + //} + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.INTEGER({ name: (names.prime || "") }), + new in_window.org.pkijs.asn1.INTEGER({ name: (names.exponent || "") }), + new in_window.org.pkijs.asn1.INTEGER({ name: (names.coefficient || "") }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "RSAPrivateKey" type (RFC3447) + //************************************************************************************** + in_window.org.pkijs.schema.x509.RSAPrivateKey = + function() + { + //RSAPrivateKey ::= SEQUENCE { + // version Version, + // modulus INTEGER, -- n + // publicExponent INTEGER, -- e + // privateExponent INTEGER, -- d + // prime1 INTEGER, -- p + // prime2 INTEGER, -- q + // exponent1 INTEGER, -- d mod (p-1) + // exponent2 INTEGER, -- d mod (q-1) + // coefficient INTEGER, -- (inverse of q) mod p + // otherPrimeInfos OtherPrimeInfos OPTIONAL + //} + // + //OtherPrimeInfos ::= SEQUENCE SIZE(1..MAX) OF OtherPrimeInfo + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.INTEGER({ name: (names.version || "") }), + new in_window.org.pkijs.asn1.INTEGER({ name: (names.modulus || "") }), + new in_window.org.pkijs.asn1.INTEGER({ name: (names.publicExponent || "") }), + new in_window.org.pkijs.asn1.INTEGER({ name: (names.privateExponent || "") }), + new in_window.org.pkijs.asn1.INTEGER({ name: (names.prime1 || "") }), + new in_window.org.pkijs.asn1.INTEGER({ name: (names.prime2 || "") }), + new in_window.org.pkijs.asn1.INTEGER({ name: (names.exponent1 || "") }), + new in_window.org.pkijs.asn1.INTEGER({ name: (names.exponent2 || "") }), + new in_window.org.pkijs.asn1.INTEGER({ name: (names.coefficient || "") }), + new in_window.org.pkijs.asn1.SEQUENCE({ + optional: true, + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.otherPrimeInfos || ""), + value: in_window.org.pkijs.schema.x509.OtherPrimeInfo(names.otherPrimeInfo || {}) + }) + ] + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "RSASSA-PSS-params" type (RFC3447) + //************************************************************************************** + in_window.org.pkijs.schema.x509.RSASSA_PSS_params = + function() + { + //RSASSA-PSS-params ::= SEQUENCE { + // hashAlgorithm [0] HashAlgorithm DEFAULT sha1Identifier, + // maskGenAlgorithm [1] MaskGenAlgorithm DEFAULT mgf1SHA1Identifier, + // saltLength [2] INTEGER DEFAULT 20, + // trailerField [3] INTEGER DEFAULT 1 } + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + optional: true, + value: [in_window.org.pkijs.schema.ALGORITHM_IDENTIFIER(names.hashAlgorithm || {})] + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + optional: true, + value: [in_window.org.pkijs.schema.ALGORITHM_IDENTIFIER(names.maskGenAlgorithm || {})] + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 2 // [2] + }, + optional: true, + value: [new in_window.org.pkijs.asn1.INTEGER({ name: (names.saltLength || "") })] + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 3 // [3] + }, + optional: true, + value: [new in_window.org.pkijs.asn1.INTEGER({ name: (names.trailerField || "") })] + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "SubjectPublicKeyInfo" type + //************************************************************************************** + in_window.org.pkijs.schema.PUBLIC_KEY_INFO = + function() + { + //SubjectPublicKeyInfo ::= SEQUENCE { + // algorithm AlgorithmIdentifier, + // subjectPublicKey BIT STRING } + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + in_window.org.pkijs.schema.ALGORITHM_IDENTIFIER(names.algorithm || {}), + new in_window.org.pkijs.asn1.BITSTRING({ name: (names.subjectPublicKey || "") }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "Attribute" type + //************************************************************************************** + in_window.org.pkijs.schema.ATTRIBUTE = + function() + { + // Attribute { ATTRIBUTE:IOSet } ::= SEQUENCE { + // type ATTRIBUTE.&id({IOSet}), + // values SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{@type}) + //} + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.OID({ name: (names.type || "") }), + new in_window.org.pkijs.asn1.SET({ + name: (names.set_name || ""), + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.values || ""), + value: new in_window.org.pkijs.asn1.ANY() + }) + ] + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "AttributeTypeAndValue" type + //************************************************************************************** + in_window.org.pkijs.schema.ATTR_TYPE_AND_VALUE = + function() + { + //AttributeTypeAndValue ::= SEQUENCE { + // type AttributeType, + // value AttributeValue } + // + //AttributeType ::= OBJECT IDENTIFIER + // + //AttributeValue ::= ANY -- DEFINED BY AttributeType + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.OID({ name: (names.type || "") }), + new in_window.org.pkijs.asn1.ANY({ name: (names.value || "") }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "RelativeDistinguishedName" type + //************************************************************************************** + in_window.org.pkijs.schema.RDN = + function() + { + //RDNSequence ::= SEQUENCE OF RelativeDistinguishedName + // + //RelativeDistinguishedName ::= + //SET SIZE (1..MAX) OF AttributeTypeAndValue + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.repeated_sequence || ""), + value: new in_window.org.pkijs.asn1.SET({ + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.repeated_set || ""), + value: in_window.org.pkijs.schema.ATTR_TYPE_AND_VALUE(names.attr_type_and_value || {}) + }) + ] + }) + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "Extension" type + //************************************************************************************** + in_window.org.pkijs.schema.EXTENSION = + function() + { + //Extension ::= SEQUENCE { + // extnID OBJECT IDENTIFIER, + // critical BOOLEAN DEFAULT FALSE, + // extnValue OCTET STRING + //} + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.OID({ name: (names.extnID || "") }), + new in_window.org.pkijs.asn1.BOOLEAN({ + name: (names.critical || ""), + optional: true + }), + new in_window.org.pkijs.asn1.OCTETSTRING({ name: (names.extnValue || "") }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "Extensions" type (sequence of many Extension) + //************************************************************************************** + in_window.org.pkijs.schema.EXTENSIONS = + function(input_names, input_optional) + { + //Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension + + var names = in_window.org.pkijs.getNames(arguments[0]); + var optional = input_optional || false; + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + optional: optional, + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.extensions || ""), + value: in_window.org.pkijs.schema.EXTENSION(names.extension || {}) + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "AuthorityKeyIdentifier" type of extension + //************************************************************************************** + in_window.org.pkijs.schema.x509.AuthorityKeyIdentifier = + function() + { + // AuthorityKeyIdentifier OID ::= 2.5.29.35 + // + //AuthorityKeyIdentifier ::= SEQUENCE { + // keyIdentifier [0] KeyIdentifier OPTIONAL, + // authorityCertIssuer [1] GeneralNames OPTIONAL, + // authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL } + // + //KeyIdentifier ::= OCTET STRING + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.keyIdentifier || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + } + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.authorityCertIssuer || ""), + value: in_window.org.pkijs.schema.GENERAL_NAME() + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.authorityCertSerialNumber || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 2 // [2] + } + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "PrivateKeyUsagePeriod" type of extension + //************************************************************************************** + in_window.org.pkijs.schema.x509.PrivateKeyUsagePeriod = + function() + { + // PrivateKeyUsagePeriod OID ::= 2.5.29.16 + // + //PrivateKeyUsagePeriod ::= SEQUENCE { + // notBefore [0] GeneralizedTime OPTIONAL, + // notAfter [1] GeneralizedTime OPTIONAL } + //-- either notBefore or notAfter MUST be present + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.notBefore || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + } + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.notAfter || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + } + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "IssuerAltName" and "SubjectAltName" types of extension + //************************************************************************************** + in_window.org.pkijs.schema.x509.AltName = + function() + { + // SubjectAltName OID ::= 2.5.29.17 + // IssuerAltName OID ::= 2.5.29.18 + // + // AltName ::= GeneralNames + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.altNames || ""), + value: in_window.org.pkijs.schema.GENERAL_NAME() + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "SubjectDirectoryAttributes" type of extension + //************************************************************************************** + in_window.org.pkijs.schema.x509.SubjectDirectoryAttributes = + function() + { + // SubjectDirectoryAttributes OID ::= 2.5.29.9 + // + //SubjectDirectoryAttributes ::= SEQUENCE SIZE (1..MAX) OF Attribute + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.attributes || ""), + value: in_window.org.pkijs.schema.ATTRIBUTE() + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "GeneralSubtree" type + //************************************************************************************** + in_window.org.pkijs.schema.x509.GeneralSubtree = + function() + { + //GeneralSubtree ::= SEQUENCE { + // base GeneralName, + // minimum [0] BaseDistance DEFAULT 0, + // maximum [1] BaseDistance OPTIONAL } + // + //BaseDistance ::= INTEGER (0..MAX) + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + in_window.org.pkijs.schema.GENERAL_NAME(names.base || ""), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [new in_window.org.pkijs.asn1.INTEGER({ name: (names.minimum || "") })] + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value: [new in_window.org.pkijs.asn1.INTEGER({ name: (names.maximum || "") })] + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "NameConstraints" type of extension + //************************************************************************************** + in_window.org.pkijs.schema.x509.NameConstraints = + function() + { + // NameConstraints OID ::= 2.5.29.30 + // + //NameConstraints ::= SEQUENCE { + // permittedSubtrees [0] GeneralSubtrees OPTIONAL, + // excludedSubtrees [1] GeneralSubtrees OPTIONAL } + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.permittedSubtrees || ""), + value: in_window.org.pkijs.schema.x509.GeneralSubtree() + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.excludedSubtrees || ""), + value: in_window.org.pkijs.schema.x509.GeneralSubtree() + }) + ] + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "BasicConstraints" type of extension + //************************************************************************************** + in_window.org.pkijs.schema.x509.BasicConstraints = + function() + { + // BasicConstraints OID ::= 2.5.29.19 + // + //BasicConstraints ::= SEQUENCE { + // cA BOOLEAN DEFAULT FALSE, + // pathLenConstraint INTEGER (0..MAX) OPTIONAL } + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.BOOLEAN({ + optional: true, + name: (names.cA || "") + }), + new in_window.org.pkijs.asn1.INTEGER({ + optional: true, + name: (names.pathLenConstraint || "") + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "PolicyQualifierInfo" type + //************************************************************************************** + in_window.org.pkijs.schema.x509.PolicyQualifierInfo = + function() + { + //PolicyQualifierInfo ::= SEQUENCE { + // policyQualifierId PolicyQualifierId, + // qualifier ANY DEFINED BY policyQualifierId } + // + //id-qt OBJECT IDENTIFIER ::= { id-pkix 2 } + //id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 } + //id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 } + // + //PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice ) + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.OID({ name: (names.policyQualifierId || "") }), + new in_window.org.pkijs.asn1.ANY({ name: (names.qualifier || "") }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "PolicyInformation" type + //************************************************************************************** + in_window.org.pkijs.schema.x509.PolicyInformation = + function() + { + //PolicyInformation ::= SEQUENCE { + // policyIdentifier CertPolicyId, + // policyQualifiers SEQUENCE SIZE (1..MAX) OF + // PolicyQualifierInfo OPTIONAL } + // + //CertPolicyId ::= OBJECT IDENTIFIER + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.OID({ name: (names.policyIdentifier || "") }), + new in_window.org.pkijs.asn1.SEQUENCE({ + optional: true, + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.policyQualifiers || ""), + value: in_window.org.pkijs.schema.x509.PolicyQualifierInfo() + }) + ] + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "CertificatePolicies" type of extension + //************************************************************************************** + in_window.org.pkijs.schema.x509.CertificatePolicies = + function() + { + // CertificatePolicies OID ::= 2.5.29.32 + // + //certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.certificatePolicies || ""), + value: in_window.org.pkijs.schema.x509.PolicyInformation() + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "PolicyMapping" type + //************************************************************************************** + in_window.org.pkijs.schema.x509.PolicyMapping = + function() + { + //PolicyMapping ::= SEQUENCE { + // issuerDomainPolicy CertPolicyId, + // subjectDomainPolicy CertPolicyId } + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.OID({ name: (names.issuerDomainPolicy || "") }), + new in_window.org.pkijs.asn1.OID({ name: (names.subjectDomainPolicy || "") }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "PolicyMappings" type of extension + //************************************************************************************** + in_window.org.pkijs.schema.x509.PolicyMappings = + function() + { + // PolicyMappings OID ::= 2.5.29.33 + // + //PolicyMappings ::= SEQUENCE SIZE (1..MAX) OF PolicyMapping + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.mappings || ""), + value: in_window.org.pkijs.schema.x509.PolicyMapping() + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "PolicyConstraints" type of extension + //************************************************************************************** + in_window.org.pkijs.schema.x509.PolicyConstraints = + function() + { + // PolicyMappings OID ::= 2.5.29.36 + // + //PolicyConstraints ::= SEQUENCE { + // requireExplicitPolicy [0] SkipCerts OPTIONAL, + // inhibitPolicyMapping [1] SkipCerts OPTIONAL } + // + //SkipCerts ::= INTEGER (0..MAX) + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.requireExplicitPolicy || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + } + }), // IMPLICIT integer value + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.inhibitPolicyMapping || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + } + }) // IMPLICIT integer value + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "ExtKeyUsage" type of extension + //************************************************************************************** + in_window.org.pkijs.schema.x509.ExtKeyUsage = + function() + { + // ExtKeyUsage OID ::= 2.5.29.37 + // + // ExtKeyUsage ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId + + // KeyPurposeId ::= OBJECT IDENTIFIER + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.keyPurposes || ""), + value: new in_window.org.pkijs.asn1.OID() + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "DistributionPoint" type + //************************************************************************************** + in_window.org.pkijs.schema.x509.DistributionPoint = + function() + { + //DistributionPoint ::= SEQUENCE { + // distributionPoint [0] DistributionPointName OPTIONAL, + // reasons [1] ReasonFlags OPTIONAL, + // cRLIssuer [2] GeneralNames OPTIONAL } + // + //DistributionPointName ::= CHOICE { + // fullName [0] GeneralNames, + // nameRelativeToCRLIssuer [1] RelativeDistinguishedName } + // + //ReasonFlags ::= BIT STRING { + // unused (0), + // keyCompromise (1), + // cACompromise (2), + // affiliationChanged (3), + // superseded (4), + // cessationOfOperation (5), + // certificateHold (6), + // privilegeWithdrawn (7), + // aACompromise (8) } + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [ + new in_window.org.pkijs.asn1.CHOICE({ + value: [ + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + name: (names.distributionPoint || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.distributionPoint_names || ""), + value: in_window.org.pkijs.schema.GENERAL_NAME() + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + name: (names.distributionPoint || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value: in_window.org.pkijs.schema.RDN().value_block.value + }) + ] + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.reasons || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + } + }), // IMPLICIT bitstring value + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + name: (names.cRLIssuer || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 2 // [2] + }, + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.cRLIssuer_names || ""), + value: in_window.org.pkijs.schema.GENERAL_NAME() + }) + ] + }) // IMPLICIT bitstring value + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "CRLDistributionPoints" type of extension + //************************************************************************************** + in_window.org.pkijs.schema.x509.CRLDistributionPoints = + function() + { + // CRLDistributionPoints OID ::= 2.5.29.31 + // + //CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.distributionPoints || ""), + value: in_window.org.pkijs.schema.x509.DistributionPoint() + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "AccessDescription" type + //************************************************************************************** + in_window.org.pkijs.schema.x509.AccessDescription = + function() + { + //AccessDescription ::= SEQUENCE { + // accessMethod OBJECT IDENTIFIER, + // accessLocation GeneralName } + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.OID({ name: (names.accessMethod || "") }), + in_window.org.pkijs.schema.GENERAL_NAME(names.accessLocation || "") + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "AuthorityInfoAccess" and "SubjectInfoAccess" types of extension + //************************************************************************************** + in_window.org.pkijs.schema.x509.InfoAccess = + function() + { + // AuthorityInfoAccess OID ::= 1.3.6.1.5.5.7.1.1 + // SubjectInfoAccess OID ::= 1.3.6.1.5.5.7.1.11 + // + //AuthorityInfoAccessSyntax ::= + //SEQUENCE SIZE (1..MAX) OF AccessDescription + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.accessDescriptions || ""), + value: in_window.org.pkijs.schema.x509.AccessDescription() + }) + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region ASN.1 schema definition for "IssuingDistributionPoint" type of extension + //************************************************************************************** + in_window.org.pkijs.schema.x509.IssuingDistributionPoint = + function() + { + // IssuingDistributionPoint OID ::= 2.5.29.28 + // + //IssuingDistributionPoint ::= SEQUENCE { + // distributionPoint [0] DistributionPointName OPTIONAL, + // onlyContainsUserCerts [1] BOOLEAN DEFAULT FALSE, + // onlyContainsCACerts [2] BOOLEAN DEFAULT FALSE, + // onlySomeReasons [3] ReasonFlags OPTIONAL, + // indirectCRL [4] BOOLEAN DEFAULT FALSE, + // onlyContainsAttributeCerts [5] BOOLEAN DEFAULT FALSE } + // + //ReasonFlags ::= BIT STRING { + // unused (0), + // keyCompromise (1), + // cACompromise (2), + // affiliationChanged (3), + // superseded (4), + // cessationOfOperation (5), + // certificateHold (6), + // privilegeWithdrawn (7), + // aACompromise (8) } + + var names = in_window.org.pkijs.getNames(arguments[0]); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + name: (names.block_name || ""), + value: [ + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [ + new in_window.org.pkijs.asn1.CHOICE({ + value: [ + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + name: (names.distributionPoint || ""), + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: (names.distributionPoint_names || ""), + value: in_window.org.pkijs.schema.GENERAL_NAME() + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + name: (names.distributionPoint || ""), + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value: in_window.org.pkijs.schema.RDN().value_block.value + }) + ] + }) + ] + }), + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.onlyContainsUserCerts || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + } + }), // IMPLICIT boolean value + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.onlyContainsCACerts || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 2 // [2] + } + }), // IMPLICIT boolean value + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.onlySomeReasons || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 3 // [3] + } + }), // IMPLICIT bitstring value + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.indirectCRL || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 4 // [4] + } + }), // IMPLICIT boolean value + new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + name: (names.onlyContainsAttributeCerts || ""), + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 5 // [5] + } + }) // IMPLICIT boolean value + ] + })); + }; + //************************************************************************************** + // #endregion + //************************************************************************************** +} +)(typeof exports !== "undefined" ? exports : window);
\ No newline at end of file diff --git a/dom/webauthn/tests/pkijs/x509_simpl.js b/dom/webauthn/tests/pkijs/x509_simpl.js new file mode 100644 index 0000000000..c00e79d55b --- /dev/null +++ b/dom/webauthn/tests/pkijs/x509_simpl.js @@ -0,0 +1,7239 @@ +/* + * Copyright (c) 2014, GMO GlobalSign + * Copyright (c) 2015, Peculiar Ventures + * All rights reserved. + * + * Author 2014-2015, Yury Strozhevsky <www.strozhevsky.com>. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + */ +( +function(in_window) +{ + //************************************************************************************** + // #region Declaration of global variables + //************************************************************************************** + // #region "org" namespace + if(typeof in_window.org === "undefined") + in_window.org = {}; + else + { + if(typeof in_window.org !== "object") + throw new Error("Name org already exists and it's not an object"); + } + // #endregion + + // #region "org.pkijs" namespace + if(typeof in_window.org.pkijs === "undefined") + in_window.org.pkijs = {}; + else + { + if(typeof in_window.org.pkijs !== "object") + throw new Error("Name org.pkijs already exists and it's not an object" + " but " + (typeof in_window.org.pkijs)); + } + // #endregion + + // #region "org.pkijs.simpl" namespace + if(typeof in_window.org.pkijs.simpl === "undefined") + in_window.org.pkijs.simpl = {}; + else + { + if(typeof in_window.org.pkijs.simpl !== "object") + throw new Error("Name org.pkijs.simpl already exists and it's not an object" + " but " + (typeof in_window.org.pkijs.simpl)); + } + // #endregion + + // #region "org.pkijs.simpl.x509" namespace + if(typeof in_window.org.pkijs.simpl.x509 === "undefined") + in_window.org.pkijs.simpl.x509 = {}; + else + { + if(typeof in_window.org.pkijs.simpl.x509 !== "object") + throw new Error("Name org.pkijs.simpl.x509 already exists and it's not an object" + " but " + (typeof in_window.org.pkijs.simpl.x509)); + } + // #endregion + + // #region "local" namespace + var local = {}; + // #endregion + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "Time" type + //************************************************************************************** + in_window.org.pkijs.simpl.TIME = + function() + { + // #region Internal properties of the object + this.type = 0; // 0 - UTCTime; 1 - GeneralizedTime; 2 - empty value + this.value = new Date(0, 0, 0); + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.TIME.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.type = (arguments[0].type || 0); + this.value = (arguments[0].value || (new Date(0, 0, 0))); + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.TIME.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.TIME({ + names: { + utcTimeName: "utcTimeName", + generalTimeName: "generalTimeName" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for TIME"); + // #endregion + + // #region Get internal properties from parsed schema + if("utcTimeName" in asn1.result) + { + this.type = 0; + this.value = asn1.result.utcTimeName.toDate(); + } + if("generalTimeName" in asn1.result) + { + this.type = 1; + this.value = asn1.result.generalTimeName.toDate(); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.TIME.prototype.toSchema = + function() + { + // #region Construct and return new ASN.1 schema for this object + var result = {}; + + if(this.type === 0) + result = new in_window.org.pkijs.asn1.UTCTIME({ value_date: this.value }); + if(this.type === 1) + result = new in_window.org.pkijs.asn1.GENERALIZEDTIME({ value_date: this.value }); + + return result; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.TIME.prototype.toJSON = + function() + { + return { + type: this.type, + value: this.value + }; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "GeneralName" type + //************************************************************************************** + in_window.org.pkijs.simpl.GENERAL_NAME = + function() + { + // #region Internal properties of the object + this.NameType = 9; // Name type - from a tagged value (0 for "otherName", 1 for "rfc822Name" etc.) + this.Name = {}; + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.GENERAL_NAME.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.NameType = arguments[0].NameType || 9; + this.Name = arguments[0].Name || {}; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.GENERAL_NAME.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.GENERAL_NAME({ + names: { + block_name: "block_name", + otherName: "otherName", + rfc822Name: "rfc822Name", + dNSName: "dNSName", + x400Address: "x400Address", + directoryName: { + names: { + block_name: "directoryName" + } + }, + ediPartyName: "ediPartyName", + uniformResourceIdentifier: "uniformResourceIdentifier", + iPAddress: "iPAddress", + registeredID: "registeredID" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for GENERAL_NAME"); + // #endregion + + // #region Get internal properties from parsed schema + this.NameType = asn1.result["block_name"].id_block.tag_number; + + switch(this.NameType) + { + case 0: // otherName + this.Name = asn1.result["block_name"]; + break; + case 1: // rfc822Name + dNSName + uniformResourceIdentifier + case 2: + case 6: + { + var value = asn1.result["block_name"]; + + value.id_block.tag_class = 1; // UNIVERSAL + value.id_block.tag_number = 22; // IA5STRING + + var value_ber = value.toBER(false); + + this.Name = in_window.org.pkijs.fromBER(value_ber).result.value_block.value; + } + break; + case 3: // x400Address + this.Name = asn1.result["block_name"]; + break; + case 4: // directoryName + this.Name = new in_window.org.pkijs.simpl.RDN({ schema: asn1.result["directoryName"] }); + break; + case 5: // ediPartyName + this.Name = asn1.result["ediPartyName"]; + break; + case 7: // iPAddress + this.Name = new in_window.org.pkijs.asn1.OCTETSTRING({ value_hex: asn1.result["block_name"].value_block.value_hex }); + break; + case 8: // registeredID + { + var value = asn1.result["block_name"]; + + value.id_block.tag_class = 1; // UNIVERSAL + value.id_block.tag_number = 6; // OID + + var value_ber = value.toBER(false); + + this.Name = in_window.org.pkijs.fromBER(value_ber).result.value_block.toString(); // Getting a string representation of the OID + } + break; + default: + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.GENERAL_NAME.prototype.toSchema = + function(schema) + { + // #region Construct and return new ASN.1 schema for this object + switch(this.NameType) + { + case 0: + case 3: + case 5: + return new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: this.NameType + }, + value: [ + this.Name + ] + }); + + break; + case 1: + case 2: + case 6: + { + var value = new in_window.org.pkijs.asn1.IA5STRING({ value: this.Name }); + + value.id_block.tag_class = 3; + value.id_block.tag_number = this.NameType; + + return value; + } + break; + case 4: + return new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 4 + }, + value: [this.Name.toSchema()] + }); + break; + case 7: + { + var value = this.Name; + + value.id_block.tag_class = 3; + value.id_block.tag_number = this.NameType; + + return value; + } + break; + case 8: + { + var value = new in_window.org.pkijs.asn1.OID({ value: this.Name }); + + value.id_block.tag_class = 3; + value.id_block.tag_number = this.NameType; + + return value; + } + break; + default: + return in_window.org.pkijs.schema.GENERAL_NAME(); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.GENERAL_NAME.prototype.toJSON = + function() + { + var _object = { + NameType: this.NameType + }; + + if((typeof this.Name) === "string") + _object.Name = this.Name; + else + _object.Name = this.Name.toJSON(); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "GeneralNames" type + //************************************************************************************** + in_window.org.pkijs.simpl.GENERAL_NAMES = + function() + { + // #region Internal properties of the object + this.names = new Array(); // Array of "org.pkijs.simpl.GENERAL_NAME" + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.GENERAL_NAMES.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.names = arguments[0].names || new Array(); // Array of "org.pkijs.simpl.GENERAL_NAME" + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.GENERAL_NAMES.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + new in_window.org.pkijs.asn1.REPEATED({ + name: "names", + value: in_window.org.pkijs.schema.GENERAL_NAME() + }) + ] + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for GENERAL_NAMES"); + // #endregion + + // #region Get internal properties from parsed schema + var n = asn1.result["names"]; + + for(var i = 0; i < n.length; i++) + this.names.push(new in_window.org.pkijs.simpl.GENERAL_NAME({ schema: n[i] })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.GENERAL_NAMES.prototype.toSchema = + function(schema) + { + // #region Construct and return new ASN.1 schema for this object + var output_array = new Array(); + + for(var i = 0; i < this.names.length; i++) + output_array.push(this.names[i].toSchema()); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.GENERAL_NAMES.prototype.toJSON = + function() + { + var _names = new Array(); + + for(var i = 0; i < this.names.length; i++) + _names.push(this.names[i].toJSON()); + + return { + names: _names + }; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "AlgorithmIdentifier" type + //************************************************************************************** + in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER = + function() + { + // #region Internal properties of the object + this.algorithm_id = ""; + // OPTIONAL this.algorithm_params = new in_window.org.pkijs.asn1.NULL(); + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.algorithm_id = arguments[0].algorithm_id || ""; + if("algorithm_params" in arguments[0]) + this.algorithm_params = arguments[0].algorithm_params; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.ALGORITHM_IDENTIFIER({ + names: { + algorithmIdentifier: "algorithm", + algorithmParams: "params" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for ALGORITHM_IDENTIFIER"); + // #endregion + + // #region Get internal properties from parsed schema + this.algorithm_id = asn1.result.algorithm.value_block.toString(); + if("params" in asn1.result) + this.algorithm_params = asn1.result.params; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + output_array.push(new in_window.org.pkijs.asn1.OID({ value: this.algorithm_id })); + if("algorithm_params" in this) + output_array.push(this.algorithm_params); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER.prototype.getCommonName = + function() + { + }; + //************************************************************************************** + in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER.prototype.toJSON = + function() + { + var _object = { + algorithm_id: this.algorithm_id + }; + + if("algorithm_params" in this) + _object.algorithm_params = this.algorithm_params.toJSON(); + + return _object; + }; + //************************************************************************************** + in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER.prototype.isEqual = + function(algorithmIdentifier) + { + /// <summary>Check that two "ALGORITHM_IDENTIFIERs" are equal</summary> + /// <param name="algorithmIdentifier" type="in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER">The algorithm identifier to compare with</param> + + // #region Check input type + if((algorithmIdentifier instanceof in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER) == false) + return false; + // #endregion + + // #region Check "algorithm_id" + if(this.algorithm_id != algorithmIdentifier.algorithm_id) + return false; + // #endregion + + // #region Check "algorithm_params" + if("algorithm_params" in this) + { + if("algorithm_params" in algorithmIdentifier) + { + return JSON.stringify(this.algorithm_params) == JSON.stringify(algorithmIdentifier.algorithm_params); + } + else + return false; + } + else + { + if("algorithm_params" in algorithmIdentifier) + return false; + } + // #endregion + + return true; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "RSAPublicKey" type (RFC3447) + //************************************************************************************** + in_window.org.pkijs.simpl.x509.RSAPublicKey = + function() + { + // #region Internal properties of the object + this.modulus = new in_window.org.pkijs.asn1.INTEGER(); + this.publicExponent = new in_window.org.pkijs.asn1.INTEGER(); + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.RSAPublicKey.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.modulus = arguments[0].modulus || new in_window.org.pkijs.asn1.INTEGER(); + this.publicExponent = arguments[0].publicExponent || new in_window.org.pkijs.asn1.INTEGER(); + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.RSAPublicKey.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.RSAPublicKey({ + names: { + modulus: "modulus", + publicExponent: "publicExponent" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for RSAPublicKey"); + // #endregion + + // #region Get internal properties from parsed schema + this.modulus = asn1.result["modulus"]; + this.publicExponent = asn1.result["publicExponent"]; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.RSAPublicKey.prototype.toSchema = + function() + { + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + this.modulus, + this.publicExponent + ] + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.RSAPublicKey.prototype.toJSON = + function() + { + return { + modulus: this.modulus.toJSON(), + publicExponent: this.publicExponent.toJSON() + }; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "OtherPrimeInfo" type (RFC3447) + //************************************************************************************** + in_window.org.pkijs.simpl.x509.OtherPrimeInfo = + function() + { + // #region Internal properties of the object + this.prime = new in_window.org.pkijs.asn1.INTEGER(); + this.exponent = new in_window.org.pkijs.asn1.INTEGER(); + this.coefficient = new in_window.org.pkijs.asn1.INTEGER(); + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.OtherPrimeInfo.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.prime = arguments[0].prime || new in_window.org.pkijs.asn1.INTEGER(); + this.exponent = arguments[0].exponent || new in_window.org.pkijs.asn1.INTEGER(); + this.coefficient = arguments[0].coefficient || new in_window.org.pkijs.asn1.INTEGER(); + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.OtherPrimeInfo.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.OtherPrimeInfo({ + names: { + prime: "prime", + exponent: "exponent", + coefficient: "coefficient" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for OtherPrimeInfo"); + // #endregion + + // #region Get internal properties from parsed schema + this.prime = asn1.result["prime"]; + this.exponent = asn1.result["exponent"]; + this.coefficient = asn1.result["coefficient"]; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.OtherPrimeInfo.prototype.toSchema = + function() + { + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + this.prime, + this.exponent, + this.coefficient + ] + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.OtherPrimeInfo.prototype.toJSON = + function() + { + return { + prime: this.prime.toJSON(), + exponent: this.exponent.toJSON(), + coefficient: this.coefficient.toJSON() + }; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "RSAPrivateKey" type (RFC3447) + //************************************************************************************** + in_window.org.pkijs.simpl.x509.RSAPrivateKey = + function() + { + // #region Internal properties of the object + this.version = 0; + this.modulus = new in_window.org.pkijs.asn1.INTEGER(); + this.publicExponent = new in_window.org.pkijs.asn1.INTEGER(); + this.privateExponent = new in_window.org.pkijs.asn1.INTEGER(); + this.prime1 = new in_window.org.pkijs.asn1.INTEGER(); + this.prime2 = new in_window.org.pkijs.asn1.INTEGER(); + this.exponent1 = new in_window.org.pkijs.asn1.INTEGER(); + this.exponent2 = new in_window.org.pkijs.asn1.INTEGER(); + this.coefficient = new in_window.org.pkijs.asn1.INTEGER(); + // OPTIONAL this.otherPrimeInfos = new Array(); // Array of "OtherPrimeInfo" + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.RSAPrivateKey.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.version = arguments[0].version || 0; + this.modulus = arguments[0].modulus || new in_window.org.pkijs.asn1.INTEGER(); + this.publicExponent = arguments[0].publicExponent || new in_window.org.pkijs.asn1.INTEGER(); + this.privateExponent = arguments[0].privateExponent || new in_window.org.pkijs.asn1.INTEGER(); + this.prime1 = arguments[0].prime1 || new in_window.org.pkijs.asn1.INTEGER(); + this.prime2 = arguments[0].prime2 || new in_window.org.pkijs.asn1.INTEGER(); + this.exponent1 = arguments[0].exponent1 || new in_window.org.pkijs.asn1.INTEGER(); + this.exponent2 = arguments[0].exponent2 || new in_window.org.pkijs.asn1.INTEGER(); + this.coefficient = arguments[0].coefficient || new in_window.org.pkijs.asn1.INTEGER(); + if("otherPrimeInfos" in arguments[0]) + this.otherPrimeInfos = arguments[0].otherPrimeInfos || new Array(); + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.RSAPrivateKey.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.RSAPrivateKey({ + names: { + version: "version", + modulus: "modulus", + publicExponent: "publicExponent", + privateExponent: "privateExponent", + prime1: "prime1", + prime2: "prime2", + exponent1: "exponent1", + exponent2: "exponent2", + coefficient: "coefficient", + otherPrimeInfos: "otherPrimeInfos" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for RSAPrivateKey"); + // #endregion + + // #region Get internal properties from parsed schema + this.version = asn1.result["version"].value_block.value_dec; + this.modulus = asn1.result["modulus"]; + this.publicExponent = asn1.result["publicExponent"]; + this.privateExponent = asn1.result["privateExponent"]; + this.prime1 = asn1.result["prime1"]; + this.prime2 = asn1.result["prime2"]; + this.exponent1 = asn1.result["exponent1"]; + this.exponent2 = asn1.result["exponent2"]; + this.coefficient = asn1.result["coefficient"]; + + if("otherPrimeInfos" in asn1.result) + { + var otherPrimeInfos_array = asn1.result["otherPrimeInfos"]; + + for(var i = 0; i < otherPrimeInfos_array.length; i++) + this.otherPrimeInfos.push(new in_window.org.pkijs.simpl.x509.OtherPrimeInfo({ schema: otherPrimeInfos_array[i] })); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.RSAPrivateKey.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + output_array.push(new in_window.org.pkijs.asn1.INTEGER({ value: this.version })); + output_array.push(this.modulus); + output_array.push(this.publicExponent); + output_array.push(this.privateExponent); + output_array.push(this.prime1); + output_array.push(this.prime2); + output_array.push(this.exponent1); + output_array.push(this.exponent2); + output_array.push(this.coefficient); + + if("otherPrimeInfos" in this) + { + var otherPrimeInfos_array = new Array(); + + for(var i = 0; i < this.otherPrimeInfos.length; i++) + otherPrimeInfos_array.push(this.otherPrimeInfos[i].toSchema()); + + output_array.push(new in_window.org.pkijs.asn1.SEQUENCE({ value: otherPrimeInfos_array })); + } + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.RSAPrivateKey.prototype.toJSON = + function() + { + var _object = { + version: this.version, + modulus: this.modulus.toJSON(), + publicExponent: this.publicExponent.toJSON(), + privateExponent: this.privateExponent.toJSON(), + prime1: this.prime1.toJSON(), + prime2: this.prime2.toJSON(), + exponent1: this.exponent1.toJSON(), + exponent2: this.exponent2.toJSON(), + coefficient: this.coefficient.toJSON() + }; + + if("otherPrimeInfos" in this) + { + _object.otherPrimeInfos = new Array(); + + for(var i = 0; i < this.otherPrimeInfos.length; i++) + _object.otherPrimeInfos.push(this.otherPrimeInfos[i].toJSON()); + } + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "RSASSA_PSS_params" type (RFC3447) + //************************************************************************************** + in_window.org.pkijs.simpl.x509.RSASSA_PSS_params = + function() + { + // #region Internal properties of the object + // OPTIONAL this.hashAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); + // OPTIONAL this.maskGenAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); + // OPTIONAL this.saltLength = 20; // new in_window.org.pkijs.asn1.INTEGER(); + // OPTIONAL this.trailerField = 1; // new in_window.org.pkijs.asn1.INTEGER(); + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.RSASSA_PSS_params.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + if("hashAlgorithm" in arguments[0]) + this.hashAlgorithm = arguments[0].hashAlgorithm; + + if("maskGenAlgorithm" in arguments[0]) + this.maskGenAlgorithm = arguments[0].maskGenAlgorithm; + + if("saltLength" in arguments[0]) + this.saltLength = arguments[0].saltLength; + + if("trailerField" in arguments[0]) + this.trailerField = arguments[0].trailerField; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.RSASSA_PSS_params.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.RSASSA_PSS_params({ + names: { + hashAlgorithm: { + names: { + block_name: "hashAlgorithm" + } + }, + maskGenAlgorithm: { + names: { + block_name: "maskGenAlgorithm" + } + }, + saltLength: "saltLength", + trailerField: "trailerField" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for RSASSA_PSS_params"); + // #endregion + + // #region Get internal properties from parsed schema + if("hashAlgorithm" in asn1.result) + this.hashAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ schema: asn1.result["hashAlgorithm"] }); + + if("maskGenAlgorithm" in asn1.result) + this.maskGenAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ schema: asn1.result["maskGenAlgorithm"] }); + + if("saltLength" in asn1.result) + this.saltLength = asn1.result["saltLength"].value_block.value_dec; + + if("trailerField" in asn1.result) + this.trailerField = asn1.result["trailerField"].value_block.value_dec; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.RSASSA_PSS_params.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + if("hashAlgorithm" in this) + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [this.hashAlgorithm.toSchema()] + })); + + if("maskGenAlgorithm" in this) + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value: [this.maskGenAlgorithm.toSchema()] + })); + + if("saltLength" in this) + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 2 // [2] + }, + value: [new in_window.org.pkijs.asn1.INTEGER({ value: this.saltLength })] + })); + + if("trailerField" in this) + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 3 // [3] + }, + value: [new in_window.org.pkijs.asn1.INTEGER({ value: this.trailerField })] + })); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.RSASSA_PSS_params.prototype.toJSON = + function() + { + var _object = {}; + + if("hashAlgorithm" in this) + _object.hashAlgorithm = this.hashAlgorithm.toJSON(); + + if("maskGenAlgorithm" in this) + _object.maskGenAlgorithm = this.maskGenAlgorithm.toJSON(); + + if("saltLength" in this) + _object.saltLength = this.saltLength.toJSON(); + + if("trailerField" in this) + _object.trailerField = this.trailerField.toJSON(); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "SubjectPublicKeyInfo" type + //************************************************************************************** + in_window.org.pkijs.simpl.PUBLIC_KEY_INFO = + function() + { + // #region Internal properties of the object + this.algorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); + this.subjectPublicKey = new in_window.org.pkijs.asn1.BITSTRING(); + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.PUBLIC_KEY_INFO.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.algorithm = (arguments[0].algorithm || (new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER())); + this.subjectPublicKey = (arguments[0].subjectPublicKey || (new in_window.org.pkijs.asn1.BITSTRING())); + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.PUBLIC_KEY_INFO.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.PUBLIC_KEY_INFO({ + names: { + algorithm: { + names: { + block_name: "algorithm" + } + }, + subjectPublicKey: "subjectPublicKey" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for PUBLIC_KEY_INFO"); + // #endregion + + // #region Get internal properties from parsed schema + this.algorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ schema: asn1.result.algorithm }); + this.subjectPublicKey = asn1.result.subjectPublicKey; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.PUBLIC_KEY_INFO.prototype.toSchema = + function() + { + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + this.algorithm.toSchema(), + this.subjectPublicKey + ] + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.PUBLIC_KEY_INFO.prototype.importKey = + function(publicKey) + { + /// <param name="publicKey" type="Key">Public key to work with</param> + + // #region Initial variables + var sequence = Promise.resolve(); + var _this = this; + // #endregion + + // #region Initial check + if(typeof publicKey === "undefined") + return new Promise(function(resolve, reject) { reject("Need to provide publicKey input parameter"); }); + // #endregion + + // #region Get a "crypto" extension + var crypto = in_window.org.pkijs.getCrypto(); + if(typeof crypto == "undefined") + return new Promise(function(resolve, reject) { reject("Unable to create WebCrypto object"); }); + // #endregion + + // #region Export public key + sequence = sequence.then( + function() + { + return crypto.exportKey("spki", publicKey); + } + ); + // #endregion + + // #region Initialize internal variables by parsing exported value + sequence = sequence.then( + function(exportedKey) + { + var asn1 = in_window.org.pkijs.fromBER(exportedKey); + try + { + in_window.org.pkijs.simpl.PUBLIC_KEY_INFO.prototype.fromSchema.call(_this, asn1.result); + } + catch(exception) + { + return new Promise(function(resolve, reject) { reject("Error during initializing object from schema"); }); + } + }, + function(error) + { + return new Promise(function(resolve, reject) { reject("Error during exporting public key: " + error); }); + } + ); + // #endregion + + return sequence; + }; + //************************************************************************************** + in_window.org.pkijs.simpl.PUBLIC_KEY_INFO.prototype.toJSON = + function() + { + return { + algorithm: this.algorithm.toJSON(), + subjectPublicKey: this.subjectPublicKey.toJSON() + }; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "AttributeTypeAndValue" type (part of RelativeDistinguishedName) + //************************************************************************************** + in_window.org.pkijs.simpl.ATTR_TYPE_AND_VALUE = + function() + { + // #region Internal properties of the object + this.type = ""; + this.value = {}; // ANY -- DEFINED BY AttributeType + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.ATTR_TYPE_AND_VALUE.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.type = (arguments[0].type || ""); + this.value = (arguments[0].value || {}); + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.ATTR_TYPE_AND_VALUE.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.ATTR_TYPE_AND_VALUE({ + names: { + type: "type", + value: "typeValue" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for ATTR_TYPE_AND_VALUE"); + // #endregion + + // #region Get internal properties from parsed schema + this.type = asn1.result.type.value_block.toString(); + this.value = asn1.result.typeValue; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.ATTR_TYPE_AND_VALUE.prototype.toSchema = + function() + { + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + new in_window.org.pkijs.asn1.OID({ value: this.type }), + this.value + ] + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.ATTR_TYPE_AND_VALUE.prototype.isEqual = + function() + { + if(arguments[0] instanceof in_window.org.pkijs.simpl.ATTR_TYPE_AND_VALUE) + { + if(this.type !== arguments[0].type) + return false; + + if(((this.value instanceof in_window.org.pkijs.asn1.UTF8STRING) && (arguments[0].value instanceof in_window.org.pkijs.asn1.UTF8STRING)) || + ((this.value instanceof in_window.org.pkijs.asn1.BMPSTRING) && (arguments[0].value instanceof in_window.org.pkijs.asn1.BMPSTRING)) || + ((this.value instanceof in_window.org.pkijs.asn1.UNIVERSALSTRING) && (arguments[0].value instanceof in_window.org.pkijs.asn1.UNIVERSALSTRING)) || + ((this.value instanceof in_window.org.pkijs.asn1.NUMERICSTRING) && (arguments[0].value instanceof in_window.org.pkijs.asn1.NUMERICSTRING)) || + ((this.value instanceof in_window.org.pkijs.asn1.PRINTABLESTRING) && (arguments[0].value instanceof in_window.org.pkijs.asn1.PRINTABLESTRING)) || + ((this.value instanceof in_window.org.pkijs.asn1.TELETEXSTRING) && (arguments[0].value instanceof in_window.org.pkijs.asn1.TELETEXSTRING)) || + ((this.value instanceof in_window.org.pkijs.asn1.VIDEOTEXSTRING) && (arguments[0].value instanceof in_window.org.pkijs.asn1.VIDEOTEXSTRING)) || + ((this.value instanceof in_window.org.pkijs.asn1.IA5STRING) && (arguments[0].value instanceof in_window.org.pkijs.asn1.IA5STRING)) || + ((this.value instanceof in_window.org.pkijs.asn1.GRAPHICSTRING) && (arguments[0].value instanceof in_window.org.pkijs.asn1.GRAPHICSTRING)) || + ((this.value instanceof in_window.org.pkijs.asn1.VISIBLESTRING) && (arguments[0].value instanceof in_window.org.pkijs.asn1.VISIBLESTRING)) || + ((this.value instanceof in_window.org.pkijs.asn1.GENERALSTRING) && (arguments[0].value instanceof in_window.org.pkijs.asn1.GENERALSTRING)) || + ((this.value instanceof in_window.org.pkijs.asn1.CHARACTERSTRING) && (arguments[0].value instanceof in_window.org.pkijs.asn1.CHARACTERSTRING))) + { + var value1 = in_window.org.pkijs.stringPrep(this.value.value_block.value); + var value2 = in_window.org.pkijs.stringPrep(arguments[0].value.value_block.value); + + if(value1.localeCompare(value2) !== 0) + return false; + } + else // Comparing as two ArrayBuffers + { + if(in_window.org.pkijs.isEqual_buffer(this.value.value_before_decode, arguments[0].value.value_before_decode) === false) + return false; + } + + return true; + } + else + return false; + }; + //************************************************************************************** + in_window.org.pkijs.simpl.ATTR_TYPE_AND_VALUE.prototype.toJSON = + function() + { + var _object = { + type: this.type + }; + + if(Object.keys(this.value).length !== 0) + _object.value = this.value.toJSON(); + else + _object.value = this.value; + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "RelativeDistinguishedName" type + //************************************************************************************** + in_window.org.pkijs.simpl.RDN = + function() + { + // #region Internal properties of the object + /// <field name="types_and_values" type="Array" elementType="in_window.org.pkijs.simpl.ATTR_TYPE_AND_VALUE">Array of "type and value" objects</field> + this.types_and_values = new Array(); + /// <field name="value_before_decode" type="ArrayBuffer">Value of the RDN before decoding from schema</field> + this.value_before_decode = new ArrayBuffer(0); + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.RDN.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.types_and_values = (arguments[0].types_and_values || (new Array())); + this.value_before_decode = arguments[0].value_before_decode || new ArrayBuffer(0); + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.RDN.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.RDN({ + names: { + block_name: "RDN", + repeated_set: "types_and_values" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for RDN"); + // #endregion + + // #region Get internal properties from parsed schema + if("types_and_values" in asn1.result) // Could be a case when there is no "types and values" + { + var types_and_values_array = asn1.result.types_and_values; + for(var i = 0; i < types_and_values_array.length; i++) + this.types_and_values.push(new in_window.org.pkijs.simpl.ATTR_TYPE_AND_VALUE({ schema: types_and_values_array[i] })); + } + + this.value_before_decode = asn1.result.RDN.value_before_decode; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.RDN.prototype.toSchema = + function() + { + // #region Decode stored TBS value + if(this.value_before_decode.byteLength === 0) // No stored encoded array, create "from scratch" + { + // #region Create array for output set + var output_array = new Array(); + + for(var i = 0; i < this.types_and_values.length; i++) + output_array.push(this.types_and_values[i].toSchema()); + // #endregion + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [new in_window.org.pkijs.asn1.SET({ value: output_array })] + })); + } + + var asn1 = in_window.org.pkijs.fromBER(this.value_before_decode); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return asn1.result; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.RDN.prototype.isEqual = + function() + { + if(arguments[0] instanceof in_window.org.pkijs.simpl.RDN) + { + if(this.types_and_values.length != arguments[0].types_and_values.length) + return false; + + for(var i = 0; i < this.types_and_values.length; i++) + { + if(this.types_and_values[i].isEqual(arguments[0].types_and_values[i]) === false) + return false; + } + + return true; + } + else + { + if(arguments[0] instanceof ArrayBuffer) + return in_window.org.pkijs.isEqual_buffer(this.value_before_decode, arguments[0]); + else + return false; + } + + return false; + }; + //************************************************************************************** + in_window.org.pkijs.simpl.RDN.prototype.toJSON = + function() + { + var _object = { + types_and_values: new Array() + }; + + for(var i = 0; i < this.types_and_values.length; i++) + _object.types_and_values.push(this.types_and_values[i].toJSON()); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "AuthorityKeyIdentifier" type of extension + //************************************************************************************** + in_window.org.pkijs.simpl.x509.AuthorityKeyIdentifier = + function() + { + // #region Internal properties of the object + // OPTIONAL this.keyIdentifier - OCTETSTRING + // OPTIONAL this.authorityCertIssuer - Array of GeneralName + // OPTIONAL this.authorityCertSerialNumber - INTEGER + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.AuthorityKeyIdentifier.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + if("keyIdentifier" in arguments[0]) + this.keyIdentifier = arguments[0].keyIdentifier; + + if("authorityCertIssuer" in arguments[0]) + this.authorityCertIssuer = arguments[0].authorityCertIssuer; + + if("authorityCertSerialNumber" in arguments[0]) + this.authorityCertSerialNumber = arguments[0].authorityCertSerialNumber; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.AuthorityKeyIdentifier.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.AuthorityKeyIdentifier({ + names: { + keyIdentifier: "keyIdentifier", + authorityCertIssuer: "authorityCertIssuer", + authorityCertSerialNumber: "authorityCertSerialNumber" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for AuthorityKeyIdentifier"); + // #endregion + + // #region Get internal properties from parsed schema + if("keyIdentifier" in asn1.result) + { + asn1.result["keyIdentifier"].id_block.tag_class = 1; // UNIVERSAL + asn1.result["keyIdentifier"].id_block.tag_number = 4; // OCTETSTRING + + this.keyIdentifier = asn1.result["keyIdentifier"]; + } + + if("authorityCertIssuer" in asn1.result) + { + this.authorityCertIssuer = new Array(); + var issuer_array = asn1.result["authorityCertIssuer"]; + + for(var i = 0; i < issuer_array.length; i++) + this.authorityCertIssuer.push(new in_window.org.pkijs.simpl.GENERAL_NAME({ schema: issuer_array[i] })); + } + + if("authorityCertSerialNumber" in asn1.result) + { + asn1.result["authorityCertSerialNumber"].id_block.tag_class = 1; // UNIVERSAL + asn1.result["authorityCertSerialNumber"].id_block.tag_number = 2; // INTEGER + + this.authorityCertSerialNumber = asn1.result["authorityCertSerialNumber"]; + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.AuthorityKeyIdentifier.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + if("keyIdentifier" in this) + { + var value = this.keyIdentifier; + + value.id_block.tag_class = 3; // CONTEXT-SPECIFIC + value.id_block.tag_number = 0; // [0] + + output_array.push(value); + } + + if("authorityCertIssuer" in this) + { + var issuer_array = new Array(); + + for(var i = 0; i < this.authorityCertIssuer.length; i++) + issuer_array.push(this.authorityCertIssuer[i].toSchema()); + + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value: [new in_window.org.pkijs.asn1.SEQUENCE({ + value: issuer_array + })] + })); + } + + if("authorityCertSerialNumber" in this) + { + var value = this.authorityCertSerialNumber; + + value.id_block.tag_class = 3; // CONTEXT-SPECIFIC + value.id_block.tag_number = 2; // [2] + + output_array.push(value); + } + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.AuthorityKeyIdentifier.prototype.toJSON = + function() + { + var _object = {}; + + if("keyIdentifier" in this) + _object.keyIdentifier = this.keyIdentifier.toJSON(); + + if("authorityCertIssuer" in this) + { + _object.authorityCertIssuer = new Array(); + + for(var i = 0; i < this.authorityCertIssuer.length; i++) + _object.authorityCertIssuer.push(this.authorityCertIssuer[i].toJSON()); + } + + if("authorityCertSerialNumber" in this) + _object.authorityCertSerialNumber = this.authorityCertSerialNumber.toJSON(); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "PrivateKeyUsagePeriod" type of extension + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PrivateKeyUsagePeriod = + function() + { + // #region Internal properties of the object + // OPTIONAL this.notBefore - new Date() + // OPTIONAL this.notAfter - new Date() + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.PrivateKeyUsagePeriod.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + if("notBefore" in arguments[0]) + this.notBefore = arguments[0].notBefore; + + if("notAfter" in arguments[0]) + this.notAfter = arguments[0].notAfter; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PrivateKeyUsagePeriod.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.PrivateKeyUsagePeriod({ + names: { + notBefore: "notBefore", + notAfter: "notAfter" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for PrivateKeyUsagePeriod"); + // #endregion + + // #region Get internal properties from parsed schema + if("notBefore" in asn1.result) + { + var localNotBefore = new in_window.org.pkijs.asn1.GENERALIZEDTIME(); + localNotBefore.fromBuffer(asn1.result["notBefore"].value_block.value_hex); + this.notBefore = localNotBefore.toDate(); + } + + if("notAfter" in asn1.result) + { + var localNotAfter = new in_window.org.pkijs.asn1.GENERALIZEDTIME({ value_hex: asn1.result["notAfter"].value_block.value_hex }); + localNotAfter.fromBuffer(asn1.result["notAfter"].value_block.value_hex); + this.notAfter = localNotAfter.toDate(); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PrivateKeyUsagePeriod.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + if("notBefore" in this) + output_array.push(new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value_hex: (new in_window.org.pkijs.asn1.GENERALIZEDTIME({ value_date: this.notBefore })).value_block.value_hex + })); + + if("notAfter" in this) + output_array.push(new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value_hex: (new in_window.org.pkijs.asn1.GENERALIZEDTIME({ value_date: this.notAfter })).value_block.value_hex + })); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PrivateKeyUsagePeriod.prototype.toJSON = + function() + { + var _object = {}; + + if("notBefore" in this) + _object.notBefore = this.notBefore; + + if("notAfter" in this) + _object.notAfter = this.notAfter; + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "IssuerAltName" and "SubjectAltName" types of extension + //************************************************************************************** + in_window.org.pkijs.simpl.x509.AltName = + function() + { + // #region Internal properties of the object + this.altNames = new Array(); //Array of GeneralName + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.AltName.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.altNames = arguments[0].altNames || new Array(); + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.AltName.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.AltName({ + names: { + altNames: "altNames" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for AltName"); + // #endregion + + // #region Get internal properties from parsed schema + if("altNames" in asn1.result) + { + var altNames_array = asn1.result["altNames"]; + + for(var i = 0; i < altNames_array.length; i++) + this.altNames.push(new in_window.org.pkijs.simpl.GENERAL_NAME({ schema: altNames_array[i] })); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.AltName.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + for(var i = 0; i < this.altNames.length; i++) + output_array.push(this.altNames[i].toSchema()); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.AltName.prototype.toJSON = + function() + { + var _object = { + altNames: new Array() + }; + + for(var i = 0; i < this.altNames.length; i++) + _object.altNames.push(this.altNames[i].toJSON()); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "SubjectDirectoryAttributes" type of extension + //************************************************************************************** + in_window.org.pkijs.simpl.x509.SubjectDirectoryAttributes = + function() + { + // #region Internal properties of the object + this.attributes = new Array(); // Array of "simpl.ATTRIBUTE" + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.SubjectDirectoryAttributes.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.attributes = arguments[0].attributes || new Array(); // Array of "simpl.ATTRIBUTE" + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.SubjectDirectoryAttributes.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.SubjectDirectoryAttributes({ + names: { + attributes: "attributes" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for SubjectDirectoryAttributes"); + // #endregion + + // #region Get internal properties from parsed schema + var attrs = asn1.result["attributes"]; + + for(var i = 0; i < attrs.length; i++) + this.attributes.push(new in_window.org.pkijs.simpl.ATTRIBUTE({ schema: attrs[i] })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.SubjectDirectoryAttributes.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + for(var i = 0; i < this.attributes.length; i++) + output_array.push(this.attributes[i].toSchema()); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.SubjectDirectoryAttributes.prototype.toJSON = + function() + { + var _object = { + attributes: new Array() + }; + + for(var i = 0; i < this.attributes.length; i++) + _object.attributes.push(this.attributes[i].toJSON()); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "PolicyMapping" type + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyMapping = + function() + { + // #region Internal properties of the object + this.issuerDomainPolicy = ""; + this.subjectDomainPolicy = ""; + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.PolicyMapping.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.issuerDomainPolicy = arguments[0].issuerDomainPolicy || ""; + this.subjectDomainPolicy = arguments[0].subjectDomainPolicy || ""; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyMapping.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.PolicyMapping({ + names: { + issuerDomainPolicy: "issuerDomainPolicy", + subjectDomainPolicy: "subjectDomainPolicy" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for PolicyMapping"); + // #endregion + + // #region Get internal properties from parsed schema + this.issuerDomainPolicy = asn1.result["issuerDomainPolicy"].value_block.toString(); + this.subjectDomainPolicy = asn1.result["subjectDomainPolicy"].value_block.toString(); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyMapping.prototype.toSchema = + function() + { + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + new in_window.org.pkijs.asn1.OID({ value: this.issuerDomainPolicy }), + new in_window.org.pkijs.asn1.OID({ value: this.subjectDomainPolicy }) + ] + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyMapping.prototype.toJSON = + function() + { + return { + issuerDomainPolicy: this.issuerDomainPolicy, + subjectDomainPolicy: this.subjectDomainPolicy + }; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "PolicyMappings" type of extension + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyMappings = + function() + { + // #region Internal properties of the object + this.mappings = new Array(); // Array of "simpl.x509.PolicyMapping" + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.PolicyMappings.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.mappings = arguments[0].mappings || new Array(); + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyMappings.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.PolicyMappings({ + names: { + mappings: "mappings" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for PolicyMappings"); + // #endregion + + // #region Get internal properties from parsed schema + var maps = asn1.result["mappings"]; + + for(var i = 0; i < maps.length; i++) + this.mappings.push(new in_window.org.pkijs.simpl.x509.PolicyMapping({ schema: maps[i] })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyMappings.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + for(var i = 0; i < this.mappings.length; i++) + output_array.push(this.mappings.toSchema()); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyMappings.prototype.toJSON = + function() + { + var _object = { + mappings: new Array() + }; + + for(var i = 0; i < this.mappings.length; i++) + _object.mappings.push(this.mappings[i].toJSON()); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "GeneralSubtree" type + //************************************************************************************** + in_window.org.pkijs.simpl.x509.GeneralSubtree = + function() + { + // #region Internal properties of the object + this.base = new in_window.org.pkijs.simpl.GENERAL_NAME(); + // OPTIONAL this.minimum // in_window.org.pkijs.asn1.INTEGER + // OPTIONAL this.maximum // in_window.org.pkijs.asn1.INTEGER + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.GeneralSubtree.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.base = arguments[0].base || new in_window.org.pkijs.simpl.GENERAL_NAME(); + + if("minimum" in arguments[0]) + this.minimum = arguments[0].minimum; + + if("maximum" in arguments[0]) + this.maximum = arguments[0].maximum; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.GeneralSubtree.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.GeneralSubtree({ + names: { + base: { + names: { + block_name: "base" + } + }, + minimum: "minimum", + maximum: "maximum" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for "); + // #endregion + + // #region Get internal properties from parsed schema + this.base = new in_window.org.pkijs.simpl.GENERAL_NAME({ schema: asn1.result["base"] }); + + if("minimum" in asn1.result) + { + if(asn1.result["minimum"].value_block.is_hex_only) + this.minimum = asn1.result["minimum"]; + else + this.minimum = asn1.result["minimum"].value_block.value_dec; + } + + if("maximum" in asn1.result) + { + if(asn1.result["maximum"].value_block.is_hex_only) + this.maximum = asn1.result["maximum"]; + else + this.maximum = asn1.result["maximum"].value_block.value_dec; + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.GeneralSubtree.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + output_array.push(this.base.toSchema()); + + if("minimum" in this) + { + var value_minimum = 0; + + if(this.minimum instanceof in_window.org.pkijs.asn1.INTEGER) + value_minimum = this.minimum; + else + value_minimum = new in_window.org.pkijs.asn1.INTEGER({ value: this.minimum }); + + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [value_minimum] + })); + } + + if("maximum" in this) + { + var value_maximum = 0; + + if(this.maximum instanceof in_window.org.pkijs.asn1.INTEGER) + value_maximum = this.maximum; + else + value_maximum = new in_window.org.pkijs.asn1.INTEGER({ value: this.maximum }); + + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value: [value_maximum] + })); + } + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.GeneralSubtree.prototype.toJSON = + function() + { + var _object = { + base: this.base.toJSON() + }; + + if("minimum" in this) + { + if((typeof this.minimum) === "number") + _object.minimum = this.minimum; + else + _object.minimum = this.minimum.toJSON(); + } + + if("maximum" in this) + { + if((typeof this.maximum) === "number") + _object.maximum = this.maximum; + else + _object.maximum = this.maximum.toJSON(); + } + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "NameConstraints" type of extension + //************************************************************************************** + in_window.org.pkijs.simpl.x509.NameConstraints = + function() + { + // #region Internal properties of the object + // OPTIONAL this.permittedSubtrees - Array of "simpl.x509.GeneralSubtree" + // OPTIONAL this.excludedSubtrees - Array of "simpl.x509.GeneralSubtree" + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.NameConstraints.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + if("permittedSubtrees" in arguments[0]) + this.permittedSubtrees = arguments[0].permittedSubtrees; + + if("excludedSubtrees" in arguments[0]) + this.excludedSubtrees = arguments[0].excludedSubtrees; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.NameConstraints.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.NameConstraints({ + names: { + permittedSubtrees: "permittedSubtrees", + excludedSubtrees: "excludedSubtrees" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for NameConstraints"); + // #endregion + + // #region Get internal properties from parsed schema + if("permittedSubtrees" in asn1.result) + { + this.permittedSubtrees = new Array(); + var permited_array = asn1.result["permittedSubtrees"]; + + for(var i = 0; i < permited_array.length; i++) + this.permittedSubtrees.push(new in_window.org.pkijs.simpl.x509.GeneralSubtree({ schema: permited_array[i] })); + } + + if("excludedSubtrees" in asn1.result) + { + this.excludedSubtrees = new Array(); + var excluded_array = asn1.result["excludedSubtrees"]; + + for(var i = 0; i < excluded_array.length; i++) + this.excludedSubtrees.push(new in_window.org.pkijs.simpl.x509.GeneralSubtree({ schema: excluded_array[i] })); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.NameConstraints.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + if("permittedSubtrees" in this) + { + var permited_array = new Array(); + + for(var i = 0; i < this.permittedSubtrees.length; i++) + permited_array.push(this.permittedSubtrees[i].toSchema()); + + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [new in_window.org.pkijs.asn1.SEQUENCE({ + value: permited_array + })] + })); + } + + if("excludedSubtrees" in this) + { + var excluded_array = new Array(); + + for(var i = 0; i < this.excludedSubtrees.length; i++) + excluded_array.push(this.excludedSubtrees[i].toSchema()); + + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value: [new in_window.org.pkijs.asn1.SEQUENCE({ + value: excluded_array + })] + })); + } + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.NameConstraints.prototype.toJSON = + function() + { + var _object = {}; + + if("permittedSubtrees" in this) + { + _object.permittedSubtrees = new Array(); + + for(var i = 0; i < this.permittedSubtrees.length; i++) + _object.permittedSubtrees.push(this.permittedSubtrees[i].toJSON()); + } + + if("excludedSubtrees" in this) + { + _object.excludedSubtrees = new Array(); + + for(var i = 0; i < this.excludedSubtrees.length; i++) + _object.excludedSubtrees.push(this.excludedSubtrees[i].toJSON()); + } + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "BasicConstraints" type of extension + //************************************************************************************** + in_window.org.pkijs.simpl.x509.BasicConstraints = + function() + { + // #region Internal properties of the object + // OPTIONAL this.cA - boolean value + // OPTIONAL this.pathLenConstraint - integer value + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.BasicConstraints.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + if("cA" in arguments[0]) + this.cA = arguments[0].cA; + + if("pathLenConstraint" in arguments[0]) + this.pathLenConstraint = arguments[0].pathLenConstraint; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.BasicConstraints.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.BasicConstraints({ + names: { + cA: "cA", + pathLenConstraint: "pathLenConstraint" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for BasicConstraints"); + // #endregion + + // #region Get internal properties from parsed schema + if("cA" in asn1.result) + this.cA = asn1.result["cA"].value_block.value; + + if("pathLenConstraint" in asn1.result) + this.pathLenConstraint = asn1.result["pathLenConstraint"].value_block.value_dec; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.BasicConstraints.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + if("cA" in this) + output_array.push(new in_window.org.pkijs.asn1.BOOLEAN({ value: this.cA })); + + if("pathLenConstraint" in this) + output_array.push(new in_window.org.pkijs.asn1.INTEGER({ value: this.pathLenConstraint })); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.BasicConstraints.prototype.toJSON = + function() + { + var _object = {}; + + if("cA" in this) + _object.cA = this.cA; + + if("pathLenConstraint" in this) + _object.pathLenConstraint = this.pathLenConstraint; + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "PolicyQualifierInfo" type + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyQualifierInfo = + function() + { + // #region Internal properties of the object + this.policyQualifierId = ""; + this.qualifier = new in_window.org.pkijs.asn1.ANY(); + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.PolicyQualifierInfo.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.policyQualifierId = arguments[0].policyQualifierId || ""; + this.qualifier = arguments[0].qualifier || new in_window.org.pkijs.asn1.ANY(); + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyQualifierInfo.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.PolicyQualifierInfo({ + names: { + policyQualifierId: "policyQualifierId", + qualifier: "qualifier" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for PolicyQualifierInfo"); + // #endregion + + // #region Get internal properties from parsed schema + this.policyQualifierId = asn1.result["policyQualifierId"].value_block.toString(); + this.qualifier = asn1.result["qualifier"]; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyQualifierInfo.prototype.toSchema = + function() + { + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + new in_window.org.pkijs.asn1.OID({ value: this.policyQualifierId }), + this.qualifier + ] + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyQualifierInfo.prototype.toJSON = + function() + { + return { + policyQualifierId: this.policyQualifierId, + qualifier: this.qualifier.toJSON() + }; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "PolicyInformation" type + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyInformation = + function() + { + // #region Internal properties of the object + this.policyIdentifier = ""; + // OPTIONAL this.policyQualifiers = new Array(); // Array of "simpl.x509.PolicyQualifierInfo" + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.PolicyInformation.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.policyIdentifier = arguments[0].policyIdentifier || ""; + + if("policyQualifiers" in arguments[0]) + this.policyQualifiers = arguments[0].policyQualifiers; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyInformation.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.PolicyInformation({ + names: { + policyIdentifier: "policyIdentifier", + policyQualifiers: "policyQualifiers" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for PolicyInformation"); + // #endregion + + // #region Get internal properties from parsed schema + this.policyIdentifier = asn1.result["policyIdentifier"].value_block.toString(); + + if("policyQualifiers" in asn1.result) + { + this.policyQualifiers = new Array(); + var qualifiers = asn1.result["policyQualifiers"]; + + for(var i = 0; i < qualifiers.length; i++) + this.policyQualifiers.push(new in_window.org.pkijs.simpl.x509.PolicyQualifierInfo({ schema: qualifiers[i] })); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyInformation.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + output_array.push(new in_window.org.pkijs.asn1.OID({ value: this.policyIdentifier })); + + if("policyQualifiers" in this) + { + var qualifiers = new Array(); + + for(var i = 0; i < this.policyQualifiers.length; i++) + qualifiers.push(this.policyQualifiers[i].toSchema()); + + output_array.push(new in_window.org.pkijs.asn1.SEQUENCE({ + value: qualifiers + })); + } + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyInformation.prototype.toJSON = + function() + { + var _object = { + policyIdentifier: this.policyIdentifier + }; + + if("policyQualifiers" in this) + { + _object.policyQualifiers = new Array(); + + for(var i = 0; i < this.policyQualifiers.length; i++) + _object.policyQualifiers.push(this.policyQualifiers[i].toJSON()); + } + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "CertificatePolicies" type of extension + //************************************************************************************** + in_window.org.pkijs.simpl.x509.CertificatePolicies = + function() + { + // #region Internal properties of the object + this.certificatePolicies = new Array(); // Array of "simpl.x509.PolicyInformation" + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.CertificatePolicies.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.certificatePolicies = arguments[0].certificatePolicies || new Array(); // Array of "simpl.x509.PolicyInformation" + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.CertificatePolicies.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.CertificatePolicies({ + names: { + certificatePolicies: "certificatePolicies" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for CertificatePolicies"); + // #endregion + + // #region Get internal properties from parsed schema + var policies = asn1.result["certificatePolicies"]; + + for(var i = 0; i < policies.length; i++) + this.certificatePolicies.push(new in_window.org.pkijs.simpl.x509.PolicyInformation({ schema: policies[i] })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.CertificatePolicies.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + for(var i = 0; i < this.certificatePolicies.length; i++) + output_array.push(this.certificatePolicies[i].toSchema()); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.CertificatePolicies.prototype.toJSON = + function() + { + var _object = { + certificatePolicies: new Array() + }; + + for(var i = 0; i < this.certificatePolicies.length; i++) + _object.certificatePolicies.push(this.certificatePolicies[i].toJSON()); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "PolicyConstraints" type of extension + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyConstraints = + function() + { + // #region Internal properties of the object + // OPTIONAL this.requireExplicitPolicy = 0; + // OPTIONAL this.inhibitPolicyMapping = 0; + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.PolicyConstraints.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.requireExplicitPolicy = arguments[0].requireExplicitPolicy || 0; + this.inhibitPolicyMapping = arguments[0].inhibitPolicyMapping || 0; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyConstraints.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.PolicyConstraints({ + names: { + requireExplicitPolicy: "requireExplicitPolicy", + inhibitPolicyMapping: "inhibitPolicyMapping" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for PolicyConstraints"); + // #endregion + + // #region Get internal properties from parsed schema + if("requireExplicitPolicy" in asn1.result) + { + var field1 = asn1.result["requireExplicitPolicy"]; + + field1.id_block.tag_class = 1; // UNIVERSAL + field1.id_block.tag_number = 2; // INTEGER + + var ber1 = field1.toBER(false); + var int1 = in_window.org.pkijs.fromBER(ber1); + + this.requireExplicitPolicy = int1.result.value_block.value_dec; + } + + if("inhibitPolicyMapping" in asn1.result) + { + var field2 = asn1.result["inhibitPolicyMapping"]; + + field2.id_block.tag_class = 1; // UNIVERSAL + field2.id_block.tag_number = 2; // INTEGER + + var ber2 = field2.toBER(false); + var int2 = in_window.org.pkijs.fromBER(ber2); + + this.inhibitPolicyMapping = int2.result.value_block.value_dec; + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyConstraints.prototype.toSchema = + function() + { + // #region Create correct values for output sequence + var output_array = new Array(); + + if("requireExplicitPolicy" in this) + { + var int1 = new in_window.org.pkijs.asn1.INTEGER({ value: this.requireExplicitPolicy }); + + int1.id_block.tag_class = 3; // CONTEXT-SPECIFIC + int1.id_block.tag_number = 0; // [0] + + output_array.push(int1); + } + + if("inhibitPolicyMapping" in this) + { + var int2 = new in_window.org.pkijs.asn1.INTEGER({ value: this.inhibitPolicyMapping }); + + int1.id_block.tag_class = 3; // CONTEXT-SPECIFIC + int1.id_block.tag_number = 1; // [1] + + output_array.push(int2); + } + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.PolicyConstraints.prototype.toJSON = + function() + { + var _object = {}; + + if("requireExplicitPolicy" in this) + _object.requireExplicitPolicy = this.requireExplicitPolicy; + + if("inhibitPolicyMapping" in this) + _object.inhibitPolicyMapping = this.inhibitPolicyMapping; + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "ExtKeyUsage" type of extension + //************************************************************************************** + in_window.org.pkijs.simpl.x509.ExtKeyUsage = + function() + { + // #region Internal properties of the object + this.keyPurposes = new Array(); // Array of strings (OIDs value for key purposes) + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.ExtKeyUsage.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.keyPurposes = arguments[0].keyPurposes || new Array(); // Array of strings (OIDs value for key purposes) + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.ExtKeyUsage.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.ExtKeyUsage({ + names: { + keyPurposes: "keyPurposes" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for ExtKeyUsage"); + // #endregion + + // #region Get internal properties from parsed schema + var purposes = asn1.result["keyPurposes"]; + + for(var i = 0; i < purposes.length; i++) + this.keyPurposes.push(purposes[i].value_block.toString()); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.ExtKeyUsage.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + for(var i = 0; i < this.keyPurposes.length; i++) + output_array.push(new in_window.org.pkijs.asn1.OID({ value: this.keyPurposes[i] })); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.ExtKeyUsage.prototype.toJSON = + function() + { + var _object = { + keyPurposes: new Array() + }; + + for(var i = 0; i < this.keyPurposes.length; i++) + _object.keyPurposes.push(this.keyPurposes[i]); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "DistributionPoint" type + //************************************************************************************** + in_window.org.pkijs.simpl.x509.DistributionPoint = + function() + { + // #region Internal properties of the object + // OPTIONAL this.distributionPoint // Array of "simpl.GENERAL_NAME" or a value of "simpl.RDN" type + // OPTIONAL this.reasons // BITSTRING value + // OPTIONAL this.cRLIssuer // Array of "simpl.GENERAL_NAME" + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.DistributionPoint.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + if("distributionPoint" in arguments[0]) + this.distributionPoint = arguments[0].distributionPoint; + + if("reasons" in arguments[0]) + this.reasons = arguments[0].reasons; + + if("cRLIssuer" in arguments[0]) + this.cRLIssuer = arguments[0].cRLIssuer; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.DistributionPoint.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.DistributionPoint({ + names: { + distributionPoint: "distributionPoint", + distributionPoint_names: "distributionPoint_names", + reasons: "reasons", + cRLIssuer: "cRLIssuer", + cRLIssuer_names: "cRLIssuer_names" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for DistributionPoint"); + // #endregion + + // #region Get internal properties from parsed schema + if("distributionPoint" in asn1.result) + { + if(asn1.result["distributionPoint"].id_block.tag_number == 0) // GENERAL_NAMES variant + { + this.distributionPoint = new Array(); + var names = asn1.result["distributionPoint_names"]; + + for(var i = 0; i < names.length; i++) + this.distributionPoint.push(new in_window.org.pkijs.simpl.GENERAL_NAME({ schema: names[i] })); + } + + if(asn1.result["distributionPoint"].id_block.tag_number == 1) // RDN variant + { + asn1.result["distributionPoint"].id_block.tag_class = 1; // UNIVERSAL + asn1.result["distributionPoint"].id_block.tag_number = 16; // SEQUENCE + + this.distributionPoint = new in_window.org.pkijs.simpl.RDN({ schema: asn1.result["distributionPoint"] }); + } + } + + if("reasons" in asn1.result) + this.reasons = new in_window.org.pkijs.asn1.BITSTRING({ value_hex: asn1.result["reasons"].value_block.value_hex }); + + if("cRLIssuer" in asn1.result) + { + this.cRLIssuer = new Array(); + var crl_names = asn1.result["cRLIssuer_names"]; + + for(var i = 0; i < crl_names; i++) + this.cRLIssuer.push(new in_window.org.pkijs.simpl.GENERAL_NAME({ schema: crl_names[i] })); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.DistributionPoint.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + if("distributionPoint" in this) + { + var internalValue; + + if(this.distributionPoint instanceof Array) + { + var namesArray = new Array(); + + for(var i = 0; i < this.distributionPoint.length; i++) + namesArray.push(this.distributionPoint[i].toSchema()); + + internalValue = new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: namesArray + }); + } + else + { + internalValue = new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value: [this.distributionPoint.toSchema()] + }); + } + + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [internalValue] + })); + } + + if("reasons" in this) + { + output_array.push(new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value_hex: this.reasons.value_block.value_hex + })); + } + + if("cRLIssuer" in this) + { + var value = new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 2 // [2] + } + }); + + for(var i = 0; i < this.cRLIssuer.length; i++) + value.value_block.value.push(this.cRLIssuer[i].toSchema()); + + output_array.push(value); + } + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.DistributionPoint.prototype.toJSON = + function() + { + var _object = {}; + + if("distributionPoint" in this) + { + if(this.distributionPoint instanceof Array) + { + _object.distributionPoint = new Array(); + + for(var i = 0; i < this.distributionPoint.length; i++) + _object.distributionPoint.push(this.distributionPoint[i].toJSON()); + } + else + _object.distributionPoint = this.distributionPoint.toJSON(); + } + + if("reasons" in this) + _object.reasons = this.reasons.toJSON(); + + if("cRLIssuer" in this) + { + _object.cRLIssuer = new Array(); + + for(var i = 0; i < this.cRLIssuer.length; i++) + _object.cRLIssuer.push(this.cRLIssuer[i].toJSON()); + } + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "CRLDistributionPoints" type of extension + //************************************************************************************** + in_window.org.pkijs.simpl.x509.CRLDistributionPoints = + function() + { + // #region Internal properties of the object + this.distributionPoints = new Array(); // Array of "simpl.x509.DistributionPoint" + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.CRLDistributionPoints.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.distributionPoints = arguments[0].distributionPoints || new Array(); // Array of "simpl.x509.DistributionPoint" + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.CRLDistributionPoints.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.CRLDistributionPoints({ + names: { + distributionPoints: "distributionPoints" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for CRLDistributionPoints"); + // #endregion + + // #region Get internal properties from parsed schema + var points = asn1.result["distributionPoints"]; + + for(var i = 0; i < points.length; i++) + this.distributionPoints.push(new in_window.org.pkijs.simpl.x509.DistributionPoint({ schema: points[i] })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.CRLDistributionPoints.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + for(var i = 0; i < this.distributionPoints.length; i++) + output_array.push(this.distributionPoints[i].toSchema()); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.CRLDistributionPoints.prototype.toJSON = + function() + { + var _object = { + distributionPoints: new Array() + }; + + for(var i = 0; i < this.distributionPoints.length; i++) + _object.distributionPoints.push(this.distributionPoints[i].toJSON()); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "AccessDescription" type + //************************************************************************************** + in_window.org.pkijs.simpl.x509.AccessDescription = + function() + { + // #region Internal properties of the object + this.accessMethod = ""; + this.accessLocation = new in_window.org.pkijs.simpl.GENERAL_NAME(); + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.AccessDescription.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.accessMethod = arguments[0].accessMethod || ""; + this.accessLocation = arguments[0].accessLocation || new in_window.org.pkijs.simpl.GENERAL_NAME(); + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.AccessDescription.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.AccessDescription({ + names: { + accessMethod: "accessMethod", + accessLocation: { + names: { + block_name: "accessLocation" + } + } + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for AccessDescription"); + // #endregion + + // #region Get internal properties from parsed schema + this.accessMethod = asn1.result["accessMethod"].value_block.toString(); + this.accessLocation = new in_window.org.pkijs.simpl.GENERAL_NAME({ schema: asn1.result["accessLocation"] }); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.AccessDescription.prototype.toSchema = + function() + { + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + new in_window.org.pkijs.asn1.OID({ value: this.accessMethod }), + this.accessLocation.toSchema() + ] + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.AccessDescription.prototype.toJSON = + function() + { + return { + accessMethod: this.accessMethod, + accessLocation: this.accessLocation.toJSON() + }; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "AuthorityInfoAccess" and "SubjectInfoAccess" types of extension + //************************************************************************************** + in_window.org.pkijs.simpl.x509.InfoAccess = + function() + { + // #region Internal properties of the object + this.accessDescriptions = new Array(); // Array of "simpl.x509.AccessDescription" + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.InfoAccess.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.accessDescriptions = arguments[0].accessDescriptions || new Array(); // Array of "simpl.x509.DistributionPoint" + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.InfoAccess.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.InfoAccess({ + names: { + accessDescriptions: "accessDescriptions" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for InfoAccess"); + // #endregion + + // #region Get internal properties from parsed schema + var descriptions = asn1.result["accessDescriptions"]; + + for(var i = 0; i < descriptions.length; i++) + this.accessDescriptions.push(new in_window.org.pkijs.simpl.x509.AccessDescription({ schema: descriptions[i] })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.InfoAccess.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + for(var i = 0; i < this.accessDescriptions.length; i++) + output_array.push(this.accessDescriptions[i].toSchema()); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.InfoAccess.prototype.toJSON = + function() + { + var _object = { + accessDescriptions: new Array() + }; + + for(var i = 0; i < this.accessDescriptions.length; i++) + _object.accessDescriptions.push(this.accessDescriptions[i].toJSON()); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "IssuingDistributionPoint" type of extension + //************************************************************************************** + in_window.org.pkijs.simpl.x509.IssuingDistributionPoint = + function() + { + // #region Internal properties of the object + // OPTIONAL this.distributionPoint // Array of "simpl.GENERAL_NAME" or a value of "simpl.RDN" type + // OPTIONAL this.onlyContainsUserCerts // BOOLEAN flag + // OPTIONAL this.onlyContainsCACerts // BOOLEAN flag + // OPTIONAL this.onlySomeReasons // BITSTRING + // OPTIONAL this.indirectCRL // BOOLEAN flag + // OPTIONAL this.onlyContainsAttributeCerts // BOOLEAN flag + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.x509.IssuingDistributionPoint.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + if("distributionPoint" in arguments[0]) + this.distributionPoint = arguments[0].distributionPoint; + + if("onlyContainsUserCerts" in arguments[0]) + this.onlyContainsUserCerts = arguments[0].onlyContainsUserCerts; + + if("onlyContainsCACerts" in arguments[0]) + this.onlyContainsCACerts = arguments[0].onlyContainsCACerts; + + if("onlySomeReasons" in arguments[0]) + this.onlySomeReasons = arguments[0].onlySomeReasons; + + if("indirectCRL" in arguments[0]) + this.indirectCRL = arguments[0].indirectCRL; + + if("onlyContainsAttributeCerts" in arguments[0]) + this.onlyContainsAttributeCerts = arguments[0].onlyContainsAttributeCerts; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.IssuingDistributionPoint.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.x509.IssuingDistributionPoint({ + names: { + distributionPoint: "distributionPoint", + distributionPoint_names: "distributionPoint_names", + onlyContainsUserCerts: "onlyContainsUserCerts", + onlyContainsCACerts: "onlyContainsCACerts", + onlySomeReasons: "onlySomeReasons", + indirectCRL: "indirectCRL", + onlyContainsAttributeCerts: "onlyContainsAttributeCerts" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for IssuingDistributionPoint"); + // #endregion + + // #region Get internal properties from parsed schema + if("distributionPoint" in asn1.result) + { + if(asn1.result["distributionPoint"].id_block.tag_number == 0) // GENERAL_NAMES variant + { + this.distributionPoint = new Array(); + var names = asn1.result["distributionPoint_names"]; + + for(var i = 0; i < names.length; i++) + this.distributionPoint.push(new in_window.org.pkijs.simpl.GENERAL_NAME({ schema: names[i] })); + } + + if(asn1.result["distributionPoint"].id_block.tag_number == 1) // RDN variant + { + asn1.result["distributionPoint"].id_block.tag_class = 1; // UNIVERSAL + asn1.result["distributionPoint"].id_block.tag_number = 16; // SEQUENCE + + this.distributionPoint = new in_window.org.pkijs.simpl.RDN({ schema: asn1.result["distributionPoint"] }); + } + } + + if("onlyContainsUserCerts" in asn1.result) + { + var view = new Uint8Array(asn1.result["onlyContainsUserCerts"].value_block.value_hex); + this.onlyContainsUserCerts = (view[0] !== 0x00); + } + + if("onlyContainsCACerts" in asn1.result) + { + var view = new Uint8Array(asn1.result["onlyContainsCACerts"].value_block.value_hex); + this.onlyContainsCACerts = (view[0] !== 0x00); + } + + if("onlySomeReasons" in asn1.result) + { + var view = new Uint8Array(asn1.result["onlySomeReasons"].value_block.value_hex); + this.onlySomeReasons = view[0]; + } + + if("indirectCRL" in asn1.result) + { + var view = new Uint8Array(asn1.result["indirectCRL"].value_block.value_hex); + this.indirectCRL = (view[0] !== 0x00); + } + + if("onlyContainsAttributeCerts" in asn1.result) + { + var view = new Uint8Array(asn1.result["onlyContainsAttributeCerts"].value_block.value_hex); + this.onlyContainsAttributeCerts = (view[0] !== 0x00); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.IssuingDistributionPoint.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + if("distributionPoint" in this) + { + var value; + + if(this.distributionPoint instanceof Array) + { + value = new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + } + }); + + for(var i = 0; i < this.distributionPoint.length; i++) + value.value_block.value.push(this.distributionPoint[i].toSchema()); + } + else + { + value = this.distributionPoint.toSchema(); + + value.id_block.tag_class = 3; // CONTEXT - SPECIFIC + value.id_block.tag_number = 1; // [1] + } + + output_array.push(value); + } + + if("onlyContainsUserCerts" in this) + { + var buffer = new ArrayBuffer(1); + var view = new Uint8Array(buffer); + + view[0] = (this.onlyContainsUserCerts === false) ? 0x00 : 0xFF; + + output_array.push(new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value_hex: buffer + })); + } + + if("onlyContainsCACerts" in this) + { + var buffer = new ArrayBuffer(1); + var view = new Uint8Array(buffer); + + view[0] = (this.onlyContainsCACerts === false) ? 0x00 : 0xFF; + + output_array.push(new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 2 // [2] + }, + value_hex: buffer + })); + } + + if("onlySomeReasons" in this) + { + var buffer = new ArrayBuffer(1); + var view = new Uint8Array(buffer); + + view[0] = this.onlySomeReasons; + + output_array.push(new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 3 // [3] + }, + value_hex: buffer + })); + } + + if("indirectCRL" in this) + { + var buffer = new ArrayBuffer(1); + var view = new Uint8Array(buffer); + + view[0] = (this.indirectCRL === false) ? 0x00 : 0xFF; + + output_array.push(new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 4 // [4] + }, + value_hex: buffer + })); + } + + if("onlyContainsAttributeCerts" in this) + { + var buffer = new ArrayBuffer(1); + var view = new Uint8Array(buffer); + + view[0] = (this.onlyContainsAttributeCerts === false) ? 0x00 : 0xFF; + + output_array.push(new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 5 // [5] + }, + value_hex: buffer + })); + } + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.x509.IssuingDistributionPoint.prototype.toJSON = + function() + { + var _object = {}; + + if("distributionPoint" in this) + { + if(this.distributionPoint instanceof Array) + { + _object.distributionPoint = new Array(); + + for(var i = 0; i < this.distributionPoint.length; i++) + _object.distributionPoint.push(this.distributionPoint[i].toJSON()); + } + else + _object.distributionPoint = this.distributionPoint.toJSON(); + } + + if("onlyContainsUserCerts" in this) + _object.onlyContainsUserCerts = this.onlyContainsUserCerts; + + if("onlyContainsCACerts" in this) + _object.onlyContainsCACerts = this.onlyContainsCACerts; + + if("onlySomeReasons" in this) + _object.onlySomeReasons = this.onlySomeReasons.toJSON(); + + if("indirectCRL" in this) + _object.indirectCRL = this.indirectCRL; + + if("onlyContainsAttributeCerts" in this) + _object.onlyContainsAttributeCerts = this.onlyContainsAttributeCerts; + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "Extension" type + //************************************************************************************** + in_window.org.pkijs.simpl.EXTENSION = + function() + { + // #region Internal properties of the object + this.extnID = ""; + this.critical = false; + this.extnValue = new in_window.org.pkijs.asn1.OCTETSTRING(); + + // OPTIONAL this.parsedValue - Parsed "extnValue" in case of well-known "extnID" + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.EXTENSION.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.extnID = (arguments[0].extnID || ""); + this.critical = (arguments[0].critical || false); + if("extnValue" in arguments[0]) + this.extnValue = new in_window.org.pkijs.asn1.OCTETSTRING({ value_hex: arguments[0].extnValue }); + else + this.extnValue = new in_window.org.pkijs.asn1.OCTETSTRING(); + + if("parsedValue" in arguments[0]) + this.parsedValue = arguments[0].parsedValue; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.EXTENSION.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.EXTENSION({ + names: { + extnID: "extnID", + critical: "critical", + extnValue: "extnValue" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for EXTENSION"); + // #endregion + + // #region Get internal properties from parsed schema + this.extnID = asn1.result.extnID.value_block.toString(); + if("critical" in asn1.result) + this.critical = asn1.result.critical.value_block.value; + this.extnValue = asn1.result.extnValue; + + // #region Get "parsedValue" for well-known extensions + var asn1 = in_window.org.pkijs.fromBER(this.extnValue.value_block.value_hex); + if(asn1.offset === (-1)) + return; + + switch(this.extnID) + { + case "2.5.29.9": // SubjectDirectoryAttributes + this.parsedValue = new in_window.org.pkijs.simpl.x509.SubjectDirectoryAttributes({ schema: asn1.result }); + break; + case "2.5.29.14": // SubjectKeyIdentifier + this.parsedValue = asn1.result; // Should be just a simple OCTETSTRING + break; + case "2.5.29.15": // KeyUsage + this.parsedValue = asn1.result; // Should be just a simple BITSTRING + break; + case "2.5.29.16": // PrivateKeyUsagePeriod + this.parsedValue = new in_window.org.pkijs.simpl.x509.PrivateKeyUsagePeriod({ schema: asn1.result }); + break; + case "2.5.29.17": // SubjectAltName + case "2.5.29.18": // IssuerAltName + this.parsedValue = new in_window.org.pkijs.simpl.x509.AltName({ schema: asn1.result }); + break; + case "2.5.29.19": // BasicConstraints + this.parsedValue = new in_window.org.pkijs.simpl.x509.BasicConstraints({ schema: asn1.result }); + break; + case "2.5.29.20": // CRLNumber + case "2.5.29.27": // BaseCRLNumber (delta CRL indicator) + this.parsedValue = asn1.result; // Should be just a simple INTEGER + break; + case "2.5.29.21": // CRLReason + this.parsedValue = asn1.result; // Should be just a simple ENUMERATED + break; + case "2.5.29.24": // InvalidityDate + this.parsedValue = asn1.result; // Should be just a simple GeneralizedTime + break; + case "2.5.29.28": // IssuingDistributionPoint + this.parsedValue = new in_window.org.pkijs.simpl.x509.IssuingDistributionPoint({ schema: asn1.result }); + break; + case "2.5.29.29": // CertificateIssuer + this.parsedValue = new in_window.org.pkijs.simpl.GENERAL_NAMES({ schema: asn1.result }); // Should be just a simple + break; + case "2.5.29.30": // NameConstraints + this.parsedValue = new in_window.org.pkijs.simpl.x509.NameConstraints({ schema: asn1.result }); + break; + case "2.5.29.31": // CRLDistributionPoints + case "2.5.29.46": // FreshestCRL + this.parsedValue = new in_window.org.pkijs.simpl.x509.CRLDistributionPoints({ schema: asn1.result }); + break; + case "2.5.29.32": // CertificatePolicies + this.parsedValue = new in_window.org.pkijs.simpl.x509.CertificatePolicies({ schema: asn1.result }); + break; + case "2.5.29.33": // PolicyMappings + this.parsedValue = new in_window.org.pkijs.simpl.x509.PolicyMappings({ schema: asn1.result }); + break; + case "2.5.29.35": // AuthorityKeyIdentifier + this.parsedValue = new in_window.org.pkijs.simpl.x509.AuthorityKeyIdentifier({ schema: asn1.result }); + break; + case "2.5.29.36": // PolicyConstraints + this.parsedValue = new in_window.org.pkijs.simpl.x509.PolicyConstraints({ schema: asn1.result }); + break; + case "2.5.29.37": // ExtKeyUsage + this.parsedValue = new in_window.org.pkijs.simpl.x509.ExtKeyUsage({ schema: asn1.result }); + break; + case "2.5.29.54": // InhibitAnyPolicy + this.parsedValue = asn1.result; // Should be just a simple INTEGER + break; + case "1.3.6.1.5.5.7.1.1": // AuthorityInfoAccess + case "1.3.6.1.5.5.7.1.11": // SubjectInfoAccess + this.parsedValue = new in_window.org.pkijs.simpl.x509.InfoAccess({ schema: asn1.result }); + break; + default: + } + // #endregion + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.EXTENSION.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + output_array.push(new in_window.org.pkijs.asn1.OID({ value: this.extnID })); + + if(this.critical) + output_array.push(new in_window.org.pkijs.asn1.BOOLEAN({ value: this.critical })); + + output_array.push(this.extnValue); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.EXTENSION.prototype.toJSON = + function() + { + var _object = { + extnID: this.extnID, + critical: this.critical, + extnValue: this.extnValue.toJSON() + }; + + if("parsedValue" in this) + _object.parsedValue = this.parsedValue.toJSON(); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "Extensions" type (sequence of many Extension) + //************************************************************************************** + in_window.org.pkijs.simpl.EXTENSIONS = + function() + { + // #region Internal properties of the object + this.extensions_array = new Array(); + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.EXTENSIONS.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + this.extensions_array = (arguments[0].extensions_array || (new Array())); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.EXTENSIONS.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.EXTENSIONS({ + names: { + extensions: "extensions" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for EXTENSIONS"); + // #endregion + + // #region Get internal properties from parsed schema + for(var i = 0; i < asn1.result.extensions.length; i++) + this.extensions_array.push(new in_window.org.pkijs.simpl.EXTENSION({ schema: asn1.result.extensions[i] })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.EXTENSIONS.prototype.toSchema = + function() + { + // #region Construct and return new ASN.1 schema for this object + var extension_schemas = new Array(); + + for(var i = 0; i < this.extensions_array.length; i++) + extension_schemas.push(this.extensions_array[i].toSchema()); + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: extension_schemas + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.EXTENSIONS.prototype.toJSON = + function() + { + var _object = { + extensions_array: new Array() + }; + + for(var i = 0; i < this.extensions_array.length; i++) + _object.extensions_array.push(this.extensions_array[i].toJSON()); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for X.509 v3 certificate (RFC5280) + //************************************************************************************** + in_window.org.pkijs.simpl.CERT = + function() + { + // #region Internal properties of the object + // #region Properties from certificate TBS part + this.tbs = new ArrayBuffer(0); // Encoded value of certificate TBS (need to have it for certificate validation) + + // OPTIONAL this.version = 0; + this.serialNumber = new in_window.org.pkijs.asn1.INTEGER(); // Might be a very long integer value + this.signature = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); // Signature algorithm from certificate TBS part + this.issuer = new in_window.org.pkijs.simpl.RDN(); + this.notBefore = new in_window.org.pkijs.simpl.TIME(); + this.notAfter = new in_window.org.pkijs.simpl.TIME(); + this.subject = new in_window.org.pkijs.simpl.RDN(); + this.subjectPublicKeyInfo = new in_window.org.pkijs.simpl.PUBLIC_KEY_INFO(); + // OPTIONAL this.issuerUniqueID = new ArrayBuffer(0); // IMPLICIT bistring value + // OPTIONAL this.subjectUniqueID = new ArrayBuffer(0); // IMPLICIT bistring value + // OPTIONAL this.extensions = new Array(); // Array of "simpl.EXTENSION" + // #endregion + + // #region Properties from certificate major part + this.signatureAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); // Signature algorithm from certificate major part + this.signatureValue = new in_window.org.pkijs.asn1.BITSTRING(); + // #endregion + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.CERT.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + // #region Properties from certificate TBS part + this.tbs = arguments[0].tbs || new ArrayBuffer(0); + + if("version" in arguments[0]) + this.version = arguments[0].version; + this.serialNumber = arguments[0].serialNumber || new in_window.org.pkijs.asn1.INTEGER(); // Might be a very long integer value + this.signature = arguments[0].signature || new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); // Signature algorithm from certificate TBS part + this.issuer = arguments[0].issuer || new in_window.org.pkijs.simpl.RDN(); + this.notBefore = arguments[0].not_before || new in_window.org.pkijs.simpl.TIME(); + this.notAfter = arguments[0].not_after || new in_window.org.pkijs.simpl.TIME(); + this.subject = arguments[0].subject || new in_window.org.pkijs.simpl.RDN(); + this.subjectPublicKeyInfo = arguments[0].subjectPublicKeyInfo || new in_window.org.pkijs.simpl.PUBLIC_KEY_INFO(); + if("issuerUniqueID" in arguments[0]) + this.issuerUniqueID = arguments[0].issuerUniqueID; + if("subjectUniqueID" in arguments[0]) + this.subjectUniqueID = arguments[0].subjectUniqueID; + if("extensions" in arguments[0]) + this.extensions = arguments[0].extensions; + // #endregion + + // #region Properties from certificate major part + this.signatureAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); // Signature algorithm from certificate major part + this.signatureValue = new in_window.org.pkijs.asn1.BITSTRING(); + // #endregion + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CERT.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.CERT({ + names: { + tbsCertificate: { + names: { + extensions: { + names: { + extensions: "tbsCertificate.extensions" + } + } + } + } + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for CERT"); + // #endregion + + // #region Get internal properties from parsed schema + this.tbs = asn1.result["tbsCertificate"].value_before_decode; + + if("tbsCertificate.version" in asn1.result) + this.version = asn1.result["tbsCertificate.version"].value_block.value_dec; + this.serialNumber = asn1.result["tbsCertificate.serialNumber"]; + this.signature = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ schema: asn1.result["tbsCertificate.signature"] }); + this.issuer = new in_window.org.pkijs.simpl.RDN({ schema: asn1.result["tbsCertificate.issuer"] }); + this.notBefore = new in_window.org.pkijs.simpl.TIME({ schema: asn1.result["tbsCertificate.notBefore"] }); + this.notAfter = new in_window.org.pkijs.simpl.TIME({ schema: asn1.result["tbsCertificate.notAfter"] }); + this.subject = new in_window.org.pkijs.simpl.RDN({ schema: asn1.result["tbsCertificate.subject"] }); + this.subjectPublicKeyInfo = new in_window.org.pkijs.simpl.PUBLIC_KEY_INFO({ schema: asn1.result["tbsCertificate.subjectPublicKeyInfo"] }); + if("tbsCertificate.issuerUniqueID" in asn1.result) + this.issuerUniqueID = asn1.result["tbsCertificate.issuerUniqueID"].value_block.value_hex; + if("tbsCertificate.subjectUniqueID" in asn1.result) + this.issuerUniqueID = asn1.result["tbsCertificate.subjectUniqueID"].value_block.value_hex; + if("tbsCertificate.extensions" in asn1.result) + { + this.extensions = new Array(); + + var extensions = asn1.result["tbsCertificate.extensions"]; + + for(var i = 0; i < extensions.length; i++) + this.extensions.push(new in_window.org.pkijs.simpl.EXTENSION({ schema: extensions[i] })); + } + + this.signatureAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ schema: asn1.result["signatureAlgorithm"] }); + this.signatureValue = asn1.result["signatureValue"]; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CERT.prototype.encodeTBS = + function() + { + /// <summary>Create ASN.1 schema for existing values of TBS part for the certificate</summary> + + // #region Create array for output sequence + var output_array = new Array(); + + if("version" in this) + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [ + new in_window.org.pkijs.asn1.INTEGER({ value: this.version }) // EXPLICIT integer value + ] + })); + + output_array.push(this.serialNumber); + output_array.push(this.signature.toSchema()); + output_array.push(this.issuer.toSchema()); + + output_array.push(new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + this.notBefore.toSchema(), + this.notAfter.toSchema() + ] + })); + + output_array.push(this.subject.toSchema()); + output_array.push(this.subjectPublicKeyInfo.toSchema()); + + if("issuerUniqueID" in this) + output_array.push(new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 1 // [1] + }, + value_hex: this.issuerUniqueID + })); + if("subjectUniqueID" in this) + output_array.push(new in_window.org.pkijs.asn1.ASN1_PRIMITIVE({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 2 // [2] + }, + value_hex: this.subjectUniqueID + })); + + if("subjectUniqueID" in this) + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 3 // [3] + }, + value: [this.extensions.toSchema()] + })); + + if("extensions" in this) + { + var extensions = new Array(); + + for(var i = 0; i < this.extensions.length; i++) + extensions.push(this.extensions[i].toSchema()); + + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 3 // [3] + }, + value: [new in_window.org.pkijs.asn1.SEQUENCE({ + value: extensions + })] + })); + } + // #endregion + + // #region Create and return output sequence + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CERT.prototype.toSchema = + function(encodeFlag) + { + /// <param name="encodeFlag" type="Boolean">If param equal to false then create TBS schema via decoding stored value. In othe case create TBS schema via assembling from TBS parts.</param> + + if(typeof encodeFlag === "undefined") + encodeFlag = false; + + var tbs_schema = {}; + + // #region Decode stored TBS value + if(encodeFlag === false) + { + if(this.tbs.length === 0) // No stored certificate TBS part + return in_window.org.pkijs.schema.CERT().value[0]; + + var tbs_asn1 = in_window.org.pkijs.fromBER(this.tbs); + + tbs_schema = tbs_asn1.result; + } + // #endregion + // #region Create TBS schema via assembling from TBS parts + else + tbs_schema = in_window.org.pkijs.simpl.CERT.prototype.encodeTBS.call(this); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + tbs_schema, + this.signatureAlgorithm.toSchema(), + this.signatureValue + ] + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CERT.prototype.verify = + function() + { + /// <summary>!!! Works well in Chrome dev versions only (April 2014th) !!!</summary> + /// <returns type="Promise">Returns a new Promise object (in case of error), or a result of "crypto.subtle.veryfy" function</returns> + + // #region Global variables + var sequence = Promise.resolve(); + + var subjectPublicKeyInfo = {}; + + var signature = this.signatureValue; + var tbs = this.tbs; + + var _this = this; + // #endregion + + // #region Set correct "subjectPublicKeyInfo" value + if(arguments[0] instanceof Object) + { + if("issuerCertificate" in arguments[0]) // Must be of type "simpl.CERT" + subjectPublicKeyInfo = arguments[0].issuerCertificate.subjectPublicKeyInfo; + } + else + { + if(this.issuer.isEqual(this.subject)) // Self-signed certificate + subjectPublicKeyInfo = this.subjectPublicKeyInfo; + } + + if((subjectPublicKeyInfo instanceof in_window.org.pkijs.simpl.PUBLIC_KEY_INFO) === false) + return new Promise(function(resolve, reject) { reject("Please provide issuer certificate as a parameter"); }); + // #endregion + + // #region Get a "crypto" extension + var crypto = in_window.org.pkijs.getCrypto(); + if(typeof crypto == "undefined") + return new Promise(function(resolve, reject) { reject("Unable to create WebCrypto object"); }); + // #endregion + + // #region Find signer's hashing algorithm + var sha_algorithm = in_window.org.pkijs.getHashAlgorithm(this.signatureAlgorithm); + if(sha_algorithm === "") + return new Promise(function(resolve, reject) { reject("Unsupported signature algorithm: " + _this.signatureAlgorithm.algorithm_id); }); + // #endregion + + // #region Importing public key + sequence = sequence.then( + function() + { + // #region Get information about public key algorithm and default parameters for import + var algorithmObject = in_window.org.pkijs.getAlgorithmByOID(_this.signatureAlgorithm.algorithm_id); + if(("name" in algorithmObject) === false) + return new Promise(function(resolve, reject) { reject("Unsupported public key algorithm: " + _this.signatureAlgorithm.algorithm_id); }); + + var algorithm_name = algorithmObject.name; + + var algorithm = in_window.org.pkijs.getAlgorithmParameters(algorithm_name, "importkey"); + if("hash" in algorithm.algorithm) + algorithm.algorithm.hash.name = sha_algorithm; + // #endregion + + var publicKeyInfo_schema = subjectPublicKeyInfo.toSchema(); + var publicKeyInfo_buffer = publicKeyInfo_schema.toBER(false); + var publicKeyInfo_view = new Uint8Array(publicKeyInfo_buffer); + + return crypto.importKey("spki", publicKeyInfo_view, algorithm.algorithm, true, algorithm.usages); + } + ); + // #endregion + + // #region Verify signature for the certificate + sequence = sequence.then( + function(publicKey) + { + // #region Get default algorithm parameters for verification + var algorithm = in_window.org.pkijs.getAlgorithmParameters(publicKey.algorithm.name, "verify"); + if("hash" in algorithm.algorithm) + algorithm.algorithm.hash.name = sha_algorithm; + // #endregion + + // #region Special case for ECDSA signatures + var signature_value = signature.value_block.value_hex; + + if(publicKey.algorithm.name === "ECDSA") + { + var asn1 = in_window.org.pkijs.fromBER(signature_value); + signature_value = in_window.org.pkijs.createECDSASignatureFromCMS(asn1.result); + } + // #endregion + + // #region Special case for RSA-PSS + if(publicKey.algorithm.name === "RSA-PSS") + { + var pssParameters; + + try + { + pssParameters = new in_window.org.pkijs.simpl.x509.RSASSA_PSS_params({ schema: _this.signatureAlgorithm.algorithm_params }); + } + catch(ex) + { + return new Promise(function(resolve, reject) { reject(ex); }); + } + + if("saltLength" in pssParameters) + algorithm.algorithm.saltLength = pssParameters.saltLength; + else + algorithm.algorithm.saltLength = 20; + + var hash_algo = "SHA-1"; + + if("hashAlgorithm" in pssParameters) + { + var hashAlgorithm = in_window.org.pkijs.getAlgorithmByOID(pssParameters.hashAlgorithm.algorithm_id); + if(("name" in hashAlgorithm) === false) + return new Promise(function(resolve, reject) { reject("Unrecognized hash algorithm: " + pssParameters.hashAlgorithm.algorithm_id); }); + + hash_algo = hashAlgorithm.name; + } + + algorithm.algorithm.hash.name = hash_algo; + } + // #endregion + + return crypto.verify(algorithm.algorithm, + publicKey, + new Uint8Array(signature_value), + new Uint8Array(tbs)); + } + ); + // #endregion + + return sequence; + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CERT.prototype.sign = + function(privateKey, hashAlgorithm) + { + /// <param name="privateKey" type="CryptoKey">Private key for "subjectPublicKeyInfo" structure</param> + /// <param name="hashAlgorithm" type="String" optional="true">Hashing algorithm. Default SHA-1</param> + + // #region Initial variables + var _this = this; + // #endregion + + // #region Get a private key from function parameter + if(typeof privateKey === "undefined") + return new Promise(function(resolve, reject) { reject("Need to provide a private key for signing"); }); + // #endregion + + // #region Get hashing algorithm + if(typeof hashAlgorithm === "undefined") + hashAlgorithm = "SHA-1"; + else + { + // #region Simple check for supported algorithm + var oid = in_window.org.pkijs.getOIDByAlgorithm({ name: hashAlgorithm }); + if(oid === "") + return new Promise(function(resolve, reject) { reject("Unsupported hash algorithm: " + hashAlgorithm); }); + // #endregion + } + // #endregion + + // #region Get a "default parameters" for current algorithm + var defParams = in_window.org.pkijs.getAlgorithmParameters(privateKey.algorithm.name, "sign"); + defParams.algorithm.hash.name = hashAlgorithm; + // #endregion + + // #region Fill internal structures base on "privateKey" and "hashAlgorithm" + switch(privateKey.algorithm.name.toUpperCase()) + { + case "RSASSA-PKCS1-V1_5": + case "ECDSA": + _this.signature.algorithm_id = in_window.org.pkijs.getOIDByAlgorithm(defParams.algorithm); + _this.signatureAlgorithm.algorithm_id = _this.signature.algorithm_id; + break; + case "RSA-PSS": + { + // #region Set "saltLength" as a length (in octets) of hash function result + switch(hashAlgorithm.toUpperCase()) + { + case "SHA-256": + defParams.algorithm.saltLength = 32; + break; + case "SHA-384": + defParams.algorithm.saltLength = 48; + break; + case "SHA-512": + defParams.algorithm.saltLength = 64; + break; + default: + } + // #endregion + + // #region Fill "RSASSA_PSS_params" object + var paramsObject = {}; + + if(hashAlgorithm.toUpperCase() !== "SHA-1") + { + var hashAlgorithmOID = in_window.org.pkijs.getOIDByAlgorithm({ name: hashAlgorithm }); + if(hashAlgorithmOID === "") + return new Promise(function(resolve, reject) { reject("Unsupported hash algorithm: " + hashAlgorithm); }); + + paramsObject.hashAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ + algorithm_id: hashAlgorithmOID, + algorithm_params: new in_window.org.pkijs.asn1.NULL() + }); + + paramsObject.maskGenAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ + algorithm_id: "1.2.840.113549.1.1.8", // MGF1 + algorithm_params: paramsObject.hashAlgorithm.toSchema() + }) + } + + if(defParams.algorithm.saltLength !== 20) + paramsObject.saltLength = defParams.algorithm.saltLength; + + var pssParameters = new in_window.org.pkijs.simpl.x509.RSASSA_PSS_params(paramsObject); + // #endregion + + // #region Automatically set signature algorithm + _this.signature = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ + algorithm_id: "1.2.840.113549.1.1.10", + algorithm_params: pssParameters.toSchema() + }); + _this.signatureAlgorithm = _this.signature; // Must be the same + // #endregion + } + break; + default: + return new Promise(function(resolve, reject) { reject("Unsupported signature algorithm: " + privateKey.algorithm.name); }); + } + // #endregion + + // #region Create TBS data for signing + _this.tbs = in_window.org.pkijs.simpl.CERT.prototype.encodeTBS.call(this).toBER(false); + // #endregion + + // #region Get a "crypto" extension + var crypto = in_window.org.pkijs.getCrypto(); + if(typeof crypto == "undefined") + return new Promise(function(resolve, reject) { reject("Unable to create WebCrypto object"); }); + // #endregion + + // #region Signing TBS data on provided private key + return crypto.sign(defParams.algorithm, + privateKey, + new Uint8Array(_this.tbs)).then( + function(result) + { + // #region Special case for ECDSA algorithm + if(defParams.algorithm.name === "ECDSA") + result = in_window.org.pkijs.createCMSECDSASignature(result); + // #endregion + + _this.signatureValue = new in_window.org.pkijs.asn1.BITSTRING({ value_hex: result }); + }, + function(error) + { + return new Promise(function(resolve, reject) { reject("Signing error: " + error); }); + } + ); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CERT.prototype.getPublicKey = + function() + { + /// <summary>Importing public key for current certificate</summary> + + // #region Initial variables + var algorithm; + // #endregion + + // #region Get a "crypto" extension + var crypto = in_window.org.pkijs.getCrypto(); + if(typeof crypto == "undefined") + return new Promise(function(resolve, reject) { reject("Unable to create WebCrypto object"); }); + // #endregion + + // #region Find correct algorithm for imported public key + if(arguments[0] instanceof Object) + { + if("algorithm" in arguments[0]) + algorithm = arguments[0].algorithm; + else + return new Promise(function(resolve, reject) { reject("Absent mandatory parameter \"algorithm\""); }); + } + else + { + // #region Find signer's hashing algorithm + var sha_algorithm = in_window.org.pkijs.getHashAlgorithm(this.signatureAlgorithm); + if(sha_algorithm === "") + return new Promise(function(resolve, reject) { reject("Unsupported signature algorithm: " + _this.signatureAlgorithm.algorithm_id); }); + // #endregion + + // #region Get information about public key algorithm and default parameters for import + var algorithmObject = in_window.org.pkijs.getAlgorithmByOID(this.signatureAlgorithm.algorithm_id); + if(("name" in algorithmObject) === false) + return new Promise(function(resolve, reject) { reject("Unsupported public key algorithm: " + _this.signatureAlgorithm.algorithm_id); }); + + var algorithm_name = algorithmObject.name; + + algorithm = in_window.org.pkijs.getAlgorithmParameters(algorithm_name, "importkey"); + if("hash" in algorithm.algorithm) + algorithm.algorithm.hash.name = sha_algorithm; + // #endregion + } + // #endregion + + // #region Get neccessary values from internal fields for current certificate + var publicKeyInfo_schema = this.subjectPublicKeyInfo.toSchema(); + var publicKeyInfo_buffer = publicKeyInfo_schema.toBER(false); + var publicKeyInfo_view = new Uint8Array(publicKeyInfo_buffer); + // #endregion + + return crypto.importKey("spki", publicKeyInfo_view, algorithm.algorithm, true, algorithm.usages); + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CERT.prototype.getKeyHash = + function() + { + /// <summary>Get SHA-1 hash value for subject public key</summary> + + // #region Get a "crypto" extension + var crypto = in_window.org.pkijs.getCrypto(); + if(typeof crypto == "undefined") + return new Promise(function(resolve, reject) { reject("Unable to create WebCrypto object"); }); + // #endregion + + return crypto.digest({ name: "sha-1" }, new Uint8Array(this.subjectPublicKeyInfo.subjectPublicKey.value_block.value_hex)); + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CERT.prototype.toJSON = + function() + { + var _object = { + tbs: in_window.org.pkijs.bufferToHexCodes(this.tbs, 0, this.tbs.byteLength), + serialNumber: this.serialNumber.toJSON(), + signature: this.signature.toJSON(), + issuer: this.issuer.toJSON(), + notBefore: this.notBefore.toJSON(), + notAfter: this.notAfter.toJSON(), + subject: this.subject.toJSON(), + subjectPublicKeyInfo: this.subjectPublicKeyInfo.toJSON(), + signatureAlgorithm: this.signatureAlgorithm.toJSON(), + signatureValue: this.signatureValue.toJSON() + }; + + if("version" in this) + _object.version = this.version; + + if("issuerUniqueID" in this) + _object.issuerUniqueID = in_window.org.pkijs.bufferToHexCodes(this.issuerUniqueID, 0, this.issuerUniqueID.byteLength); + + if("subjectUniqueID" in this) + _object.subjectUniqueID = in_window.org.pkijs.bufferToHexCodes(this.subjectUniqueID, 0, this.subjectUniqueID.byteLength); + + if("extensions" in this) + { + _object.extensions = new Array(); + + for(var i = 0; i < this.extensions.length; i++) + _object.extensions.push(this.extensions[i].toJSON()); + } + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "revoked certificate" type (to use in CRL) + //************************************************************************************** + in_window.org.pkijs.simpl.REV_CERT = + function() + { + // #region Internal properties of the object + this.userCertificate = new in_window.org.pkijs.asn1.INTEGER(); + this.revocationDate = new in_window.org.pkijs.simpl.TIME(); + // OPTIONAL this.crlEntryExtensions = new Array(); // Array of "in_window.org.pkijs.simpl.EXTENSION"); + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.REV_CERT.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.userCertificate = arguments[0].userCertificate || new in_window.org.pkijs.asn1.INTEGER(); + this.revocationDate = arguments[0].revocationDate || new in_window.org.pkijs.simpl.TIME(); + if("crlEntryExtensions" in arguments[0]) + this.crlEntryExtensions = arguments[0].crlEntryExtensions; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.REV_CERT.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + new in_window.org.pkijs.asn1.INTEGER({ name: "userCertificate" }), + in_window.org.pkijs.schema.TIME({ + names: { + utcTimeName: "revocationDate", + generalTimeName: "revocationDate" + } + }), + in_window.org.pkijs.schema.EXTENSIONS({ + names: { + block_name: "crlEntryExtensions" + } + }, true) + ] + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for REV_CERT"); + // #endregion + + // #region Get internal properties from parsed schema + this.userCertificate = asn1.result["userCertificate"]; + this.revocationDate = new in_window.org.pkijs.simpl.TIME({ schema: asn1.result["revocationDate"] }); + + if("crlEntryExtensions" in asn1.result) + { + this.crlEntryExtensions = new Array(); + var exts = asn1.result["crlEntryExtensions"].value_block.value; + + for(var i = 0; i < exts.length; i++) + this.crlEntryExtensions.push(new in_window.org.pkijs.simpl.EXTENSION({ schema: exts[i] })); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.REV_CERT.prototype.toSchema = + function() + { + // #region Create array for output sequence + var sequence_array = new Array(); + sequence_array.push(this.userCertificate); + sequence_array.push(this.revocationDate.toSchema()); + + if("crlEntryExtensions" in this) + { + var exts = new Array(); + + for(var i = 0; i < this.crlEntryExtensions.length; i++) + exts.push(this.crlEntryExtensions[i].toSchema()); + + sequence_array.push(new in_window.org.pkijs.asn1.SEQUENCE({ value: exts })); + } + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: sequence_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.REV_CERT.prototype.toJSON = + function() + { + var _object = { + userCertificate: this.userCertificate.toJSON(), + revocationDate: this.revocationDate.toJSON + }; + + if("crlEntryExtensions" in this) + { + _object.crlEntryExtensions = new Array(); + + for(var i = 0; i < this.crlEntryExtensions.length; i++) + _object.crlEntryExtensions.push(this.crlEntryExtensions[i].toJSON()); + } + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for X.509 CRL (Certificate Revocation List)(RFC5280) + //************************************************************************************** + in_window.org.pkijs.simpl.CRL = + function() + { + // #region Internal properties of the object + // #region Properties from CRL TBS part + this.tbs = new ArrayBuffer(0); + + // OPTIONAL this.version = 1; + this.signature = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); + this.issuer = new in_window.org.pkijs.simpl.RDN(); + this.thisUpdate = new in_window.org.pkijs.simpl.TIME(); + // OPTIONAL this.nextUpdate = new in_window.org.pkijs.simpl.TIME(); + // OPTIONAL this.revokedCertificates = new Array(); // Array of REV_CERT objects + // OPTIONAL this.crlExtensions = new Array(); // Array of in_window.org.pkijs.simpl.EXTENSION(); + // #endregion + + // #region Properties from CRL major part + this.signatureAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); + this.signatureValue = new in_window.org.pkijs.asn1.BITSTRING(); + // #endregion + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.CRL.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + // #region Properties from CRL TBS part + this.tbs = arguments[0].tbs || new ArrayBuffer(0); + + if("version" in arguments[0]) + this.version = arguments[0].version; + this.signature = arguments[0].signature || new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); + this.issuer = arguments[0].issuer || new in_window.org.pkijs.simpl.RDN(); + this.thisUpdate = arguments[0].thisUpdate || new in_window.org.pkijs.simpl.TIME(); + if("nextUpdate" in arguments[0]) + this.nextUpdate = arguments[0].nextUpdate; + if("revokedCertificates" in arguments[0]) + this.revokedCertificates = arguments[0].revokedCertificates; + if("crlExtensions" in arguments[0]) + this.crlExtensions = arguments[0].crlExtensions; + // #endregion + + // #region Properties from CRL major part + this.signatureAlgorithm = arguments[0].signatureAlgorithm || new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); + this.signatureValue = arguments[0].signatureValue || new in_window.org.pkijs.asn1.BITSTRING(); + // #endregion + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CRL.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.CRL() + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for CRL"); + // #endregion + + // #region Get internal properties from parsed schema + this.tbs = asn1.result["tbsCertList"].value_before_decode; + + if("tbsCertList.version" in asn1.result) + this.version = asn1.result["tbsCertList.version"].value_block.value_dec; + this.signature = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ schema: asn1.result["tbsCertList.signature"] }); + this.issuer = new in_window.org.pkijs.simpl.RDN({ schema: asn1.result["tbsCertList.issuer"] }); + this.thisUpdate = new in_window.org.pkijs.simpl.TIME({ schema: asn1.result["tbsCertList.thisUpdate"] }); + if("tbsCertList.nextUpdate" in asn1.result) + this.nextUpdate = new in_window.org.pkijs.simpl.TIME({ schema: asn1.result["tbsCertList.nextUpdate"] }); + if("tbsCertList.revokedCertificates" in asn1.result) + { + this.revokedCertificates = new Array(); + + var rev_certs = asn1.result["tbsCertList.revokedCertificates"]; + for(var i = 0; i < rev_certs.length; i++) + this.revokedCertificates.push(new in_window.org.pkijs.simpl.REV_CERT({ schema: rev_certs[i] })); + } + if("tbsCertList.extensions" in asn1.result) + { + this.crlExtensions = new Array(); + var exts = asn1.result["tbsCertList.extensions"].value_block.value; + + for(var i = 0; i < exts.length; i++) + this.crlExtensions.push(new in_window.org.pkijs.simpl.EXTENSION({ schema: exts[i] })); + } + + this.signatureAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ schema: asn1.result["signatureAlgorithm"] }); + this.signatureValue = asn1.result["signatureValue"]; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CRL.prototype.encodeTBS = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + if("version" in this) + output_array.push(new in_window.org.pkijs.asn1.INTEGER({ value: this.version })); + + output_array.push(this.signature.toSchema()); + output_array.push(this.issuer.toSchema()); + output_array.push(this.thisUpdate.toSchema()); + + if("nextUpdate" in this) + output_array.push(this.nextUpdate.toSchema()); + + if("revokedCertificates" in this) + { + var rev_certificates = new Array(); + + for(var i = 0; i < this.revokedCertificates.length; i++) + rev_certificates.push(this.revokedCertificates[i].toSchema()); + + output_array.push(new in_window.org.pkijs.asn1.SEQUENCE({ + value: rev_certificates + })); + } + + if("crlExtensions" in this) + { + var extensions = new Array(); + + for(var j = 0; j < this.crlExtensions.length; j++) + extensions.push(this.crlExtensions[j].toSchema()); + + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: [ + new in_window.org.pkijs.asn1.SEQUENCE({ + value: extensions + }) + ] + })); + } + // #endregion + + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CRL.prototype.toSchema = + function(encodeFlag) + { + /// <param name="encodeFlag" type="Boolean">If param equal to false then create TBS schema via decoding stored value. In othe case create TBS schema via assembling from TBS parts.</param> + + // #region Check "encodeFlag" + if(typeof encodeFlag === "undefined") + encodeFlag = false; + // #endregion + + // #region Decode stored TBS value + var tbs_schema; + + if(encodeFlag === false) + { + if(this.tbs.length === 0) // No stored TBS part + return in_window.org.pkijs.schema.CRL(); + + tbs_schema = in_window.org.pkijs.fromBER(this.tbs).result; + } + // #endregion + // #region Create TBS schema via assembling from TBS parts + else + tbs_schema = in_window.org.pkijs.simpl.CRL.prototype.encodeTBS.call(this); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + tbs_schema, + this.signatureAlgorithm.toSchema(), + this.signatureValue + ] + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CRL.prototype.verify = + function() + { + // #region Global variables + var sequence = Promise.resolve(); + + var signature = this.signatureValue; + var tbs = this.tbs; + + var subjectPublicKeyInfo = -1; + + var _this = this; + // #endregion + + // #region Get information about CRL issuer certificate + if(arguments[0] instanceof Object) + { + if("issuerCertificate" in arguments[0]) // "issuerCertificate" must be of type "simpl.CERT" + { + subjectPublicKeyInfo = arguments[0].issuerCertificate.subjectPublicKeyInfo; + + // The CRL issuer name and "issuerCertificate" subject name are not equal + if(this.issuer.isEqual(arguments[0].issuerCertificate.subject) == false) + return new Promise(function(resolve, reject) { resolve(false); }); + } + + // #region In case if there is only public key during verification + if("publicKeyInfo" in arguments[0]) + subjectPublicKeyInfo = arguments[0].publicKeyInfo; // Must be of type "org.pkijs.simpl.PUBLIC_KEY_INFO" + // #endregion + } + + if((subjectPublicKeyInfo instanceof in_window.org.pkijs.simpl.PUBLIC_KEY_INFO) === false) + return new Promise(function(resolve, reject) { reject("Issuer's certificate must be provided as an input parameter"); }); + // #endregion + + // #region Check the CRL for unknown critical extensions + if("crlExtensions" in this) + { + for(var i = 0; i < this.crlExtensions.length; i++) + { + if(this.crlExtensions[i].critical) + { + // We can not be sure that unknown extension has no value for CRL signature + if(("parsedValue" in this.crlExtensions[i]) == false) + return new Promise(function(resolve, reject) { resolve(false); }); + } + } + } + // #endregion + + // #region Get a "crypto" extension + var crypto = in_window.org.pkijs.getCrypto(); + if(typeof crypto == "undefined") + return new Promise(function(resolve, reject) { reject("Unable to create WebCrypto object"); }); + // #endregion + + // #region Find signer's hashing algorithm + var sha_algorithm = in_window.org.pkijs.getHashAlgorithm(this.signatureAlgorithm); + if(sha_algorithm === "") + return new Promise(function(resolve, reject) { reject("Unsupported signature algorithm: " + _this.signatureAlgorithm.algorithm_id); }); + // #endregion + + // #region Import public key + sequence = sequence.then( + function() + { + // #region Get information about public key algorithm and default parameters for import + var algorithmObject = in_window.org.pkijs.getAlgorithmByOID(_this.signature.algorithm_id); + if(("name" in algorithmObject) === "") + return new Promise(function(resolve, reject) { reject("Unsupported public key algorithm: " + _this.signature.algorithm_id); }); + + var algorithm_name = algorithmObject.name; + + var algorithm = in_window.org.pkijs.getAlgorithmParameters(algorithm_name, "importkey"); + if("hash" in algorithm.algorithm) + algorithm.algorithm.hash.name = sha_algorithm; + // #endregion + + var publicKeyInfo_schema = subjectPublicKeyInfo.toSchema(); + var publicKeyInfo_buffer = publicKeyInfo_schema.toBER(false); + var publicKeyInfo_view = new Uint8Array(publicKeyInfo_buffer); + + return crypto.importKey("spki", + publicKeyInfo_view, + algorithm.algorithm, + true, + algorithm.usages); + } + ); + // #endregion + + // #region Verify signature for the certificate + sequence = sequence.then( + function(publicKey) + { + // #region Get default algorithm parameters for verification + var algorithm = in_window.org.pkijs.getAlgorithmParameters(publicKey.algorithm.name, "verify"); + if("hash" in algorithm.algorithm) + algorithm.algorithm.hash.name = sha_algorithm; + // #endregion + + // #region Special case for ECDSA signatures + var signature_value = signature.value_block.value_hex; + + if(publicKey.algorithm.name === "ECDSA") + { + var asn1 = in_window.org.pkijs.fromBER(signature_value); + signature_value = in_window.org.pkijs.createECDSASignatureFromCMS(asn1.result); + } + // #endregion + + // #region Special case for RSA-PSS + if(publicKey.algorithm.name === "RSA-PSS") + { + var pssParameters; + + try + { + pssParameters = new in_window.org.pkijs.simpl.x509.RSASSA_PSS_params({ schema: _this.signatureAlgorithm.algorithm_params }); + } + catch(ex) + { + return new Promise(function(resolve, reject) { reject(ex); }); + } + + if("saltLength" in pssParameters) + algorithm.algorithm.saltLength = pssParameters.saltLength; + else + algorithm.algorithm.saltLength = 20; + + var hash_algo = "SHA-1"; + + if("hashAlgorithm" in pssParameters) + { + var hashAlgorithm = in_window.org.pkijs.getAlgorithmByOID(pssParameters.hashAlgorithm.algorithm_id); + if(("name" in hashAlgorithm) === false) + return new Promise(function(resolve, reject) { reject("Unrecognized hash algorithm: " + pssParameters.hashAlgorithm.algorithm_id); }); + + hash_algo = hashAlgorithm.name; + } + + algorithm.algorithm.hash.name = hash_algo; + } + // #endregion + + return crypto.verify(algorithm.algorithm, + publicKey, + new Uint8Array(signature_value), + new Uint8Array(tbs)); + } + ); + // #endregion + + return sequence; + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CRL.prototype.sign = + function(privateKey, hashAlgorithm) + { + /// <param name="privateKey" type="Key">Private key for "subjectPublicKeyInfo" structure</param> + /// <param name="hashAlgorithm" type="String" optional="true">Hashing algorithm. Default SHA-1</param> + + // #region Initial variables + var _this = this; + // #endregion + + // #region Get a private key from function parameter + if(typeof privateKey === "undefined") + return new Promise(function(resolve, reject) { reject("Need to provide a private key for signing"); }); + // #endregion + + // #region Get hashing algorithm + if(typeof hashAlgorithm === "undefined") + hashAlgorithm = "SHA-1"; + else + { + // #region Simple check for supported algorithm + var oid = in_window.org.pkijs.getOIDByAlgorithm({ name: hashAlgorithm }); + if(oid === "") + return new Promise(function(resolve, reject) { reject("Unsupported hash algorithm: " + hashAlgorithm); }); + // #endregion + } + // #endregion + + // #region Get a "default parameters" for current algorithm + var defParams = in_window.org.pkijs.getAlgorithmParameters(privateKey.algorithm.name, "sign"); + defParams.algorithm.hash.name = hashAlgorithm; + // #endregion + + // #region Fill internal structures base on "privateKey" and "hashAlgorithm" + switch(privateKey.algorithm.name.toUpperCase()) + { + case "RSASSA-PKCS1-V1_5": + case "ECDSA": + _this.signature.algorithm_id = in_window.org.pkijs.getOIDByAlgorithm(defParams.algorithm); + _this.signatureAlgorithm.algorithm_id = _this.signature.algorithm_id; + break; + case "RSA-PSS": + { + // #region Set "saltLength" as a length (in octets) of hash function result + switch(hashAlgorithm.toUpperCase()) + { + case "SHA-256": + defParams.algorithm.saltLength = 32; + break; + case "SHA-384": + defParams.algorithm.saltLength = 48; + break; + case "SHA-512": + defParams.algorithm.saltLength = 64; + break; + default: + } + // #endregion + + // #region Fill "RSASSA_PSS_params" object + var paramsObject = {}; + + if(hashAlgorithm.toUpperCase() !== "SHA-1") + { + var hashAlgorithmOID = in_window.org.pkijs.getOIDByAlgorithm({ name: hashAlgorithm }); + if(hashAlgorithmOID === "") + return new Promise(function(resolve, reject) { reject("Unsupported hash algorithm: " + hashAlgorithm); }); + + paramsObject.hashAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ + algorithm_id: hashAlgorithmOID, + algorithm_params: new in_window.org.pkijs.asn1.NULL() + }); + + paramsObject.maskGenAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ + algorithm_id: "1.2.840.113549.1.1.8", // MGF1 + algorithm_params: paramsObject.hashAlgorithm.toSchema() + }) + } + + if(defParams.algorithm.saltLength !== 20) + paramsObject.saltLength = defParams.algorithm.saltLength; + + var pssParameters = new in_window.org.pkijs.simpl.x509.RSASSA_PSS_params(paramsObject); + // #endregion + + // #region Automatically set signature algorithm + _this.signature = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ + algorithm_id: "1.2.840.113549.1.1.10", + algorithm_params: pssParameters.toSchema() + }); + _this.signatureAlgorithm = _this.signature; // Must be the same + // #endregion + } + break; + default: + return new Promise(function(resolve, reject) { reject("Unsupported signature algorithm: " + privateKey.algorithm.name); }); + } + // #endregion + + // #region Create TBS data for signing + _this.tbs = in_window.org.pkijs.simpl.CRL.prototype.encodeTBS.call(this).toBER(false); + // #endregion + + // #region Get a "crypto" extension + var crypto = in_window.org.pkijs.getCrypto(); + if(typeof crypto == "undefined") + return new Promise(function(resolve, reject) { reject("Unable to create WebCrypto object"); }); + // #endregion + + // #region Signing TBS data on provided private key + return crypto.sign( + defParams.algorithm, + privateKey, + new Uint8Array(_this.tbs)). + then( + function(result) + { + // #region Special case for ECDSA algorithm + if(defParams.algorithm.name === "ECDSA") + result = in_window.org.pkijs.createCMSECDSASignature(result); + // #endregion + + _this.signatureValue = new in_window.org.pkijs.asn1.BITSTRING({ value_hex: result }); + }, + function(error) + { + return new Promise(function(resolve, reject) { reject("Signing error: " + error); }); + } + ); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CRL.prototype.isCertificateRevoked = + function() + { + // #region Get input certificate + var certificate = {}; + + if(arguments[0] instanceof Object) + { + if("certificate" in arguments[0]) + certificate = arguments[0].certificate; + } + + if((certificate instanceof in_window.org.pkijs.simpl.CERT) === false) + return false; + // #endregion + + // #region Check that issuer of the input certificate is the same with issuer of this CRL + if(this.issuer.isEqual(certificate.issuer) === false) + return false; + // #endregion + + // #region Check that there are revoked certificates in this CRL + if(("revokedCertificates" in this) === false) + return false; + // #endregion + + // #region Search for input certificate in revoked certificates array + for(var i = 0; i < this.revokedCertificates.length; i++) + { + if(this.revokedCertificates[i].userCertificate.isEqual(certificate.serialNumber)) + return true; + } + // #endregion + + return false; + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CRL.prototype.toJSON = + function() + { + var _object = { + tbs: in_window.org.pkijs.bufferToHexCodes(this.tbs, 0, this.tbs.byteLength), + signature: this.signature.toJSON(), + issuer: this.issuer.toJSON(), + thisUpdate: this.thisUpdate.toJSON(), + signatureAlgorithm: this.signatureAlgorithm.toJSON(), + signatureValue: this.signatureValue.toJSON() + }; + + if("version" in this) + _object.version = this.version; + + if("nextUpdate" in this) + _object.nextUpdate = this.nextUpdate.toJSON(); + + if("revokedCertificates" in this) + { + _object.revokedCertificates = new Array(); + + for(var i = 0; i < this.revokedCertificates.length; i++) + _object.revokedCertificates.push(this.revokedCertificates[i].toJSON()); + } + + if("crlExtensions" in this) + { + _object.crlExtensions = new Array(); + + for(var i = 0; i < this.crlExtensions.length; i++) + _object.crlExtensions.push(this.crlExtensions[i].toJSON()); + } + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for "Attribute" type + //************************************************************************************** + in_window.org.pkijs.simpl.ATTRIBUTE = + function() + { + // #region Internal properties of the object + this.type = ""; + this.values = new Array(); + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.ATTRIBUTE.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.type = arguments[0].type || ""; + this.values = arguments[0].values || new Array(); + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.ATTRIBUTE.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.ATTRIBUTE({ + names: { + type: "type", + values: "values" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for ATTRIBUTE"); + // #endregion + + // #region Get internal properties from parsed schema + this.type = asn1.result["type"].value_block.toString(); + this.values = asn1.result["values"]; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.ATTRIBUTE.prototype.toSchema = + function() + { + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + new in_window.org.pkijs.asn1.OID({ value: this.type }), + new in_window.org.pkijs.asn1.SET({ + value: this.values + }) + ] + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.ATTRIBUTE.prototype.toJSON = + function() + { + var _object = { + type: this.type, + values: new Array() + }; + + for(var i = 0; i < this.values.length; i++) + _object.values.push(this.values[i].toJSON()); + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for PKCS#10 certificate request + //************************************************************************************** + in_window.org.pkijs.simpl.PKCS10 = + function() + { + // #region Internal properties of the object + this.tbs = new ArrayBuffer(0); + + this.version = 0; + this.subject = new in_window.org.pkijs.simpl.RDN(); + this.subjectPublicKeyInfo = new in_window.org.pkijs.simpl.PUBLIC_KEY_INFO(); + // OPTIONAL this.attributes = new Array(); // Array of simpl.ATTRIBUTE objects + + this.signatureAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); // Signature algorithm from certificate major part + this.signatureValue = new in_window.org.pkijs.asn1.BITSTRING(); + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.PKCS10.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.tbs = arguments[0].tbs || new ArrayBuffer(0); + + this.version = arguments[0].version || 0; + this.subject = arguments[0].subject || new in_window.org.pkijs.simpl.RDN(); + this.subjectPublicKeyInfo = arguments[0].subjectPublicKeyInfo || new in_window.org.pkijs.simpl.PUBLIC_KEY_INFO(); + + if("attributes" in arguments[0]) + this.attributes = arguments[0].attributes; + + this.signatureAlgorithm = arguments[0].signatureAlgorithm || new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); // Signature algorithm from certificate major part + this.signatureValue = arguments[0].signatureValue || new in_window.org.pkijs.asn1.BITSTRING(); + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.PKCS10.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.PKCS10() + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for PKCS10"); + // #endregion + + // #region Get internal properties from parsed schema + this.tbs = asn1.result["CertificationRequestInfo"].value_before_decode; + + this.version = asn1.result["CertificationRequestInfo.version"].value_block.value_dec; + this.subject = new in_window.org.pkijs.simpl.RDN({ schema: asn1.result["CertificationRequestInfo.subject"] }); + this.subjectPublicKeyInfo = new in_window.org.pkijs.simpl.PUBLIC_KEY_INFO({ schema: asn1.result["CertificationRequestInfo.subjectPublicKeyInfo"] }); + if("CertificationRequestInfo.attributes" in asn1.result) + { + this.attributes = new Array(); + + var attrs = asn1.result["CertificationRequestInfo.attributes"]; + for(var i = 0; i < attrs.length; i++) + this.attributes.push(new in_window.org.pkijs.simpl.ATTRIBUTE({ schema: attrs[i] })); + } + + this.signatureAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ schema: asn1.result["signatureAlgorithm"] }); + this.signatureValue = asn1.result["signatureValue"]; + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.PKCS10.prototype.encodeTBS = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + output_array.push(new in_window.org.pkijs.asn1.INTEGER({ value: this.version })); + output_array.push(this.subject.toSchema()); + output_array.push(this.subjectPublicKeyInfo.toSchema()); + + if("attributes" in this) + { + var attributes = new Array(); + + for(var i = 0; i < this.attributes.length; i++) + attributes.push(this.attributes[i].toSchema()); + + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: attributes + })); + } + // #endregion + + return (new in_window.org.pkijs.asn1.SEQUENCE({ value: output_array })); + }; + //************************************************************************************** + in_window.org.pkijs.simpl.PKCS10.prototype.toSchema = + function(encodeFlag) + { + /// <param name="encodeFlag" type="Boolean">If param equal to false then create TBS schema via decoding stored value. In othe case create TBS schema via assembling from TBS parts.</param> + + // #region Check "encodeFlag" + if(typeof encodeFlag === "undefined") + encodeFlag = false; + // #endregion + + // #region Decode stored TBS value + var tbs_schema; + + if(encodeFlag === false) + { + if(this.tbs.length === 0) // No stored TBS part + return in_window.org.pkijs.schema.PKCS10(); + + tbs_schema = in_window.org.pkijs.fromBER(this.tbs).result; + } + // #endregion + // #region Create TBS schema via assembling from TBS parts + else + tbs_schema = in_window.org.pkijs.simpl.PKCS10.prototype.encodeTBS.call(this); + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: [ + tbs_schema, + this.signatureAlgorithm.toSchema(), + this.signatureValue + ] + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.PKCS10.prototype.verify = + function() + { + /// <summary>!!! Works well in Chrome dev versions only (April 2014th) !!!</summary> + /// <returns type="Promise">Returns a new Promise object (in case of error), or a result of "crypto.subtle.veryfy" function</returns> + + // #region Global variables + var _this = this; + var sha_algorithm = ""; + + var sequence = Promise.resolve(); + + var subjectPublicKeyInfo = this.subjectPublicKeyInfo; + var signature = this.signatureValue; + var tbs = this.tbs; + // #endregion + + // #region Get a "crypto" extension + var crypto = in_window.org.pkijs.getCrypto(); + if(typeof crypto == "undefined") + return new Promise(function(resolve, reject) { reject("Unable to create WebCrypto object"); }); + // #endregion + + // #region Find a correct hashing algorithm + sha_algorithm = in_window.org.pkijs.getHashAlgorithm(this.signatureAlgorithm); + if(sha_algorithm === "") + return new Promise(function(resolve, reject) { reject("Unsupported signature algorithm: " + _this.signatureAlgorithm.algorithm_id); }); + // #endregion + + // #region Importing public key + sequence = sequence.then( + function() + { + // #region Get information about public key algorithm and default parameters for import + var algorithmObject = in_window.org.pkijs.getAlgorithmByOID(_this.signatureAlgorithm.algorithm_id); + if(("name" in algorithmObject) === false) + return new Promise(function(resolve, reject) { reject("Unsupported public key algorithm: " + _this.signatureAlgorithm.algorithm_id); }); + + var algorithm_name = algorithmObject.name; + + var algorithm = in_window.org.pkijs.getAlgorithmParameters(algorithm_name, "importkey"); + if("hash" in algorithm.algorithm) + algorithm.algorithm.hash.name = sha_algorithm; + // #endregion + + var publicKeyInfo_schema = subjectPublicKeyInfo.toSchema(); + var publicKeyInfo_buffer = publicKeyInfo_schema.toBER(false); + var publicKeyInfo_view = new Uint8Array(publicKeyInfo_buffer); + + return crypto.importKey("spki", publicKeyInfo_view, algorithm.algorithm, true, algorithm.usages); + } + ); + // #endregion + + // #region Verify signature + sequence = sequence.then( + function(publicKey) + { + // #region Get default algorithm parameters for verification + var algorithm = in_window.org.pkijs.getAlgorithmParameters(publicKey.algorithm.name, "verify"); + if("hash" in algorithm.algorithm) + algorithm.algorithm.hash.name = sha_algorithm; + // #endregion + + // #region Special case for ECDSA signatures + var signature_value = signature.value_block.value_hex; + + if(publicKey.algorithm.name === "ECDSA") + { + var asn1 = in_window.org.pkijs.fromBER(signature_value); + signature_value = in_window.org.pkijs.createECDSASignatureFromCMS(asn1.result); + } + // #endregion + + // #region Special case for RSA-PSS + if(publicKey.algorithm.name === "RSA-PSS") + { + var pssParameters; + + try + { + pssParameters = new in_window.org.pkijs.simpl.x509.RSASSA_PSS_params({ schema: _this.signatureAlgorithm.algorithm_params }); + } + catch(ex) + { + return new Promise(function(resolve, reject) { reject(ex); }); + } + + if("saltLength" in pssParameters) + algorithm.algorithm.saltLength = pssParameters.saltLength; + else + algorithm.algorithm.saltLength = 20; + + var hash_algo = "SHA-1"; + + if("hashAlgorithm" in pssParameters) + { + var hashAlgorithm = in_window.org.pkijs.getAlgorithmByOID(pssParameters.hashAlgorithm.algorithm_id); + if(("name" in hashAlgorithm) === false) + return new Promise(function(resolve, reject) { reject("Unrecognized hash algorithm: " + pssParameters.hashAlgorithm.algorithm_id); }); + + hash_algo = hashAlgorithm.name; + } + + algorithm.algorithm.hash.name = hash_algo; + } + // #endregion + + return crypto.verify(algorithm.algorithm, + publicKey, + new Uint8Array(signature_value), + new Uint8Array(tbs)); + } + ); + // #endregion + + return sequence; + }; + //************************************************************************************** + in_window.org.pkijs.simpl.PKCS10.prototype.sign = + function(privateKey, hashAlgorithm) + { + /// <param name="privateKey" type="Key">Private key for "subjectPublicKeyInfo" structure</param> + /// <param name="hashAlgorithm" type="String" optional="true">Hashing algorithm. Default SHA-1</param> + + // #region Initial variables + var _this = this; + // #endregion + + // #region Get a private key from function parameter + if(typeof privateKey === "undefined") + return new Promise(function(resolve, reject) { reject("Need to provide a private key for signing"); }); + // #endregion + + // #region Get hashing algorithm + if(typeof hashAlgorithm === "undefined") + hashAlgorithm = "SHA-1"; + else + { + // #region Simple check for supported algorithm + var oid = in_window.org.pkijs.getOIDByAlgorithm({ name: hashAlgorithm }); + if(oid === "") + return new Promise(function(resolve, reject) { reject("Unsupported hash algorithm: " + hashAlgorithm); }); + // #endregion + } + // #endregion + + // #region Get a "default parameters" for current algorithm + var defParams = in_window.org.pkijs.getAlgorithmParameters(privateKey.algorithm.name, "sign"); + defParams.algorithm.hash.name = hashAlgorithm; + // #endregion + + // #region Fill internal structures base on "privateKey" and "hashAlgorithm" + switch(privateKey.algorithm.name.toUpperCase()) + { + case "RSASSA-PKCS1-V1_5": + case "ECDSA": + _this.signatureAlgorithm.algorithm_id = in_window.org.pkijs.getOIDByAlgorithm(defParams.algorithm); + break; + case "RSA-PSS": + { + // #region Set "saltLength" as a length (in octets) of hash function result + switch(hashAlgorithm.toUpperCase()) + { + case "SHA-256": + defParams.algorithm.saltLength = 32; + break; + case "SHA-384": + defParams.algorithm.saltLength = 48; + break; + case "SHA-512": + defParams.algorithm.saltLength = 64; + break; + default: + } + // #endregion + + // #region Fill "RSASSA_PSS_params" object + var paramsObject = {}; + + if(hashAlgorithm.toUpperCase() !== "SHA-1") + { + var hashAlgorithmOID = in_window.org.pkijs.getOIDByAlgorithm({ name: hashAlgorithm }); + if(hashAlgorithmOID === "") + return new Promise(function(resolve, reject) { reject("Unsupported hash algorithm: " + hashAlgorithm); }); + + paramsObject.hashAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ + algorithm_id: hashAlgorithmOID, + algorithm_params: new in_window.org.pkijs.asn1.NULL() + }); + + paramsObject.maskGenAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ + algorithm_id: "1.2.840.113549.1.1.8", // MGF1 + algorithm_params: paramsObject.hashAlgorithm.toSchema() + }) + } + + if(defParams.algorithm.saltLength !== 20) + paramsObject.saltLength = defParams.algorithm.saltLength; + + var pssParameters = new in_window.org.pkijs.simpl.x509.RSASSA_PSS_params(paramsObject); + // #endregion + + // #region Automatically set signature algorithm + _this.signatureAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ + algorithm_id: "1.2.840.113549.1.1.10", + algorithm_params: pssParameters.toSchema() + }); + // #endregion + } + break; + default: + return new Promise(function(resolve, reject) { reject("Unsupported signature algorithm: " + privateKey.algorithm.name); }); + } + // #endregion + + // #region Create TBS data for signing + _this.tbs = in_window.org.pkijs.simpl.PKCS10.prototype.encodeTBS.call(this).toBER(false); + // #endregion + + // #region Get a "crypto" extension + var crypto = in_window.org.pkijs.getCrypto(); + if(typeof crypto == "undefined") + return new Promise(function(resolve, reject) { reject("Unable to create WebCrypto object"); }); + // #endregion + + // #region Signing TBS data on provided private key + return crypto.sign(defParams.algorithm, + privateKey, + new Uint8Array(_this.tbs)).then( + function(result) + { + // #region Special case for ECDSA algorithm + if(defParams.algorithm.name === "ECDSA") + result = in_window.org.pkijs.createCMSECDSASignature(result); + // #endregion + + _this.signatureValue = new in_window.org.pkijs.asn1.BITSTRING({ value_hex: result }); + }, + function(error) + { + return new Promise(function(resolve, reject) { reject("Signing error: " + error); }); + } + ); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.PKCS10.prototype.toJSON = + function() + { + var _object = { + tbs: in_window.org.pkijs.bufferToHexCodes(this.tbs, 0, this.tbs.byteLength), + version: this.version, + subject: this.subject.toJSON(), + subjectPublicKeyInfo: this.subjectPublicKeyInfo.toJSON(), + signatureAlgorithm: this.signatureAlgorithm.toJSON(), + signatureValue: this.signatureValue.toJSON() + }; + + if("attributes" in this) + { + _object.attributes = new Array(); + + for(var i = 0; i < this.attributes.length; i++) + _object.attributes.push(this.attributes[i].toJSON()); + } + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for PKCS#8 private key bag + //************************************************************************************** + in_window.org.pkijs.simpl.PKCS8 = + function() + { + // #region Internal properties of the object + this.version = 0; + this.privateKeyAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); + this.privateKey = new in_window.org.pkijs.asn1.OCTETSTRING(); + // OPTIONAL this.attributes // Array of "in_window.org.pkijs.simpl.ATTRIBUTE" + // #endregion + + // #region If input argument array contains "schema" for this object + if((arguments[0] instanceof Object) && ("schema" in arguments[0])) + in_window.org.pkijs.simpl.PKCS8.prototype.fromSchema.call(this, arguments[0].schema); + // #endregion + // #region If input argument array contains "native" values for internal properties + else + { + if(arguments[0] instanceof Object) + { + this.version = arguments[0].version || 0; + this.privateKeyAlgorithm = arguments[0].privateKeyAlgorithm || new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER(); + this.privateKey = arguments[0].privateKey || new in_window.org.pkijs.asn1.OCTETSTRING(); + + if("attributes" in arguments[0]) + this.attributes = arguments[0].attributes; + } + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.PKCS8.prototype.fromSchema = + function(schema) + { + // #region Check the schema is valid + var asn1 = in_window.org.pkijs.compareSchema(schema, + schema, + in_window.org.pkijs.schema.PKCS8({ + names: { + version: "version", + privateKeyAlgorithm: { + names: { + block_name: "privateKeyAlgorithm" + } + }, + privateKey: "privateKey", + attributes: "attributes" + } + }) + ); + + if(asn1.verified === false) + throw new Error("Object's schema was not verified against input data for PKCS8"); + // #endregion + + // #region Get internal properties from parsed schema + this.version = asn1.result["version"].value_block.value_dec; + this.privateKeyAlgorithm = new in_window.org.pkijs.simpl.ALGORITHM_IDENTIFIER({ schema: asn1.result["privateKeyAlgorithm"] }); + this.privateKey = asn1.result["privateKey"]; + + if("attributes" in asn1.result) + { + this.attributes = new Array(); + var attrs = asn1.result["attributes"]; + + for(var i = 0; i < attrs.length; i++) + this.attributes.push(new in_window.org.pkijs.simpl.ATTRIBUTE({ schema: attrs[i] })); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.PKCS8.prototype.toSchema = + function() + { + // #region Create array for output sequence + var output_array = new Array(); + + output_array.push(new in_window.org.pkijs.asn1.INTEGER({ value: this.version })); + output_array.push(this.privateKeyAlgorithm.toSchema()); + output_array.push(this.privateKey); + + if("attributes" in this) + { + var attrs = new Array(); + + for(var i = 0; i < this.attributes.length; i++) + attrs.push(this.attributes[i].toSchema()); + + output_array.push(new in_window.org.pkijs.asn1.ASN1_CONSTRUCTED({ + optional: true, + id_block: { + tag_class: 3, // CONTEXT-SPECIFIC + tag_number: 0 // [0] + }, + value: attrs + })); + } + // #endregion + + // #region Construct and return new ASN.1 schema for this object + return (new in_window.org.pkijs.asn1.SEQUENCE({ + value: output_array + })); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.PKCS8.prototype.toJSON = + function() + { + var _object = { + version: this.version, + privateKeyAlgorithm: this.privateKeyAlgorithm.toJSON(), + privateKey: this.privateKey.toJSON() + }; + + if("attributes" in this) + { + _object.attributes = new Array(); + + for(var i = 0; i < this.attributes.length; i++) + _object.attributes.push(this.attributes[i].toJSON()); + } + + return _object; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** + // #region Simplified structure for working with X.509 certificate chains + //************************************************************************************** + in_window.org.pkijs.simpl.CERT_CHAIN = + function() + { + // #region Internal properties of the object + /// <field name="trusted_certs" type="Array" elementType="in_window.org.pkijs.simpl.CERT">Array of pre-defined trusted (by user) certificates</field> + this.trusted_certs = new Array(); + /// <field name="certs" type="Array" elementType="in_window.org.pkijs.simpl.CERT">Array with certificate chain. Could be only one end-user certificate in there!</field> + this.certs = new Array(); + /// <field name="crls" type="Array" elementType="in_window.org.pkijs.simpl.CRL">Array of all CRLs for all certificates from certificate chain</field> + this.crls = new Array(); + // #endregion + + // #region Initialize internal properties by input values + if(arguments[0] instanceof Object) + { + this.trusted_certs = arguments[0].trusted_certs || new Array(); + this.certs = arguments[0].certs || new Array(); + this.crls = arguments[0].crls || new Array(); + } + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CERT_CHAIN.prototype.sort = + function() + { + // #region Initial variables + /// <var type="Array" elementType="in_window.org.pkijs.simpl.CERT">Array of sorted certificates</var> + var sorted_certs = new Array(); + + /// <var type="Array" elementType="in_window.org.pkijs.simpl.CERT">Initial array of certificates</var> + var certs = this.certs.slice(0); // Explicity copy "this.certs" + + /// <var type="Date">Date for checking certificate validity period</var> + var check_date = new Date(); + + var _this = this; + // #endregion + + // #region Initial checks + if(certs.length === 0) + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 2, + result_message: "Certificate's array can not be empty" + }); + }); + // #endregion + + // #region Find end-user certificate + var end_user_index = -1; + + for(var i = 0; i < certs.length; i++) + { + var isCA = false; + + if("extensions" in certs[i]) + { + var mustBeCA = false; + var keyUsagePresent = false; + var cRLSign = false; + + for(var j = 0; j < certs[i].extensions.length; j++) + { + if((certs[i].extensions[j].critical === true) && + (("parsedValue" in certs[i].extensions[j]) === false)) + { + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 6, + result_message: "Unable to parse critical certificate extension: " + certs[i].extensions[j].extnID + }); + }); + } + + if(certs[i].extensions[j].extnID === "2.5.29.15") // KeyUsage + { + keyUsagePresent = true; + + var view = new Uint8Array(certs[i].extensions[j].parsedValue.value_block.value_hex); + + if((view[0] & 0x04) === 0x04) // Set flag "keyCertSign" + mustBeCA = true; + + if((view[0] & 0x02) === 0x02) // Set flag "cRLSign" + cRLSign = true; + } + + if(certs[i].extensions[j].extnID === "2.5.29.19") // BasicConstraints + { + if("cA" in certs[i].extensions[j].parsedValue) + { + if(certs[i].extensions[j].parsedValue.cA === true) + isCA = true; + } + } + } + + if((mustBeCA === true) && (isCA === false)) + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 3, + result_message: "Unable to build certificate chain - using \"keyCertSign\" flag set without BasicConstaints" + }); + }); + + if((keyUsagePresent === true) && (isCA === true) && (mustBeCA === false)) + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 4, + result_message: "Unable to build certificate chain - \"keyCertSign\" flag was not set" + }); + }); + + if((isCA === true) && (keyUsagePresent === true) && (cRLSign === false)) + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 5, + result_message: "Unable to build certificate chain - intermediate certificate must have \"cRLSign\" key usage flag" + }); + }); + } + + if(isCA === false) + { + if(sorted_certs.length !== 0) + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 7, + result_message: "Unable to build certificate chain - more than one possible end-user certificate" + }); + }); + + sorted_certs.push(certs[i]); + end_user_index = i; + } + } + + certs.splice(end_user_index, 1); + // #endregion + + // #region Check that end-user certificate was found + if(sorted_certs.length === 0) + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 1, + result_message: "Can't find end-user certificate" + }); + }); + // #endregion + + // #region Return if there is only one certificate in certificate's array + if(certs.length === 0) + { + if(sorted_certs[0].issuer.isEqual(sorted_certs[0].subject) === true) + return new Promise(function(resolve, reject) { resolve(sorted_certs); }); + else + { + if(this.trusted_certs.length === 0) + { + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 70, + result_message: "Can't find root certificate" + }); + }); + } + else + { + certs = _this.trusted_certs.splice(0); + } + } + + } + // #endregion + + /// <var type="in_window.org.pkijs.simpl.CERT">Current certificate (to find issuer for)</var> + var current_certificate = sorted_certs[0]; + + // #region Auxiliary functions working with Promises + function basic(subject_certificate, issuer_certificate) + { + /// <summary>Basic certificate checks</summary> + /// <param name="subject_certificate" type="in_window.org.pkijs.simpl.CERT">Certificate for testing (subject)</param> + /// <param name="issuer_certificate" type="in_window.org.pkijs.simpl.CERT">Certificate for issuer of subject certificate</param> + + // #region Initial variables + var sequence = Promise.resolve(); + // #endregion + + // #region Check validity period for subject certificate + sequence = sequence.then( + function() + { + if((subject_certificate.notBefore.value > check_date) || + (subject_certificate.notAfter.value < check_date)) + { + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 8, + result_message: "Certificate validity period is out of checking date" + }); + }); + } + } + ); + // #endregion + + // #region Give ability to not provide CRLs (all certificates assume to be valid) + if(_this.crls.length === 0) + return sequence.then( + function() + { + return new Promise(function(resolve, reject) { resolve(); }); + } + ); + // #endregion + + // #region Find correct CRL for "issuer_certificate" + function find_crl(index) + { + return _this.crls[index].verify({ issuerCertificate: issuer_certificate }).then( + function(result) + { + if(result === true) + return new Promise(function(resolve, reject) { resolve(_this.crls[index]); }); + else + { + index++; + + if(index < _this.crls.length) + return find_crl(index); + else + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 9, + result_message: "Unable to find CRL for issuer's certificate" + }); + }); + } + }, + function(error) + { + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 10, + result_message: "Unable to find CRL for issuer's certificate" + }); + }); + } + ); + } + + sequence = sequence.then( + function() + { + return find_crl(0); + } + ); + // #endregion + + // #region Check that subject certificate is not in the CRL + sequence = sequence.then( + function(crl) + { + /// <param name="crl" type="in_window.org.pkijs.simpl.CRL">CRL for issuer's certificate</param> + + if(crl.isCertificateRevoked({ certificate: subject_certificate }) === true) + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 11, + result_message: "Subject certificate was revoked" + }); + }); + else + return new Promise(function(resolve, reject) { resolve(); }); + }, + function(error) + { + /// <summary>Not for all certificates we have a CRL. So, this "stub" is for handling such situation - assiming we have a valid, non-revoked certificate</summary> + return new Promise(function(resolve, reject) { resolve(); }); + } + ); + // #endregion + + return sequence; + } + + function outer() + { + return inner(current_certificate, 0).then( + function(index) + { + sorted_certs.push(certs[index]); + current_certificate = certs[index]; + + certs.splice(index, 1); + + if(current_certificate.issuer.isEqual(current_certificate.subject) === true) + { + // #region Check that the "self-signed" certificate there is in "trusted_certs" array + var found = (_this.trusted_certs.length === 0); // If user did not set "trusted_certs" then we have an option to trust any self-signed certificate as root + + for(var i = 0; i < _this.trusted_certs.length; i++) + { + if((current_certificate.issuer.isEqual(_this.trusted_certs[i].issuer) === true) && + (current_certificate.subject.isEqual(_this.trusted_certs[i].subject) === true) && + (current_certificate.serialNumber.isEqual(_this.trusted_certs[i].serialNumber) === true)) + { + found = true; + break; + } + } + + if(found === false) + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 22, + result_message: "Self-signed root certificate not in \"trusted certificates\" array" + }); + }); + // #endregion + + return (current_certificate.verify()).then( // Verifing last, self-signed certificate + function(result) + { + if(result === true) + return basic(current_certificate, current_certificate).then( + function() + { + return new Promise(function(resolve, reject) { resolve(sorted_certs); }); + }, + function(error) + { + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 12, + result_message: error + }); + }); + } + ); + else + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 13, + result_message: "Unable to build certificate chain - signature of root certificate is invalid" + }); + }); + }, + function(error) + { + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 14, + result_message: error + }); + }); + } + ); + } + else // In case if self-signed cert for the chain in the "trusted_certs" array + { + if(certs.length > 0) + return outer(); + else + { + if(_this.trusted_certs.length !== 0) + { + certs = _this.trusted_certs.splice(0); + return outer(); + } + else + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 23, + result_message: "Root certificate not found" + }); + }); + } + } + }, + function(error) + { + return new Promise(function(resolve, reject) + { + reject(error); + }); + } + ); + } + + function inner(current_certificate, index) + { + if(certs[index].subject.isEqual(current_certificate.issuer) === true) + { + return current_certificate.verify({ issuerCertificate: certs[index] }).then( + function(result) + { + if(result === true) + { + return basic(current_certificate, certs[index]).then( + function() + { + return new Promise(function(resolve, reject) { resolve(index); }); + }, + function(error) + { + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 16, + result_message: error + }); + }); + } + ); + } + else + { + if(index < (certs.length - 1)) + return inner(current_certificate, index + 1); + else + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 17, + result_message: "Unable to build certificate chain - incomplete certificate chain or signature of some certificate is invalid" + }); + }); + } + }, + function(error) + { + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 18, + result_message: "Unable to build certificate chain - error during certificate signature verification" + }); + }); + } + ); + } + else + { + if(index < (certs.length - 1)) + return inner(current_certificate, index + 1); + else + return new Promise(function(resolve, reject) + { + reject({ + result: false, + result_code: 19, + result_message: "Unable to build certificate chain - incomplete certificate chain" + }); + }); + } + } + // #endregion + + // #region Find certificates for all issuers + return outer(); + // #endregion + }; + //************************************************************************************** + in_window.org.pkijs.simpl.CERT_CHAIN.prototype.verify = + function() + { + // #region Initial checks + if(this.certs.length === 0) + return new Promise(function(resolve, reject) { reject("Empty certificate array"); }); + // #endregion + + // #region Initial variables + var sequence = Promise.resolve(); + + var _this = this; + // #endregion + + // #region Get input variables + var initial_policy_set = new Array(); + initial_policy_set.push("2.5.29.32.0"); // "anyPolicy" + + var initial_explicit_policy = false; + var initial_policy_mapping_inhibit = false; + var initial_inhibit_policy = false; + + var initial_permitted_subtrees_set = new Array(); // Array of "simpl.x509.GeneralSubtree" + var initial_excluded_subtrees_set = new Array(); // Array of "simpl.x509.GeneralSubtree" + var initial_required_name_forms = new Array(); // Array of "simpl.x509.GeneralSubtree" + + var verification_time = new Date(); + + if(arguments[0] instanceof Object) + { + if("initial_policy_set" in arguments[0]) + initial_policy_set = arguments[0].initial_policy_set; + + if("initial_explicit_policy" in arguments[0]) + initial_explicit_policy = arguments[0].initial_explicit_policy; + + if("initial_policy_mapping_inhibit" in arguments[0]) + initial_policy_mapping_inhibit = arguments[0].initial_policy_mapping_inhibit; + + if("initial_inhibit_policy" in arguments[0]) + initial_inhibit_policy = arguments[0].initial_inhibit_policy; + + if("initial_permitted_subtrees_set" in arguments[0]) + initial_permitted_subtrees_set = arguments[0].initial_permitted_subtrees_set; + + if("initial_excluded_subtrees_set" in arguments[0]) + initial_excluded_subtrees_set = arguments[0].initial_excluded_subtrees_set; + + if("initial_required_name_forms" in arguments[0]) + initial_required_name_forms = arguments[0].initial_required_name_forms; + } + + var explicit_policy_indicator = initial_explicit_policy; + var policy_mapping_inhibit_indicator = initial_policy_mapping_inhibit; + var inhibit_any_policy_indicator = initial_inhibit_policy; + + var pending_constraints = new Array(3); + pending_constraints[0] = false; // For "explicit_policy_pending" + pending_constraints[1] = false; // For "policy_mapping_inhibit_pending" + pending_constraints[2] = false; // For "inhibit_any_policy_pending" + + var explicit_policy_pending = 0; + var policy_mapping_inhibit_pending = 0; + var inhibit_any_policy_pending = 0; + + var permitted_subtrees = initial_permitted_subtrees_set; + var excluded_subtrees = initial_excluded_subtrees_set; + var required_name_forms = initial_required_name_forms; + + var path_depth = 1; + // #endregion + + // #region Sorting certificates in the chain array + sequence = (in_window.org.pkijs.simpl.CERT_CHAIN.prototype.sort.call(this)).then( + function(sorted_certs) + { + _this.certs = sorted_certs; + } + ); + // #endregion + + // #region Work with policies + sequence = sequence.then( + function() + { + // #region Support variables + var all_policies = new Array(); // Array of all policies (string values) + all_policies.push("2.5.29.32.0"); // Put "anyPolicy" at first place + + var policies_and_certs = new Array(); // In fact "array of array" where rows are for each specific policy, column for each certificate and value is "true/false" + + var any_policy_array = new Array(_this.certs.length - 1); // Minus "trusted anchor" + for(var ii = 0; ii < (_this.certs.length - 1) ; ii++) + any_policy_array[ii] = true; + + policies_and_certs.push(any_policy_array); + + var policy_mappings = new Array(_this.certs.length - 1); // Array of "PolicyMappings" for each certificate + var cert_policies = new Array(_this.certs.length - 1); // Array of "CertificatePolicies" for each certificate + // #endregion + + for(var i = (_this.certs.length - 2) ; i >= 0 ; i--, path_depth++) + { + if("extensions" in _this.certs[i]) + { + for(var j = 0; j < _this.certs[i].extensions.length; j++) + { + // #region CertificatePolicies + if(_this.certs[i].extensions[j].extnID === "2.5.29.32") + { + cert_policies[i] = _this.certs[i].extensions[j].parsedValue; + + for(var k = 0; k < _this.certs[i].extensions[j].parsedValue.certificatePolicies.length; k++) + { + var policy_index = (-1); + + // #region Try to find extension in "all_policies" array + for(var s = 0; s < all_policies.length; s++) + { + if(_this.certs[i].extensions[j].parsedValue.certificatePolicies[k].policyIdentifier === all_policies[s]) + { + policy_index = s; + break; + } + } + // #endregion + + if(policy_index === (-1)) + { + all_policies.push(_this.certs[i].extensions[j].parsedValue.certificatePolicies[k].policyIdentifier); + + var cert_array = new Array(_this.certs.length - 1); + cert_array[i] = true; + + policies_and_certs.push(cert_array); + } + else(policies_and_certs[policy_index])[i] = true; + } + } + // #endregion + + // #region PolicyMappings + if(_this.certs[i].extensions[j].extnID === "2.5.29.33") + policy_mappings[i] = _this.certs[i].extensions[j].parsedValue; + // #endregion + + // #region PolicyConstraints + if(_this.certs[i].extensions[j].extnID === "2.5.29.36") + { + if(explicit_policy_indicator == false) + { + // #region requireExplicitPolicy + if(_this.certs[i].extensions[j].parsedValue.requireExplicitPolicy === 0) + explicit_policy_indicator = true; + else + { + if(pending_constraints[0] === false) + { + pending_constraints[0] = true; + explicit_policy_pending = _this.certs[i].extensions[j].parsedValue.requireExplicitPolicy; + } + else + { + explicit_policy_pending = (explicit_policy_pending > _this.certs[i].extensions[j].parsedValue.requireExplicitPolicy) ? _this.certs[i].extensions[j].parsedValue.requireExplicitPolicy : explicit_policy_pending; + } + } + // #endregion + + // #region inhibitPolicyMapping + if(_this.certs[i].extensions[j].parsedValue.inhibitPolicyMapping === 0) + policy_mapping_inhibit_indicator = true; + else + { + if(pending_constraints[1] === false) + { + pending_constraints[1] = true; + policy_mapping_inhibit_pending = _this.certs[i].extensions[j].parsedValue.requireExplicitPolicy; + } + else + { + policy_mapping_inhibit_pending = (policy_mapping_inhibit_pending > _this.certs[i].extensions[j].parsedValue.requireExplicitPolicy) ? _this.certs[i].extensions[j].parsedValue.requireExplicitPolicy : policy_mapping_inhibit_pending; + } + } + // #endregion + } + } + // #endregion + + // #region InhibitAnyPolicy + if(_this.certs[i].extensions[j].extnID === "2.5.29.54") + { + if(inhibit_any_policy_indicator === false) + { + if(_this.certs[i].extensions[j].parsedValue.value_block.value_dec === 0) + inhibit_any_policy_indicator = true; + else + { + if(pending_constraints[2] === false) + { + pending_constraints[2] = true; + inhibit_any_policy_pending = _this.certs[i].extensions[j].parsedValue.value_block.value_dec; + } + else + { + inhibit_any_policy_pending = (inhibit_any_policy_pending > _this.certs[i].extensions[j].parsedValue.value_block.value_dec) ? _this.certs[i].extensions[j].parsedValue.value_block.value_dec : inhibit_any_policy_pending; + } + } + } + } + // #endregion + } + + // #region Check "inhibit_any_policy_indicator" + if(inhibit_any_policy_indicator === true) + delete (policies_and_certs[0])[i]; // Unset value to "undefined" for "anyPolicies" value for current certificate + // #endregion + + // #region Combine information from certificate policies and policy mappings + if((typeof cert_policies[i] !== "undefined") && + (typeof policy_mappings[i] !== "undefined") && + (policy_mapping_inhibit_indicator === false)) + { + for(var m = 0; m < cert_policies[i].certificatePolicies.length; m++) + { + var domainPolicy = ""; + + // #region Find if current policy is in "mappings" array + for(var n = 0; n < policy_mappings[i].mappings.length; n++) + { + if(policy_mappings[i].mappings[n].subjectDomainPolicy === cert_policies[i].certificatePolicies[m].policyIdentifier) + { + domainPolicy = policy_mappings[i].mappings[n].issuerDomainPolicy; + break; + } + + // #region Could be the case for some reasons + if(policy_mappings[i].mappings[n].issuerDomainPolicy === cert_policies[i].certificatePolicies[m].policyIdentifier) + { + domainPolicy = policy_mappings[i].mappings[n].subjectDomainPolicy; + break; + } + // #endregion + } + + if(domainPolicy === "") + continue; + // #endregion + + // #region Find the index of "domainPolicy" + var domainPolicy_index = (-1); + + for(var p = 0; p < all_policies.length; p++) + { + if(all_policies[p] === domainPolicy) + { + domainPolicy_index = p; + break; + } + } + // #endregion + + // #region Change array value for "domainPolicy" + if(domainPolicy_index !== (-1)) + (policies_and_certs[domainPolicy_index])[i] = true; // Put "set" in "domainPolicy" cell for specific certificate + // #endregion + } + } + // #endregion + + // #region Process with "pending constraints" + if(explicit_policy_indicator === false) + { + if(pending_constraints[0] === true) + { + explicit_policy_pending--; + if(explicit_policy_pending === 0) + { + explicit_policy_indicator = true; + pending_constraints[0] = false; + } + } + } + + if(policy_mapping_inhibit_indicator === false) + { + if(pending_constraints[1] === true) + { + policy_mapping_inhibit_pending--; + if(policy_mapping_inhibit_pending === 0) + { + policy_mapping_inhibit_indicator = true; + pending_constraints[1] = false; + } + } + } + + if(inhibit_any_policy_indicator === false) + { + if(pending_constraints[2] === true) + { + inhibit_any_policy_pending--; + if(inhibit_any_policy_pending === 0) + { + inhibit_any_policy_indicator = true; + pending_constraints[2] = false; + } + } + } + // #endregion + } + } + + // #region Create "set of authorities-constrained policies" + var auth_constr_policies = new Array(); + + for(var i = 0; i < policies_and_certs.length; i++) + { + var found = true; + + for(var j = 0; j < (_this.certs.length - 1) ; j++) + { + if(typeof (policies_and_certs[i])[j] === "undefined") + { + found = false; + break; + } + } + + if(found === true) + auth_constr_policies.push(all_policies[i]); + } + // #endregion + + // #region Create "set of user-constrained policies" + var user_constr_policies = new Array(); + + for(var i = 0; i < auth_constr_policies.length; i++) + { + for(var j = 0; j < initial_policy_set.length; j++) + { + if(initial_policy_set[j] === auth_constr_policies[i]) + { + user_constr_policies.push(initial_policy_set[j]); + break; + } + } + } + // #endregion + + // #region Combine output object + return { + result: (user_constr_policies.length > 0), + result_code: 0, + result_message: (user_constr_policies.length > 0) ? "" : "Zero \"user_constr_policies\" array, no intersections with \"auth_constr_policies\"", + auth_constr_policies: auth_constr_policies, + user_constr_policies: user_constr_policies, + explicit_policy_indicator: explicit_policy_indicator, + policy_mappings: policy_mappings + }; + // #endregion + } + ); + // #endregion + + // #region Work with name constraints + sequence = sequence.then( + function(policy_result) + { + // #region Auxiliary functions for name constraints checking + function compare_dNSName(name, constraint) + { + /// <summary>Compare two dNSName values</summary> + /// <param name="name" type="String">DNS from name</param> + /// <param name="constraint" type="String">Constraint for DNS from name</param> + /// <returns type="Boolean">Boolean result - valid or invalid the "name" against the "constraint"</returns> + + // #region Make a "string preparation" for both name and constrain + var name_prepared = in_window.org.pkijs.stringPrep(name); + var constraint_prepared = in_window.org.pkijs.stringPrep(constraint); + // #endregion + + // #region Make a "splitted" versions of "constraint" and "name" + var name_splitted = name_prepared.split("."); + var constraint_splitted = constraint_prepared.split("."); + // #endregion + + // #region Length calculation and additional check + var name_len = name_splitted.length; + var constr_len = constraint_splitted.length; + + if((name_len === 0) || (constr_len === 0) || (name_len < constr_len)) + return false; + // #endregion + + // #region Check that no part of "name" has zero length + for(var i = 0; i < name_len; i++) + { + if(name_splitted[i].length === 0) + return false; + } + // #endregion + + // #region Check that no part of "constraint" has zero length + for(var i = 0; i < constr_len; i++) + { + if(constraint_splitted[i].length === 0) + { + if(i === 0) + { + if(constr_len === 1) + return false; + else + continue; + } + + return false; + } + } + // #endregion + + // #region Check that "name" has a tail as "constraint" + + for(var i = 0; i < constr_len; i++) + { + if(constraint_splitted[constr_len - 1 - i].length === 0) + continue; + + if(name_splitted[name_len - 1 - i].localeCompare(constraint_splitted[constr_len - 1 - i]) !== 0) + return false; + } + // #endregion + + return true; + } + + function compare_rfc822Name(name, constraint) + { + /// <summary>Compare two rfc822Name values</summary> + /// <param name="name" type="String">E-mail address from name</param> + /// <param name="constraint" type="String">Constraint for e-mail address from name</param> + /// <returns type="Boolean">Boolean result - valid or invalid the "name" against the "constraint"</returns> + + // #region Make a "string preparation" for both name and constrain + var name_prepared = in_window.org.pkijs.stringPrep(name); + var constraint_prepared = in_window.org.pkijs.stringPrep(constraint); + // #endregion + + // #region Make a "splitted" versions of "constraint" and "name" + var name_splitted = name_prepared.split("@"); + var constraint_splitted = constraint_prepared.split("@"); + // #endregion + + // #region Splitted array length checking + if((name_splitted.length === 0) || (constraint_splitted.length === 0) || (name_splitted.length < constraint_splitted.length)) + return false; + // #endregion + + if(constraint_splitted.length === 1) + { + var result = compare_dNSName(name_splitted[1], constraint_splitted[0]); + + if(result) + { + // #region Make a "splitted" versions of domain name from "constraint" and "name" + var ns = name_splitted[1].split("."); + var cs = constraint_splitted[0].split("."); + // #endregion + + if(cs[0].length === 0) + return true; + + return ns.length === cs.length; + } + else + return false; + } + else + return (name_prepared.localeCompare(constraint_prepared) === 0); + + return false; + } + + function compare_uniformResourceIdentifier(name, constraint) + { + /// <summary>Compare two uniformResourceIdentifier values</summary> + /// <param name="name" type="String">uniformResourceIdentifier from name</param> + /// <param name="constraint" type="String">Constraint for uniformResourceIdentifier from name</param> + /// <returns type="Boolean">Boolean result - valid or invalid the "name" against the "constraint"</returns> + + // #region Make a "string preparation" for both name and constrain + var name_prepared = in_window.org.pkijs.stringPrep(name); + var constraint_prepared = in_window.org.pkijs.stringPrep(constraint); + // #endregion + + // #region Find out a major URI part to compare with + var ns = name_prepared.split("/"); + var cs = constraint_prepared.split("/"); + + if(cs.length > 1) // Malformed constraint + return false; + + if(ns.length > 1) // Full URI string + { + for(var i = 0; i < ns.length; i++) + { + if((ns[i].length > 0) && (ns[i].charAt(ns[i].length - 1) !== ':')) + { + var ns_port = ns[i].split(":"); + name_prepared = ns_port[0]; + break; + } + } + } + // #endregion + + var result = compare_dNSName(name_prepared, constraint_prepared); + + if(result) + { + // #region Make a "splitted" versions of "constraint" and "name" + var name_splitted = name_prepared.split("."); + var constraint_splitted = constraint_prepared.split("."); + // #endregion + + if(constraint_splitted[0].length === 0) + return true; + + return name_splitted.length === constraint_splitted.length; + } + else + return false; + + return false; + } + + function compare_iPAddress(name, constraint) + { + /// <summary>Compare two iPAddress values</summary> + /// <param name="name" type="in_window.org.pkijs.asn1.OCTETSTRING">iPAddress from name</param> + /// <param name="constraint" type="in_window.org.pkijs.asn1.OCTETSTRING">Constraint for iPAddress from name</param> + /// <returns type="Boolean">Boolean result - valid or invalid the "name" against the "constraint"</returns> + + // #region Common variables + var name_view = new Uint8Array(name.value_block.value_hex); + var constraint_view = new Uint8Array(constraint.value_block.value_hex); + // #endregion + + // #region Work with IPv4 addresses + if((name_view.length === 4) && (constraint_view.length === 8)) + { + for(var i = 0; i < 4; i++) + { + if((name_view[i] ^ constraint_view[i]) & constraint_view[i + 4]) + return false; + } + + return true; + } + // #endregion + + // #region Work with IPv6 addresses + if((name_view.length === 16) && (constraint_view.length === 32)) + { + for(var i = 0; i < 16; i++) + { + if((name_view[i] ^ constraint_view[i]) & constraint_view[i + 16]) + return false; + } + + return true; + } + // #endregion + + return false; + } + + function compare_directoryName(name, constraint) + { + /// <summary>Compare two directoryName values</summary> + /// <param name="name" type="in_window.org.pkijs.simpl.RDN">directoryName from name</param> + /// <param name="constraint" type="in_window.org.pkijs.simpl.RDN">Constraint for directoryName from name</param> + /// <param name="any" type="Boolean">Boolean flag - should be comparision interrupted after first match or we need to match all "constraints" parts</param> + /// <returns type="Boolean">Boolean result - valid or invalid the "name" against the "constraint"</returns> + + // #region Initial check + if((name.types_and_values.length === 0) || (constraint.types_and_values.length === 0)) + return true; + + if(name.types_and_values.length < constraint.types_and_values.length) + return false; + // #endregion + + // #region Initial variables + var result = true; + var name_start = 0; + // #endregion + + for(var i = 0; i < constraint.types_and_values.length; i++) + { + var local_result = false; + + for(var j = name_start; j < name.types_and_values.length; j++) + { + local_result = name.types_and_values[j].isEqual(constraint.types_and_values[i]); + + if(name.types_and_values[j].type === constraint.types_and_values[i].type) + result = result && local_result; + + if(local_result === true) + { + if((name_start === 0) || (name_start === j)) + { + name_start = j + 1; + break; + } + else // Structure of "name" must be the same with "constraint" + return false; + } + } + + if(local_result === false) + return false; + } + + return (name_start === 0) ? false : result; + } + // #endregion + + // #region Check a result from "policy checking" part + if(policy_result.result === false) + return policy_result; + // #endregion + + // #region Check all certificates, excluding "trust anchor" + path_depth = 1; + + for(var i = (_this.certs.length - 2) ; i >= 0 ; i--, path_depth++) + { + // #region Support variables + var subject_alt_names = new Array(); + + var cert_permitted_subtrees = new Array(); + var cert_excluded_subtrees = new Array(); + // #endregion + + if("extensions" in _this.certs[i]) + { + for(var j = 0; j < _this.certs[i].extensions.length; j++) + { + // #region NameConstraints + if(_this.certs[i].extensions[j].extnID === "2.5.29.30") + { + if("permittedSubtrees" in _this.certs[i].extensions[j].parsedValue) + cert_permitted_subtrees = cert_permitted_subtrees.concat(_this.certs[i].extensions[j].parsedValue.permittedSubtrees); + + if("excludedSubtrees" in _this.certs[i].extensions[j].parsedValue) + cert_excluded_subtrees = cert_excluded_subtrees.concat(_this.certs[i].extensions[j].parsedValue.excludedSubtrees); + } + // #endregion + + // #region SubjectAltName + if(_this.certs[i].extensions[j].extnID === "2.5.29.17") + subject_alt_names = subject_alt_names.concat(_this.certs[i].extensions[j].parsedValue.altNames); + // #endregion + } + } + + // #region Checking for "required name forms" + var form_found = (required_name_forms.length <= 0); + + for(var j = 0; j < required_name_forms.length; j++) + { + switch(required_name_forms[j].base.NameType) + { + case 4: // directoryName + { + if(required_name_forms[j].base.Name.types_and_values.length !== _this.certs[i].subject.types_and_values.length) + continue; + + form_found = true; + + for(var k = 0; k < _this.certs[i].subject.types_and_values.length; k++) + { + if(_this.certs[i].subject.types_and_values[k].type !== required_name_forms[j].base.Name.types_and_values[k].type) + { + form_found = false; + break; + } + } + + if(form_found === true) + break; + } + break; + default: // ??? Probably here we should reject the certificate ??? + } + } + + if(form_found === false) + { + policy_result.result = false; + policy_result.result_code = 21; + policy_result.result_message = "No neccessary name form found"; + + return new Promise(function(resolve, reject) + { + reject(policy_result); + }); + } + // #endregion + + // #region Checking for "permited sub-trees" + // #region Make groups for all types of constraints + var constr_groups = new Array(); // Array of array for groupped constraints + constr_groups[0] = new Array(); // rfc822Name + constr_groups[1] = new Array(); // dNSName + constr_groups[2] = new Array(); // directoryName + constr_groups[3] = new Array(); // uniformResourceIdentifier + constr_groups[4] = new Array(); // iPAddress + + for(var j = 0; j < permitted_subtrees.length; j++) + { + switch(permitted_subtrees[j].base.NameType) + { + // #region rfc822Name + case 1: + constr_groups[0].push(permitted_subtrees[j]); + break; + // #endregion + // #region dNSName + case 2: + constr_groups[1].push(permitted_subtrees[j]); + break; + // #endregion + // #region directoryName + case 4: + constr_groups[2].push(permitted_subtrees[j]); + break; + // #endregion + // #region uniformResourceIdentifier + case 6: + constr_groups[3].push(permitted_subtrees[j]); + break; + // #endregion + // #region iPAddress + case 7: + constr_groups[4].push(permitted_subtrees[j]); + break; + // #endregion + // #region default + + default: + // #endregion + } + } + // #endregion + + // #region Check name constraints groupped by type, one-by-one + for(var p = 0; p < 5; p++) + { + var group_permitted = false; + var valueExists = false; + var group = constr_groups[p]; + + for(var j = 0; j < group.length; j++) + { + switch(p) + { + // #region rfc822Name + case 0: + if(subject_alt_names.length > 0) + { + for(var k = 0; k < subject_alt_names.length; k++) + { + if(subject_alt_names[k].NameType === 1) // rfc822Name + { + valueExists = true; + group_permitted = group_permitted || compare_rfc822Name(subject_alt_names[k].Name, group[j].base.Name); + } + } + } + else // Try to find out "emailAddress" inside "subject" + { + for(var k = 0; k < _this.certs[i].subject.types_and_values.length; k++) + { + if((_this.certs[i].subject.types_and_values[k].type === "1.2.840.113549.1.9.1") || // PKCS#9 e-mail address + (_this.certs[i].subject.types_and_values[k].type === "0.9.2342.19200300.100.1.3")) // RFC1274 "rfc822Mailbox" e-mail address + { + valueExists = true; + group_permitted = group_permitted || compare_rfc822Name(_this.certs[i].subject.types_and_values[k].value.value_block.value, group[j].base.Name); + } + } + } + break; + // #endregion + // #region dNSName + case 1: + if(subject_alt_names.length > 0) + { + for(var k = 0; k < subject_alt_names.length; k++) + { + if(subject_alt_names[k].NameType === 2) // dNSName + { + valueExists = true; + group_permitted = group_permitted || compare_dNSName(subject_alt_names[k].Name, group[j].base.Name); + } + } + } + break; + // #endregion + // #region directoryName + case 2: + valueExists = true; + group_permitted = compare_directoryName(_this.certs[i].subject, group[j].base.Name); + break; + // #endregion + // #region uniformResourceIdentifier + case 3: + if(subject_alt_names.length > 0) + { + for(var k = 0; k < subject_alt_names.length; k++) + { + if(subject_alt_names[k].NameType === 6) // uniformResourceIdentifier + { + valueExists = true; + group_permitted = group_permitted || compare_uniformResourceIdentifier(subject_alt_names[k].Name, group[j].base.Name); + } + } + } + break; + // #endregion + // #region iPAddress + case 4: + if(subject_alt_names.length > 0) + { + for(var k = 0; k < subject_alt_names.length; k++) + { + if(subject_alt_names[k].NameType === 7) // iPAddress + { + valueExists = true; + group_permitted = group_permitted || compare_iPAddress(subject_alt_names[k].Name, group[j].base.Name); + } + } + } + break; + // #endregion + // #region default + + default: + // #endregion + } + + if(group_permitted) + break; + } + + if((group_permitted === false) && (group.length > 0) && valueExists) + { + policy_result.result = false; + policy_result.result_code = 41; + policy_result.result_message = "Failed to meet \"permitted sub-trees\" name constraint"; + + return new Promise(function(resolve, reject) + { + reject(policy_result); + }); + } + } + // #endregion + // #endregion + + // #region Checking for "excluded sub-trees" + var excluded = false; + + for(var j = 0; j < excluded_subtrees.length; j++) + { + switch(excluded_subtrees[j].base.NameType) + { + // #region rfc822Name + case 1: + if(subject_alt_names.length >= 0) + { + for(var k = 0; k < subject_alt_names.length; k++) + { + if(subject_alt_names[k].NameType === 1) // rfc822Name + excluded = excluded || compare_rfc822Name(subject_alt_names[k].Name, excluded_subtrees[j].base.Name); + } + } + else // Try to find out "emailAddress" inside "subject" + { + for(var k = 0; k < _this.subject.types_and_values.length; k++) + { + if((_this.subject.types_and_values[k].type === "1.2.840.113549.1.9.1") || // PKCS#9 e-mail address + (_this.subject.types_and_values[k].type === "0.9.2342.19200300.100.1.3")) // RFC1274 "rfc822Mailbox" e-mail address + { + excluded = excluded || compare_rfc822Name(_this.subject.types_and_values[k].value.value_block.value, excluded_subtrees[j].base.Name); + } + } + } + break; + // #endregion + // #region dNSName + case 2: + if(subject_alt_names.length > 0) + { + for(var k = 0; k < subject_alt_names.length; k++) + { + if(subject_alt_names[k].NameType === 2) // dNSName + excluded = excluded || compare_dNSName(subject_alt_names[k].Name, excluded_subtrees[j].base.Name); + } + } + break; + // #endregion + // #region directoryName + case 4: + excluded = excluded || compare_directoryName(_this.certs[i].subject, excluded_subtrees[j].base.Name); + break; + // #endregion + // #region uniformResourceIdentifier + case 6: + if(subject_alt_names.length > 0) + { + for(var k = 0; k < subject_alt_names.length; k++) + { + if(subject_alt_names[k].NameType === 6) // uniformResourceIdentifier + excluded = excluded || compare_uniformResourceIdentifier(subject_alt_names[k].Name, excluded_subtrees[j].base.Name); + } + } + break; + // #endregion + // #region iPAddress + case 7: + if(subject_alt_names.length > 0) + { + for(var k = 0; k < subject_alt_names.length; k++) + { + if(subject_alt_names[k].NameType === 7) // iPAddress + excluded = excluded || compare_iPAddress(subject_alt_names[k].Name, excluded_subtrees[j].base.Name); + } + } + break; + // #endregion + // #region default + + default: // No action, but probably here we need to create a warning for "malformed constraint" + // #endregion + } + + if(excluded) + break; + } + + if(excluded === true) + { + policy_result.result = false; + policy_result.result_code = 42; + policy_result.result_message = "Failed to meet \"excluded sub-trees\" name constraint"; + + return new Promise(function(resolve, reject) + { + reject(policy_result); + }); + } + // #endregion + + // #region Append "cert_..._subtrees" to "..._subtrees" + permitted_subtrees = permitted_subtrees.concat(cert_permitted_subtrees); + excluded_subtrees = excluded_subtrees.concat(cert_excluded_subtrees); + // #endregion + } + // #endregion + + return policy_result; + } + ); + // #endregion + + return sequence; + }; + //************************************************************************************** + // #endregion + //************************************************************************************** +} +)(typeof exports !== "undefined" ? exports : window); diff --git a/dom/webauthn/tests/test_webauthn_abort_signal.html b/dom/webauthn/tests/test_webauthn_abort_signal.html new file mode 100644 index 0000000000..eb9f43333a --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_abort_signal.html @@ -0,0 +1,146 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for aborting W3C Web Authentication request</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>Test for aborting W3C Web Authentication request</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1415675">Mozilla Bug 1415675</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectAbortError(aResult) { + is(aResult.code, DOMException.ABORT_ERR, "Expecting an AbortError"); + } + + add_task(async () => { + // Enable USB tokens. + await SpecialPowers.pushPrefEnv({"set": [ + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ]}); + }); + + // Start a new MakeCredential() request. + function requestMakeCredential(signal) { + let publicKey = { + rp: {id: document.domain, name: "none"}, + user: {id: new Uint8Array(), name: "none", displayName: "none"}, + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + }; + + return navigator.credentials.create({publicKey, signal}); + } + + // Start a new GetAssertion() request. + async function requestGetAssertion(signal) { + let newCredential = { + type: "public-key", + id: crypto.getRandomValues(new Uint8Array(16)), + transports: ["usb"], + }; + + let publicKey = { + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: document.domain, + allowCredentials: [newCredential] + }; + + // Start the request, handle failures only. + return navigator.credentials.get({publicKey, signal}); + } + + // Create an AbortController and abort immediately. + add_task(async function test_create_abortcontroller_and_abort() { + let ctrl = new AbortController(); + ctrl.abort(); + + // The event shouldn't fire. + ctrl.signal.onabort = arrivingHereIsBad; + + // MakeCredential() should abort immediately. + await requestMakeCredential(ctrl.signal) + .then(arrivingHereIsBad) + .catch(expectAbortError); + + // GetAssertion() should abort immediately. + await requestGetAssertion(ctrl.signal) + .then(arrivingHereIsBad) + .catch(expectAbortError); + }); + + // Request a new credential and abort the request. + add_task(async function test_request_credential_and_abort() { + let aborted = false; + let ctrl = new AbortController(); + + ctrl.signal.onabort = () => { + ok(!aborted, "abort event fired once"); + aborted = true; + }; + + // Request a new credential. + let request = requestMakeCredential(ctrl.signal) + .then(arrivingHereIsBad) + .catch(err => { + ok(aborted, "abort event was fired"); + expectAbortError(err); + }); + + // Wait a tick for the statemachine to start. + await Promise.resolve(); + + // Abort the request. + ok(!aborted, "request still pending"); + ctrl.abort(); + ok(aborted, "request aborted"); + + // Wait for the request to terminate. + await request; + }); + + // Request a new assertion and abort the request. + add_task(async function test_request_assertion_and_abort() { + let aborted = false; + let ctrl = new AbortController(); + + ctrl.signal.onabort = () => { + ok(!aborted, "abort event fired once"); + aborted = true; + }; + + // Request a new assertion. + let request = requestGetAssertion(ctrl.signal) + .then(arrivingHereIsBad) + .catch(err => { + ok(aborted, "abort event was fired"); + expectAbortError(err); + }); + + // Wait a tick for the statemachine to start. + await Promise.resolve(); + + // Abort the request. + ok(!aborted, "request still pending"); + ctrl.abort(); + ok(aborted, "request aborted"); + + // Wait for the request to terminate. + await request; + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_attestation_conveyance.html b/dom/webauthn/tests/test_webauthn_attestation_conveyance.html new file mode 100644 index 0000000000..061e2c2624 --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_attestation_conveyance.html @@ -0,0 +1,103 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>W3C Web Authentication - Attestation Conveyance</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="u2futil.js"></script> + <script type="text/javascript" src="pkijs/common.js"></script> + <script type="text/javascript" src="pkijs/asn1.js"></script> + <script type="text/javascript" src="pkijs/x509_schema.js"></script> + <script type="text/javascript" src="pkijs/x509_simpl.js"></script> + <script type="text/javascript" src="cbor.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>W3C Web Authentication - Attestation Conveyance</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1428916">Mozilla Bug 1428916</a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1416056">Mozilla Bug 1416056</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + add_task(async () => { + await SpecialPowers.pushPrefEnv({"set": [ + ["security.webauth.webauthn_testing_allow_direct_attestation", true], + ]}); + await addVirtualAuthenticator(); + }); + + function verifyAnonymizedCertificate(aResult) { + return webAuthnDecodeCBORAttestation(aResult.response.attestationObject) + .then(({fmt, attStmt}) => { + is(fmt, "none", "Is a None Attestation"); + is(typeof(attStmt), "object", "attStmt is a map"); + is(Object.keys(attStmt).length, 0, "attStmt is empty"); + }); + } + + async function verifyDirectCertificate(aResult) { + let clientDataHash = await crypto.subtle.digest("SHA-256", aResult.response.clientDataJSON) + .then(digest => new Uint8Array(digest)); + let {fmt, attStmt, authData, authDataObj} = await webAuthnDecodeCBORAttestation(aResult.response.attestationObject); + is(fmt, "packed", "Is a Packed Attestation"); + let signedData = new Uint8Array(authData.length + clientDataHash.length); + signedData.set(authData); + signedData.set(clientDataHash, authData.length); + let valid = await verifySignature(authDataObj.publicKeyHandle, signedData, new Uint8Array(attStmt.sig)); + ok(valid, "Signature is valid."); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectTypeError(aResult) { + ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError, got " + aResult); + } + + // Start a new MakeCredential() request. + function requestMakeCredential(attestation) { + let publicKey = { + rp: {id: document.domain, name: "none"}, + user: {id: new Uint8Array(), name: "none", displayName: "none"}, + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + attestation, + }; + + return navigator.credentials.create({publicKey}); + } + + // Test success cases for make credential. + add_task(async function test_make_credential_success () { + // No selection criteria should be equal to none, which means anonymized + await requestMakeCredential() + .then(verifyAnonymizedCertificate) + .catch(arrivingHereIsBad); + + // Request an unknown attestation type. This should be treated as "none". + await requestMakeCredential("unknown") + .then(verifyAnonymizedCertificate) + .catch(arrivingHereIsBad); + + // Request no attestation. + await requestMakeCredential("none") + .then(verifyAnonymizedCertificate) + .catch(arrivingHereIsBad); + + // Request indirect attestation, which is the same as direct. + await requestMakeCredential("indirect") + .then(verifyDirectCertificate) + .catch(arrivingHereIsBad); + + // Request direct attestation, which will prompt for user intervention. + await requestMakeCredential("direct") + .then(verifyDirectCertificate) + .catch(arrivingHereIsBad); + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_authenticator_selection.html b/dom/webauthn/tests/test_webauthn_authenticator_selection.html new file mode 100644 index 0000000000..35667493a7 --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_authenticator_selection.html @@ -0,0 +1,152 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>W3C Web Authentication - Authenticator Selection Criteria</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>W3C Web Authentication - Authenticator Selection Criteria</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1406462">Mozilla Bug 1406462</a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1406467">Mozilla Bug 1406467</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + add_task(async () => { + await addVirtualAuthenticator(); + }); + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectNotAllowedError(aResult) { + ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError, got " + aResult); + } + + // We store the credential of the first successful make credential + // operation so we can use it for get assertion tests later. + let gCredential; + + // Start a new MakeCredential() request. + function requestMakeCredential(authenticatorSelection) { + let publicKey = { + rp: {id: document.domain, name: "none"}, + user: {id: new Uint8Array(), name: "none", displayName: "none"}, + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + authenticatorSelection, + }; + + return navigator.credentials.create({publicKey}); + } + + // Start a new GetAssertion() request. + function requestGetAssertion(userVerification) { + let newCredential = { + type: "public-key", + id: gCredential, + transports: ["usb"], + }; + + let publicKey = { + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: document.domain, + allowCredentials: [newCredential] + }; + + if (userVerification) { + publicKey.userVerification = userVerification; + } + + return navigator.credentials.get({publicKey}); + } + + // Test success cases for make credential. + add_task(async function test_make_credential_successes() { + // No selection criteria. + await requestMakeCredential({}) + // Save the credential so we can use it for sign success tests. + .then(res => gCredential = res.rawId) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Request a cross-platform authenticator. + await requestMakeCredential({authenticatorAttachment: "cross-platform"}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Require a resident key. + await requestMakeCredential({requireResidentKey: true}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Don't require a resident key. + await requestMakeCredential({requireResidentKey: false}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Require user verification. + await requestMakeCredential({userVerification: "required"}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Prefer user verification. + await requestMakeCredential({userVerification: "preferred"}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Discourage user verification. + await requestMakeCredential({userVerification: "discouraged"}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + }); + + // Test success cases for get assertion. + add_task(async function test_get_assertion_successes() { + // No selection criteria. + await requestGetAssertion() + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Require user verification. + await requestGetAssertion("required") + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Prefer user verification. + await requestGetAssertion("preferred") + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Discourage user verification. + await requestGetAssertion("discouraged") + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + }); + + // Test failure cases for make credential. + add_task(async function test_make_credential_failures() { + // Request a platform authenticator. + await requestMakeCredential({authenticatorAttachment: "platform"}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + }); + + // Test failure cases for get assertion. + add_task(async function test_get_assertion_failures() { + // No failures currently tested + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_authenticator_transports.html b/dom/webauthn/tests/test_webauthn_authenticator_transports.html new file mode 100644 index 0000000000..0d6e6d3055 --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_authenticator_transports.html @@ -0,0 +1,165 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>W3C Web Authentication - Authenticator Transports</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>W3C Web Authentication - Authenticator Transports</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1406467">Mozilla Bug 1406467</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + add_task(async () => { + await addVirtualAuthenticator(); + }); + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectNotAllowedError(aResult) { + ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError, got " + aResult); + } + + function expectInvalidStateError(aResult) { + ok(aResult.toString().startsWith("InvalidStateError"), "Expecting a InvalidStateError, got " + aResult); + } + + // Store the credential and transports of the first successful make credential + // operation so we can use them to get assertions later. + let gCredential; + let gTransports; + + // Start a new MakeCredential() request. + function requestMakeCredential(excludeCredentials) { + let publicKey = { + rp: {id: document.domain, name: "none"}, + user: {id: new Uint8Array(), name: "none", displayName: "none"}, + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + excludeCredentials + }; + + return navigator.credentials.create({publicKey}); + } + + // Start a new GetAssertion() request. + function requestGetAssertion(allowCredentials) { + let publicKey = { + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: document.domain, + allowCredentials + }; + + return navigator.credentials.get({publicKey}); + } + + // Test make credential behavior. + add_task(async function test_make_credential() { + // Make a credential. + await requestMakeCredential([]) + // Save the credential for later. + .then(res => { + gCredential = res.rawId; + gTransports = res.response.getTransports(); + }) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // gTransports should be "a sequence of zero or more unique DOMStrings in lexicographical order." + for (let i = 0; i < gTransports.length - 1; i++) { + if (gTransports[i] >= gTransports[i+1]) { + ok(false, "getTransports() should return a sorted list"); + } + } + + // Pass a random credential to exclude. + await requestMakeCredential([{ + type: "public-key", + id: crypto.getRandomValues(new Uint8Array(16)), + transports: gTransports, + }]).then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Pass gCredential with transport=usb. + // The credential already exists, and the softoken consents to create, + // so the error is InvalidState and not NotAllowed. + await requestMakeCredential([{ + type: "public-key", + id: gCredential, + transports: ["usb"], + }]).then(arrivingHereIsBad) + .catch(expectInvalidStateError); + + // Pass gCredential with transport=nfc. + // The softoken pretends to support all transports. + // Also, as above, the credential exists and the token indicates consent. + await requestMakeCredential([{ + type: "public-key", + id: gCredential, + transports: ["nfc"], + }]).then(arrivingHereIsBad) + .catch(expectInvalidStateError); + + // Pass gCredential with an empty transports list. + // As above, the token indicates consent, so expect InvalidStateError. + await requestMakeCredential([{ + type: "public-key", + id: gCredential, + transports: [], + }]).then(arrivingHereIsBad) + .catch(expectInvalidStateError); + }); + + // Test get assertion behavior. + add_task(async function test_get_assertion() { + // Request an assertion for gCredential. + await requestGetAssertion([{ + type: "public-key", + id: gCredential, + transports: gTransports, + }]).then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Request an assertion for a random credential. This should be + // indistinguishable from the user denying consent for a known + // credential, so expect a NotAllowedError. + await requestGetAssertion([{ + type: "public-key", + id: crypto.getRandomValues(new Uint8Array(16)), + transports: ["usb"], + }]).then(arrivingHereIsBad) + .catch(expectNotAllowedError); + + // Request an assertion for gCredential with transport=nfc. + // The softoken pretends to support all transports. + await requestGetAssertion([{ + type: "public-key", + id: gCredential, + transports: ["nfc"], + }]).then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Request an assertion for gCredential with an empty transports list. + await requestGetAssertion([{ + type: "public-key", + id: gCredential, + transports: [], + }]).then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_crossorigin_featurepolicy.html b/dom/webauthn/tests/test_webauthn_crossorigin_featurepolicy.html new file mode 100644 index 0000000000..6e092d02ba --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_crossorigin_featurepolicy.html @@ -0,0 +1,258 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Tests for Publickey-Credentials-Get Feature Policy for W3C Web Authentication</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 for Publickey-Credentials-Get Feature Policy for W3C Web Authentication</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1460986">Mozilla Bug 1460986</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + var gAuthenticatorId; + var gSameCredId; + var gCrossCredId; + + const CROSS_DOMAIN = "example.org"; + + function compare(a, b) { + if (a.length != b.length) return false; + for (let i = 0; i < a.length; i += 1) { + if (a[i] !== b[i]) return false; + } + return true; + } + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectNotAllowedError(aResult) { + ok(aResult == "NotAllowedError", "Expecting a NotAllowedError, got " + aResult); + } + + function expectSameCredId(aResult) { + ok(compare(aResult, gSameCredId), "Expecting credential for " + document.domain); + } + + function expectCrossCredId(aResult) { + ok(compare(aResult, gCrossCredId), "Expecting credential for " + CROSS_DOMAIN); + } + + function getAssertion(id) { + let chall = new Uint8Array(16); + this.content.window.crypto.getRandomValues(chall); + + let options = { + challenge: chall, + allowCredentials: [ { type: "public-key", id } ], + }; + + return this.content.window.navigator.credentials.get({publicKey: options}) + .then(res => Promise.resolve(new Uint8Array(res.rawId))) + .catch(e => Promise.reject(e.name)); + } + + function createCredential() { + this.content.document.notifyUserGestureActivation(); + + const cose_alg_ECDSA_w_SHA256 = -7; + let publicKey = { + rp: {id: this.content.window.document.domain, name: "none"}, + user: {id: new Uint8Array(), name: "none", displayName: "none"}, + challenge: this.content.window.crypto.getRandomValues(new Uint8Array(16)), + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + }; + + return this.content.window.navigator.credentials.create({publicKey}) + .then(res => Promise.resolve(new Uint8Array(res.rawId))) + .catch(e => Promise.reject(e.name)); + } + + async function setup(preloadSame, preloadCross) { + if (!gAuthenticatorId) { + gAuthenticatorId = await addVirtualAuthenticator(); + } + if (gSameCredId) { + removeCredential(gAuthenticatorId, bytesToBase64UrlSafe(gSameCredId)); + gSameCredId = undefined; + } + if (gCrossCredId) { + removeCredential(gAuthenticatorId, bytesToBase64UrlSafe(gCrossCredId)); + gCrossCredId = undefined; + } + if (preloadSame) { + gSameCredId = await addCredential(gAuthenticatorId, document.domain).then(id => base64ToBytesUrlSafe(id)); + } + if (preloadCross) { + gCrossCredId = await addCredential(gAuthenticatorId, CROSS_DOMAIN).then(id => base64ToBytesUrlSafe(id)); + } + } + + add_task(async function test_same_origin_iframe_allow() { + // Don't preload any credentials. We'll try to create one in content. + await setup(false, false); + + let iframe = document.createElement("iframe"); + iframe.setAttribute("src", "https://" + document.domain + "/tests/dom/webauthn/tests/empty.html"); + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", resolve, {once: true})); + + ok("featurePolicy" in iframe, "we have iframe.featurePolicy"); + ok(iframe.featurePolicy.allowsFeature("publickey-credentials-create"), "iframe allows publickey-credentials-create"); + ok(iframe.featurePolicy.allowsFeature("publickey-credentials-get"), "iframe allows publickey-credentials-get"); + + // We should be able to create a credential in a same-origin iframe by default. + is(gSameCredId, undefined); + gSameCredId = new Uint8Array(await SpecialPowers.spawn(iframe, [], createCredential)); + + // We should be able to assert a credential in a same-origin iframe by default. + await SpecialPowers.spawn(iframe, [gSameCredId], getAssertion) + .then(expectSameCredId) + .catch(expectNotAllowedError); + }); + + add_task(async function test_same_origin_iframe_deny() { + // Preload same-origin credential to ensure we cannot assert it. + await setup(true, false); + + let iframe = document.createElement("iframe"); + iframe.setAttribute("src", "https://" + document.domain + "/tests/dom/webauthn/tests/empty.html"); + iframe.setAttribute("allow", "publickey-credentials-create 'none'; publickey-credentials-get 'none'"); + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", resolve, {once: true})); + + ok("featurePolicy" in iframe, "we have iframe.featurePolicy"); + ok(!iframe.featurePolicy.allowsFeature("publickey-credentials-create"), "iframe does not allow publickey-credentials-create"); + ok(!iframe.featurePolicy.allowsFeature("publickey-credentials-get"), "iframe does not allow publickey-credentials-get"); + + // We should not be able to create a credential in a same-origin iframe if + // the iframe does not allow publickey-credentials-create. + await SpecialPowers.spawn(iframe, [], createCredential) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + + // We should not be able to assert a credential in a same-origin iframe if + // the iframe does not allow publickey-credentials-get. + await SpecialPowers.spawn(iframe, [gSameCredId], getAssertion) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + }); + + add_task(async function test_cross_origin_iframe_allow() { + // Don't preload any credentials. We'll try to create one in content. + await setup(false, false); + + let iframe = document.createElement("iframe"); + iframe.setAttribute("src", "https://" + CROSS_DOMAIN + "/tests/dom/webauthn/tests/empty.html"); + iframe.setAttribute("allow", "publickey-credentials-create https://" + CROSS_DOMAIN + "; publickey-credentials-get https://" + CROSS_DOMAIN); + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", resolve, {once: true})); + + ok("featurePolicy" in iframe, "we have iframe.featurePolicy"); + ok(iframe.featurePolicy.allowsFeature("publickey-credentials-create"), "iframe allows publickey-credentials-create"); + ok(iframe.featurePolicy.allowsFeature("publickey-credentials-get"), "iframe allows publickey-credentials-get"); + + // We should be able to create a credential in a same-origin iframe if + // the iframe allows publickey-credentials-create. + is(gCrossCredId, undefined); + gCrossCredId = new Uint8Array(await SpecialPowers.spawn(iframe, [], createCredential)); + + // We should be able to assert a credential in a cross-origin iframe if + // the iframe allows publickey-credentials-get. + await SpecialPowers.spawn(iframe, [gCrossCredId], getAssertion) + .then(expectCrossCredId) + .catch(arrivingHereIsBad); + }); + + add_task(async function test_cross_origin_iframe_deny() { + // Preload cross-origin credential to ensure we cannot assert it. + await setup(false, true); + + let iframe = document.createElement("iframe"); + iframe.setAttribute("src", "https://" + CROSS_DOMAIN + "/tests/dom/webauthn/tests/empty.html"); + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", resolve, {once: true})); + + ok("featurePolicy" in iframe, "we have iframe.featurePolicy"); + ok(!iframe.featurePolicy.allowsFeature("publickey-credentials-create"), "iframe does not allow publickey-credentials-create"); + ok(!iframe.featurePolicy.allowsFeature("publickey-credentials-get"), "iframe does not allow publickey-credentials-get"); + + // We should not be able to create a credential in a cross-origin iframe if + // the iframe does not allow publickey-credentials-create. + await SpecialPowers.spawn(iframe, [], createCredential) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + + // We should not be able to assert a credential in a cross-origin iframe if + // the iframe does not allow publickey-credentials-get. + await SpecialPowers.spawn(iframe, [gCrossCredId], getAssertion) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + }); + + add_task(async function test_cross_origin_iframe_create_but_not_get() { + // Don't preload any credentials. We'll try to create one in content. + await setup(false, false); + + let iframe = document.createElement("iframe"); + iframe.setAttribute("src", "https://" + CROSS_DOMAIN + "/tests/dom/webauthn/tests/empty.html"); + iframe.setAttribute("allow", "publickey-credentials-create https://" + CROSS_DOMAIN); + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", resolve, {once: true})); + + ok("featurePolicy" in iframe, "we have iframe.featurePolicy"); + ok(iframe.featurePolicy.allowsFeature("publickey-credentials-create"), "iframe allows publickey-credentials-create"); + ok(!iframe.featurePolicy.allowsFeature("publickey-credentials-get"), "iframe does not allow publickey-credentials-get"); + + // We should be able to create a credential in a cross-origin iframe if + // the iframe allows publickey-credentials-create. + is(gCrossCredId, undefined); + gCrossCredId = new Uint8Array(await SpecialPowers.spawn(iframe, [], createCredential)); + + // We should not be able to assert a credential in a cross-origin iframe if + // the iframe does not allow publickey-credentials-get. + await SpecialPowers.spawn(iframe, [gCrossCredId], getAssertion) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + }); + + add_task(async function test_cross_origin_iframe_get_but_not_create() { + // Preload cross-origin credential so we can assert it. + await setup(false, true); + + let iframe = document.createElement("iframe"); + iframe.setAttribute("src", "https://" + CROSS_DOMAIN + "/tests/dom/webauthn/tests/empty.html"); + iframe.setAttribute("allow", "publickey-credentials-get https://" + CROSS_DOMAIN); + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", resolve, {once: true})); + + ok("featurePolicy" in iframe, "we have iframe.featurePolicy"); + ok(!iframe.featurePolicy.allowsFeature("publickey-credentials-create"), "iframe does not publickey-credentials-create"); + ok(iframe.featurePolicy.allowsFeature("publickey-credentials-get"), "iframe allows publickey-credentials-get"); + + // We should not be able to create a credential in a cross-origin iframe if + // the iframe does not allow publickey-credentials-create. + await SpecialPowers.spawn(iframe, [], createCredential) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + + // We should not be able to assert a credential in a cross-origin iframe if + // the iframe does not allow publickey-credentials-get. + await SpecialPowers.spawn(iframe, [gCrossCredId], getAssertion) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_ctap2_omitted_credential_id.html b/dom/webauthn/tests/test_webauthn_ctap2_omitted_credential_id.html new file mode 100644 index 0000000000..a2c14eb91d --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_ctap2_omitted_credential_id.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Tests for omitted credential ID in a CTAP 2.0 authenticator response</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 for omitted credential ID in a CTAP 2.0 authenticator response</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1864504">Mozilla Bug 1864504</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + add_task(async () => { + // CTAP 2.0 allows GetAssertion responses to omit a credential + // ID if the allowlist has length one. This can cause problems in + // MakeCredential as well because GetAssertion is used for pre-flighting. + await addVirtualAuthenticator("ctap2"); + }); + + let validCred = null; + + add_task(test_setup_valid_credential); + add_task(test_create_with_one_excluded_credential); + add_task(test_get_with_one_allowed_credential); + + async function test_setup_valid_credential() { + let publicKey = { + rp: {id: document.domain, name: "none"}, + user: {id: new Uint8Array(), name: "none", displayName: "none"}, + challenge: crypto.getRandomValues(new Uint8Array(16)), + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + }; + + let res = await navigator.credentials.create({publicKey}); + validCred = {type: "public-key", id: res.rawId}; + } + + async function test_create_with_one_excluded_credential() { + let publicKey = { + rp: {id: document.domain, name: "none"}, + user: {id: new Uint8Array(), name: "none", displayName: "none"}, + challenge: crypto.getRandomValues(new Uint8Array(16)), + excludeList: [validCred], + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + }; + + await navigator.credentials.create({publicKey}); + ok(true, "create should not throw"); + } + + async function test_get_with_one_allowed_credential() { + let publicKey = { + challenge: crypto.getRandomValues(new Uint8Array(16)), + allowCredentials: [validCred] + }; + + await navigator.credentials.get({publicKey}); + ok(true, "get should not throw"); + } + + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_get_assertion.html b/dom/webauthn/tests/test_webauthn_get_assertion.html new file mode 100644 index 0000000000..3de282a09a --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_get_assertion.html @@ -0,0 +1,212 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Tests for GetAssertion for W3C Web Authentication</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="u2futil.js"></script> + <script type="text/javascript" src="pkijs/common.js"></script> + <script type="text/javascript" src="pkijs/asn1.js"></script> + <script type="text/javascript" src="pkijs/x509_schema.js"></script> + <script type="text/javascript" src="pkijs/x509_simpl.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>Tests for GetAssertion for W3C Web Authentication</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + add_task(async () => { + await addVirtualAuthenticator(); + }); + + is(navigator.authentication, undefined, "navigator.authentication does not exist any longer"); + isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); + isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); + isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + + let gAssertionChallenge = new Uint8Array(16); + window.crypto.getRandomValues(gAssertionChallenge); + + let unknownCredType = {type: "Magic", id: base64ToBytes("AAA=")}; + let unknownCred = {type: "public-key", id: base64ToBytes("AAA=")}; + let validCred = null; + + add_task(test_setup_valid_credential); + add_task(test_with_credential); + add_task(test_unexpected_option); + add_task(test_unexpected_option_with_credential); + add_task(test_unexpected_transport); + add_task(test_unknown_credential_type); + add_task(test_unknown_credential); + add_task(test_too_many_credentials); + add_task(test_unexpected_option_unknown_credential_type); + add_task(test_empty_credential_list); + + function requestGetAssertion(params) { + return navigator.credentials.get(params); + } + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectNotAllowedError(aResult) { + ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError, got " + aResult); + } + + function expectInvalidStateError(aResult) { + ok(aResult.toString().startsWith("InvalidStateError"), "Expecting a InvalidStateError, got " + aResult); + } + + function expectTypeError(aResult) { + ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError, got " + aResult); + } + + function expectSecurityError(aResult) { + ok(aResult.toString().startsWith("SecurityError"), "Expecting a SecurityError, got " + aResult); + } + + function expectAbortError(aResult) { + is(aResult.code, DOMException.ABORT_ERR, "Expecting an AbortError"); + } + + // Set up a valid credential + async function test_setup_valid_credential() { + let publicKey = { + rp: {id: document.domain, name: "none"}, + user: {id: new Uint8Array(), name: "none", displayName: "none"}, + challenge: crypto.getRandomValues(new Uint8Array(16)), + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + }; + + return navigator.credentials.create({publicKey}) + .then(res => validCred = {type: "public-key", id: res.rawId} ); + } + + // Test with a valid credential + async function test_with_credential() { + let publicKey = { + challenge: gAssertionChallenge, + allowCredentials: [validCred] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test with an unexpected option. That won't stop anything, and we'll + // fail with NotAllowed just as if we had no valid credentials -- which + // we don't. + async function test_unexpected_option() { + let publicKey = { + challenge: gAssertionChallenge, + unknownValue: "hi" + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + } + + // Test with an unexpected option but a valid credential + async function test_unexpected_option_with_credential() { + let publicKey = { + challenge: gAssertionChallenge, + unknownValue: "hi", + allowCredentials: [validCred] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test with an unexpected transport on a valid credential + async function test_unexpected_transport() { + let cred = validCred; + cred.transports = ["unknown", "usb"]; + + let publicKey = { + challenge: gAssertionChallenge, + unknownValue: "hi", + allowCredentials: [cred] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test with an unknown credential type + async function test_unknown_credential_type() { + let publicKey = { + challenge: gAssertionChallenge, + allowCredentials: [unknownCredType] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + } + + // Test with an unknown credential + async function test_unknown_credential() { + let publicKey = { + challenge: gAssertionChallenge, + allowCredentials: [unknownCred] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + } + + // Test with too many credentials + async function test_too_many_credentials() { + let tooManyCredentials = Array(21).fill(validCred); + let publicKey = { + challenge: gAssertionChallenge, + allowCredentials: tooManyCredentials, + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + + // Test with an unexpected option and an unknown credential type + async function test_unexpected_option_unknown_credential_type() { + let publicKey = { + challenge: gAssertionChallenge, + unknownValue: "hi", + allowCredentials: [unknownCredType] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + } + + // Test with an empty credential list + async function test_empty_credential_list() { + let publicKey = { + challenge: gAssertionChallenge, + allowCredentials: [] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + } + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_get_assertion_dead_object.html b/dom/webauthn/tests/test_webauthn_get_assertion_dead_object.html new file mode 100644 index 0000000000..35892a60d1 --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_get_assertion_dead_object.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for GetAssertion on dead object</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>Test for GetAssertion on dead object</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1483905">Mozilla Bug 1483905</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout( + "Due to the nature of this test, there's no way for the window we're opening to signal " + + "that it's done (the `document.writeln('')` is essential and basically clears any state " + + "we could use). So, we have to wait at least 15 seconds for the webauthn call to time out."); + SpecialPowers.pushPrefEnv({"set": [ + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ]}).then(() => { + let win = window.open("https://example.com/tests/dom/webauthn/tests/get_assertion_dead_object.html"); + setTimeout(() => { + win.close(); + SimpleTest.finish(); + }, 20000); + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_isplatformauthenticatoravailable.html b/dom/webauthn/tests/test_webauthn_isplatformauthenticatoravailable.html new file mode 100644 index 0000000000..15b83659d5 --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_isplatformauthenticatoravailable.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for W3C Web Authentication isUserVerifyingPlatformAuthenticatorAvailable</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="u2futil.js"></script> + <script type="text/javascript" src="pkijs/common.js"></script> + <script type="text/javascript" src="pkijs/asn1.js"></script> + <script type="text/javascript" src="pkijs/x509_schema.js"></script> + <script type="text/javascript" src="pkijs/x509_simpl.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<h1>Test for W3C Web Authentication isUserVerifyingPlatformAuthenticatorAvailable</h1> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + +<script class="testbody" type="text/javascript"> +"use strict"; + +add_task(async function test_uvpaa_with_no_authenticator() { + let uvpaa = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); + ok(uvpaa === false, "Platform authenticator is not available"); +}); + +add_task(async () => { + await addVirtualAuthenticator("ctap2_1", "usb"); +}); + +add_task(async function test_uvpaa_with_usb_authenticator() { + let uvpaa = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); + ok(uvpaa === false, "Platform authenticator is not available"); +}); + +add_task(async () => { + await addVirtualAuthenticator("ctap2_1", "internal"); +}); + +add_task(async function test_uvpaa_with_usb_and_platform_authenticator() { + let uvpaa = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); + ok(uvpaa === true, "Platform authenticator is available"); +}); + + +</script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_loopback.html b/dom/webauthn/tests/test_webauthn_loopback.html new file mode 100644 index 0000000000..70905739a0 --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_loopback.html @@ -0,0 +1,179 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="u2futil.js"></script> + <script type="text/javascript" src="pkijs/common.js"></script> + <script type="text/javascript" src="pkijs/asn1.js"></script> + <script type="text/javascript" src="pkijs/x509_schema.js"></script> + <script type="text/javascript" src="pkijs/x509_simpl.js"></script> + <script type="text/javascript" src="cbor.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<h1>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</h1> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + +<script class="testbody" type="text/javascript"> +"use strict"; + +add_task(async function() { + // This test intentionally compares items to themselves. + /* eslint-disable no-self-compare */ + await SpecialPowers.pushPrefEnv({"set": [["security.webauth.webauthn_testing_allow_direct_attestation", true]]}); + await addVirtualAuthenticator(); + is(navigator.authentication, undefined, "navigator.authentication does not exist any longer"); + isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); + isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); + isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + + let credm = navigator.credentials; + + let gCredentialChallenge = new Uint8Array(16); + window.crypto.getRandomValues(gCredentialChallenge); + let gAssertionChallenge = new Uint8Array(16); + window.crypto.getRandomValues(gAssertionChallenge); + + await testMakeCredential(); + + async function decodeCreatedCredential(aCredInfo) { + /* PublicKeyCredential : Credential + - rawId: Key Handle buffer pulled from U2F Register() Response + - id: Key Handle buffer in base64url form, should == rawId + - type: Literal 'public-key' + - response : AuthenticatorAttestationResponse : AuthenticatorResponse + - attestationObject: CBOR object + - clientDataJSON: serialized JSON + */ + + is(aCredInfo.type, "public-key", "Credential type must be public-key") + + ok(aCredInfo.rawId.byteLength > 0, "Key ID exists"); + is(aCredInfo.id, bytesToBase64UrlSafe(aCredInfo.rawId), "Encoded Key ID and Raw Key ID match"); + + ok(aCredInfo.rawId === aCredInfo.rawId, "PublicKeyCredential.RawID is SameObject"); + ok(aCredInfo.response === aCredInfo.response, "PublicKeyCredential.Response is SameObject"); + ok(aCredInfo.response.clientDataJSON === aCredInfo.response.clientDataJSON, "PublicKeyCredential.Response.ClientDataJSON is SameObject"); + ok(aCredInfo.response.attestationObject === aCredInfo.response.attestationObject, "PublicKeyCredential.Response.AttestationObject is SameObject"); + + let clientData = JSON.parse(buffer2string(aCredInfo.response.clientDataJSON)); + is(clientData.challenge, bytesToBase64UrlSafe(gCredentialChallenge), "Challenge is correct"); + is(clientData.origin, window.location.origin, "Origin is correct"); + is(clientData.type, "webauthn.create", "Type is correct"); + + let attestationObj = await webAuthnDecodeCBORAttestation(aCredInfo.response.attestationObject); + // Make sure the RP ID hash matches what we calculate. + let calculatedRpIdHash = await crypto.subtle.digest("SHA-256", string2buffer(document.domain)); + let calcHashStr = bytesToBase64UrlSafe(new Uint8Array(calculatedRpIdHash)); + let providedHashStr = bytesToBase64UrlSafe(new Uint8Array(attestationObj.authDataObj.rpIdHash)); + is(calcHashStr, providedHashStr, "Calculated RP ID hash must match what the browser derived."); + ok(true, attestationObj.authDataObj.flags[0] & (flag_TUP | flag_AT)); + ok(attestationObj.authDataObj.flags[0] & (flag_TUP | flag_AT) == (flag_TUP | flag_AT), + "User presence and Attestation Object flags must be set"); + aCredInfo.clientDataObj = clientData; + aCredInfo.publicKeyHandle = attestationObj.authDataObj.publicKeyHandle; + aCredInfo.attestationObject = attestationObj.authDataObj.attestationAuthData; + return aCredInfo; + } + + async function checkAssertionAndSigValid(aPublicKey, aAssertion) { + /* PublicKeyCredential : Credential + - rawId: ID of Credential from AllowList that succeeded + - id: Key Handle buffer in base64url form, should == rawId + - type: Literal 'public-key' + - response : AuthenticatorAssertionResponse : AuthenticatorResponse + - clientDataJSON: serialized JSON + - authenticatorData: RP ID Hash || U2F Sign() Response + - signature: U2F Sign() Response + */ + is(aAssertion.type, "public-key", "Credential type must be public-key") + ok(aAssertion.rawId.byteLength > 0, "Key ID exists"); + is(aAssertion.id, bytesToBase64UrlSafe(new Uint8Array(aAssertion.rawId)), "Encoded Key ID and Raw Key ID match"); + ok(aAssertion.response.authenticatorData === aAssertion.response.authenticatorData, "AuthenticatorAssertionResponse.AuthenticatorData is SameObject"); + ok(aAssertion.response.authenticatorData instanceof ArrayBuffer, "AuthenticatorAssertionResponse.AuthenticatorData is an ArrayBuffer"); + ok(aAssertion.response.signature === aAssertion.response.signature, "AuthenticatorAssertionResponse.Signature is SameObject"); + ok(aAssertion.response.signature instanceof ArrayBuffer, "AuthenticatorAssertionResponse.Signature is an ArrayBuffer"); + ok(aAssertion.response.userHandle === null, "AuthenticatorAssertionResponse.UserHandle is null for u2f authenticators"); + + ok(aAssertion.response.authenticatorData.byteLength > 0, "Authenticator data exists"); + let clientData = JSON.parse(buffer2string(aAssertion.response.clientDataJSON)); + is(clientData.challenge, bytesToBase64UrlSafe(gAssertionChallenge), "Challenge is correct"); + is(clientData.origin, window.location.origin, "Origin is correct"); + is(clientData.type, "webauthn.get", "Type is correct"); + + let attestation = await webAuthnDecodeAuthDataArray(aAssertion.response.authenticatorData); + ok(new Uint8Array(attestation.flags)[0] & flag_TUP == flag_TUP, "User presence flag must be set"); + is(attestation.counter.byteLength, 4, "Counter must be 4 bytes"); + let params = await deriveAppAndChallengeParam(window.location.host, aAssertion.response.clientDataJSON, attestation); + console.log(params); + console.log("ClientData buffer: ", hexEncode(aAssertion.response.clientDataJSON)); + console.log("ClientDataHash: ", hexEncode(params.challengeParam)); + let signedData = await assembleSignedData(params.appParam, params.attestation.flags, + params.attestation.counter, params.challengeParam); + console.log(aPublicKey, signedData, aAssertion.response.signature); + return await verifySignature(aPublicKey, signedData, aAssertion.response.signature); + } + + async function testMakeCredential() { + let rp = {id: document.domain, name: "none"}; + let user = {id: new Uint8Array(), name: "none", displayName: "none"}; + let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256}; + let makeCredentialOptions = { + rp, + user, + challenge: gCredentialChallenge, + pubKeyCredParams: [param], + attestation: "direct" + }; + let credential = await credm.create({publicKey: makeCredentialOptions}) + let decodedCredential = await decodeCreatedCredential(credential); + await testMakeDuplicate(decodedCredential); + } + + async function testMakeDuplicate(aCredInfo) { + let rp = {id: document.domain, name: "none"}; + let user = {id: new Uint8Array(), name: "none", displayName: "none"}; + let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256}; + let makeCredentialOptions = { + rp, + user, + challenge: gCredentialChallenge, + pubKeyCredParams: [param], + excludeCredentials: [{type: "public-key", id: new Uint8Array(aCredInfo.rawId), + transports: ["usb"]}] + }; + await credm.create({publicKey: makeCredentialOptions}) + .then(function() { + // We should have errored here! + ok(false, "The excludeList didn't stop a duplicate being created!"); + }).catch((aReason) => { + ok(aReason.toString().startsWith("InvalidStateError"), "Expect InvalidStateError, got " + aReason); + }); + await testAssertion(aCredInfo); + } + + async function testAssertion(aCredInfo) { + let newCredential = { + type: "public-key", + id: new Uint8Array(aCredInfo.rawId), + transports: ["usb"], + } + + let publicKeyCredentialRequestOptions = { + challenge: gAssertionChallenge, + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: document.domain, + allowCredentials: [newCredential] + }; + let assertion = await credm.get({publicKey: publicKeyCredentialRequestOptions}); + let sigVerifyResult = await checkAssertionAndSigValid(aCredInfo.publicKeyHandle, assertion); + ok(sigVerifyResult, "Signing signature verified"); + } +}); + +</script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_make_credential.html b/dom/webauthn/tests/test_webauthn_make_credential.html new file mode 100644 index 0000000000..4812d0da0d --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_make_credential.html @@ -0,0 +1,459 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for MakeCredential for W3C Web Authentication</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="u2futil.js"></script> + <script type="text/javascript" src="pkijs/common.js"></script> + <script type="text/javascript" src="pkijs/asn1.js"></script> + <script type="text/javascript" src="pkijs/x509_schema.js"></script> + <script type="text/javascript" src="pkijs/x509_simpl.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>Test for MakeCredential for W3C Web Authentication</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + add_task(async () => { + await addVirtualAuthenticator(); + }); + + is(navigator.authentication, undefined, "navigator.authentication does not exist any longer"); + isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); + isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); + isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + + let credm; + let gCredentialChallenge; + let rp; + let user; + let param; + let unsupportedParam; + let unknownParam; + + // Setup test env + add_task(() => { + gCredentialChallenge = new Uint8Array(16); + window.crypto.getRandomValues(gCredentialChallenge); + + rp = {id: document.domain, name: "none"}; + user = {id: new Uint8Array(64), name: "none", displayName: "none"}; + param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256}; + unsupportedParam = {type: "public-key", alg: cose_alg_ECDSA_w_SHA512}; + unknownParam = {type: "SimplePassword", alg: "MaxLength=2"}; + credm = navigator.credentials; + }); + // Add tests + add_task(test_good_call); + add_task(test_empty_account); + add_task(test_without_rp_name); + add_task(test_without_user_id); + add_task(test_without_user_name); + add_task(test_without_user_displayname); + add_task(test_user_too_large); + add_task(test_empty_parameters); + add_task(test_without_parameters); + add_task(test_unsupported_parameter); + add_task(test_unsupported_but_one_param); + add_task(test_one_unknown_parameter); + add_task(test_one_unknown_one_unsupported_param); + add_task(test_one_of_each_parameters); + add_task(test_without_challenge); + add_task(test_invalid_challenge); + add_task(test_duplicate_pub_key); + add_task(test_invalid_rp_id); + add_task(test_invalid_rp_id_2); + add_task(test_missing_rp); + add_task(test_incorrect_user_id_type); + add_task(test_missing_user); + add_task(test_complete_account); + add_task(test_too_large_user_id); + add_task(test_excluding_unknown_transports); + add_task(test_unknown_attestation_type); + add_task(test_unknown_selection_criteria); + add_task(test_no_unexpected_extensions); + add_task(test_cred_props_with_rk_required); + add_task(test_cred_props_with_rk_discouraged); + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + return Promise.resolve(); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + return Promise.resolve(); + } + + function expectNotAllowedError(aResult) { + ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError"); + return Promise.resolve(); + } + + function expectTypeError(aResult) { + ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError"); + return Promise.resolve(); + } + + function expectNotSupportedError(aResult) { + ok(aResult.toString().startsWith("NotSupportedError"), "Expecting a NotSupportedError"); + return Promise.resolve(); + } + + // Test basic good call + async function test_good_call() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test empty account + async function test_empty_account() { + let makeCredentialOptions = { + challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test without rp.name + async function test_without_rp_name() { + let rp1 = {id: document.domain}; + let makeCredentialOptions = { + rp: rp1, user, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test without user.id + async function test_without_user_id() { + let user1 = {name: "none", displayName: "none"}; + let makeCredentialOptions = { + rp, user: user1, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test without user.name + async function test_without_user_name() { + let user1 = {id: new Uint8Array(64), displayName: "none"}; + let makeCredentialOptions = { + rp, user: user1, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test without user.displayName + async function test_without_user_displayname() { + let user1 = {id: new Uint8Array(64), name: "none"}; + let makeCredentialOptions = { + rp, user: user1, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with a user handle that exceeds the max length + async function test_user_too_large() { + let user1 = {id: new Uint8Array(65), name: "none", displayName: "none"}; + let makeCredentialOptions = { + rp, user: user1, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test without any parameters; this is acceptable meaning the RP ID is + // happy to accept either ECDSA-SHA256 or RSA-SHA256 + async function test_empty_parameters() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test without a parameter array at all + async function test_without_parameters() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with an unsupported parameter + // The result depends on the tokens that are available, so we + // expect a NotAllowedError so as not to leak this. + async function test_unsupported_parameter() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [unsupportedParam] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + } + + // Test with an unsupported parameter and a good one + async function test_unsupported_but_one_param() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, + pubKeyCredParams: [param, unsupportedParam] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test with one unknown (not "public-key") parameter. + async function test_one_unknown_parameter() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [unknownParam] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectNotSupportedError); + } + + // Test with an unsupported parameter, and an unknown one + // The result depends on the tokens that are available, so we + // expect a NotAllowedError so as not to leak this. + async function test_one_unknown_one_unsupported_param() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, + pubKeyCredParams: [unsupportedParam, unknownParam] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + } + + // Test with an unsupported parameter, an unknown one, and a good one. This + // should succeed, as the unsupported and unknown should be ignored. + async function test_one_of_each_parameters() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, + pubKeyCredParams: [param, unsupportedParam, unknownParam] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test without a challenge + async function test_without_challenge() { + let makeCredentialOptions = { + rp, user, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with an invalid challenge + async function test_invalid_challenge() { + let makeCredentialOptions = { + rp, user, challenge: "begone, thou ill-fitting moist glove!", + pubKeyCredParams: [unsupportedParam] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with duplicate pubKeyCredParams + async function test_duplicate_pub_key() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, + pubKeyCredParams: [param, param, param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test with an RP ID that is not a valid domain string + async function test_invalid_rp_id() { + let rp1 = { id: document.domain + ":somejunk", name: "none"}; + let makeCredentialOptions = { + rp: rp1, user, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(arrivingHereIsGood); + } + + // Test with another RP ID that is not a valid domain string + async function test_invalid_rp_id_2() { + let rp1 = { id: document.domain + ":8888", name: "none"}; + let makeCredentialOptions = { + rp: rp1, user, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(arrivingHereIsGood); + } + + // Test with missing rp + async function test_missing_rp() { + let makeCredentialOptions = { + user, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with incorrect user ID type + async function test_incorrect_user_id_type() { + let invalidType = {id: "a string, which is not a buffer", name: "none", displayName: "none"}; + let makeCredentialOptions = { + user: invalidType, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with missing user + async function test_missing_user() { + let makeCredentialOptions = { + rp, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test a complete account + async function test_complete_account() { + // the icon fields are deprecated, but including them should not cause an error + let completeRP = {id: document.domain, name: "Foxxy Name", + icon: "https://example.com/fox.svg"}; + let completeUser = {id: string2buffer("foxes_are_the_best@example.com"), + name: "Fox F. Foxington", + icon: "https://example.com/fox.svg", + displayName: "Foxxy V"}; + let makeCredentialOptions = { + rp: completeRP, user: completeUser, challenge: gCredentialChallenge, + pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test with too-large user ID buffer + async function test_too_large_user_id() { + let hugeUser = {id: new Uint8Array(65), + name: "Fox F. Foxington", + displayName: "Foxxy V"}; + let makeCredentialOptions = { + rp, user: hugeUser, challenge: gCredentialChallenge, + pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with excluding unknown transports + async function test_excluding_unknown_transports() { + let completeRP = {id: document.domain, name: "Foxxy Name"}; + let completeUser = {id: string2buffer("foxes_are_the_best@example.com"), + name: "Fox F. Foxington", + displayName: "Foxxy V"}; + let excludedUnknownTransport = {type: "public-key", + id: string2buffer("123"), + transports: ["unknown", "usb"]}; + let makeCredentialOptions = { + rp: completeRP, user: completeUser, challenge: gCredentialChallenge, + pubKeyCredParams: [param], excludeCredentials: [excludedUnknownTransport] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + async function test_unknown_attestation_type() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [param], + attestation: "unknown" + }; + return credm.create({publicKey: makeCredentialOptions }) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + async function test_unknown_selection_criteria() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [param], + authenticatorSelection: { + userVerificationRequirement: "unknown UV requirement", + authenticatorAttachment: "unknown authenticator attachment type" + }, + }; + return credm.create({publicKey: makeCredentialOptions }) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + async function test_no_unexpected_extensions() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [param], + }; + let cred = await credm.create({publicKey: makeCredentialOptions}).catch(arrivingHereIsBad); + let extensionResults = cred.getClientExtensionResults(); + is(extensionResults.credProps, undefined, "no credProps output"); + } + + async function test_cred_props_with_rk_required() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [param], + authenticatorSelection: { + authenticatorAttachment: "cross-platform", + residentKey: "required", + }, + extensions: { credProps: true } + }; + let cred = await credm.create({publicKey: makeCredentialOptions}).catch(arrivingHereIsBad); + let extensionResults = cred.getClientExtensionResults(); + is(extensionResults.credProps?.rk, true, "rk is true"); + } + + async function test_cred_props_with_rk_discouraged() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [param], + authenticatorSelection: { + authenticatorAttachment: "cross-platform", + residentKey: "discouraged", + }, + extensions: { credProps: true } + }; + let cred = await credm.create({publicKey: makeCredentialOptions}).catch(arrivingHereIsBad); + let extensionResults = cred.getClientExtensionResults(); + is(extensionResults.credProps?.rk, false, "rk is false"); + } + + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_no_token.html b/dom/webauthn/tests/test_webauthn_no_token.html new file mode 100644 index 0000000000..28fe6e43eb --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_no_token.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for W3C Web Authentication with no token</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="u2futil.js"></script> + <script type="text/javascript" src="pkijs/common.js"></script> + <script type="text/javascript" src="pkijs/asn1.js"></script> + <script type="text/javascript" src="pkijs/x509_schema.js"></script> + <script type="text/javascript" src="pkijs/x509_simpl.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<h1>Test for W3C Web Authentication with no token</h1> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + +<script class="testbody" type="text/javascript"> +"use strict"; + +is(navigator.authentication, undefined, "navigator.authentication does not exist any longer"); +isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); +isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); +isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + +let credm; +let credentialChallenge; +let assertionChallenge; +let credentialId; + +// Setup test env +add_task(async () => { + credentialChallenge = new Uint8Array(16); + window.crypto.getRandomValues(credentialChallenge); + assertionChallenge = new Uint8Array(16); + window.crypto.getRandomValues(assertionChallenge); + credentialId = new Uint8Array(128); + window.crypto.getRandomValues(credentialId); + credm = navigator.credentials; + // Turn off all tokens. This should result in "not allowed" failures + await SpecialPowers.pushPrefEnv({"set": [ + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", false], + ]}); +}); + +add_task(async function test_no_token_make_credential() { + let rp = {id: document.domain, name: "none"}; + let user = {id: new Uint8Array(), name: "none", displayName: "none"}; + let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256}; + let makeCredentialOptions = { + rp, user, challenge: credentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(function(aResult) { + ok(false, "Should have failed."); + }) + .catch(function(aReason) { + ok(aReason.toString().startsWith("NotAllowedError"), aReason); + }); +}); + +add_task(async function test_no_token_get_assertion() { + let newCredential = { + type: "public-key", + id: credentialId, + transports: ["usb"], + } + let publicKeyCredentialRequestOptions = { + challenge: assertionChallenge, + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: document.domain, + allowCredentials: [newCredential] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(function(aResult) { + ok(false, "Should have failed."); + }) + .catch(function(aReason) { + ok(aReason.toString().startsWith("NotAllowedError"), aReason); + }) +}); + +</script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_sameorigin.html b/dom/webauthn/tests/test_webauthn_sameorigin.html new file mode 100644 index 0000000000..61a6430e87 --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_sameorigin.html @@ -0,0 +1,317 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for MakeCredential for W3C Web Authentication</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="u2futil.js"></script> + <script type="text/javascript" src="pkijs/common.js"></script> + <script type="text/javascript" src="pkijs/asn1.js"></script> + <script type="text/javascript" src="pkijs/x509_schema.js"></script> + <script type="text/javascript" src="pkijs/x509_simpl.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>Test Same Origin Policy for W3C Web Authentication</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + add_task(async () => { + await addVirtualAuthenticator(); + }); + + is(navigator.authentication, undefined, "navigator.authentication does not exist any longer"); + isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); + isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); + isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + + let credm; + let chall; + let user; + let param; + let gTrackedCredential; + add_task(() => { + credm = navigator.credentials; + + chall = new Uint8Array(16); + window.crypto.getRandomValues(chall); + + user = {id: new Uint8Array(16), name: "none", displayName: "none"}; + param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256}; + gTrackedCredential = {}; + }); + + add_task(test_basic_good); + add_task(test_rp_id_unset); + add_task(test_rp_name_unset); + add_task(test_origin_with_optional_fields); + add_task(test_blank_rp_id); + add_task(test_subdomain); + add_task(test_same_origin); + add_task(test_etld); + add_task(test_different_domain_same_tld); + add_task(test_assertion_basic_good); + add_task(test_assertion_rp_id_unset); + add_task(test_assertion_origin_with_optional_fields); + add_task(test_assertion_blank_rp_id); + add_task(test_assertion_subdomain); + add_task(test_assertion_same_origin); + add_task(test_assertion_etld); + add_task(test_assertion_different_domain_same_tld); + add_task(test_basic_good_with_origin); + add_task(test_assertion_basic_good_with_origin); + add_task(test_assertion_invalid_rp_id); + add_task(test_assertion_another_invalid_rp_id); + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectSecurityError(aResult) { + ok(aResult.toString().startsWith("SecurityError"), "Expecting a SecurityError"); + } + + function expectTypeError(aResult) { + ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError"); + } + + function keepThisPublicKeyCredential(aIdentifier) { + return function(aPublicKeyCredential) { + gTrackedCredential[aIdentifier] = { + type: "public-key", + id: new Uint8Array(aPublicKeyCredential.rawId), + transports: [ "usb" ], + } + return Promise.resolve(aPublicKeyCredential); + } + } + + function test_basic_good() { + // Test basic good call + let rp = {id: document.domain, name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(keepThisPublicKeyCredential("basic")) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + function test_rp_id_unset() { + // Test rp.id being unset + let makeCredentialOptions = { + rp: {name: "none"}, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + function test_rp_name_unset() { + // Test rp.name being unset + let makeCredentialOptions = { + rp: {id: document.domain}, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + function test_origin_with_optional_fields() { + // Test this origin with optional fields + let rp = {id: "user:pass@" + document.domain + ":8888", name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_blank_rp_id() { + // Test blank rp.id + let rp = {id: "", name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_subdomain() { + // Test subdomain of this origin + let rp = {id: "subdomain." + document.domain, name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_same_origin() { + // Test the same origin + let rp = {id: "example.com", name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + function test_etld() { + // Test the eTLD + let rp = {id: "com", name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_different_domain_same_tld() { + // Test a different domain within the same TLD + let rp = {id: "alt.test", name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_basic_good() { + // Test basic good call + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: document.domain, + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + function test_assertion_rp_id_unset() { + // Test rpId being unset + let publicKeyCredentialRequestOptions = { + challenge: chall, + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + function test_assertion_origin_with_optional_fields() { + // Test this origin with optional fields + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: "user:pass@" + document.origin + ":8888", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_blank_rp_id() { + // Test blank rpId + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: "", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_subdomain() { + // Test subdomain of this origin + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: "subdomain." + document.domain, + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_same_origin() { + // Test the same origin + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: "example.com", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + function test_assertion_etld() { + // Test the eTLD + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: "com", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_different_domain_same_tld() { + // Test a different domain within the same TLD + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: "alt.test", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_basic_good_with_origin() { + // Test basic good Create call but using an origin (Bug 1380421) + let rp = {id: window.origin, name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_basic_good_with_origin() { + // Test basic good Get call but using an origin (Bug 1380421) + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: window.origin, + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_invalid_rp_id() { + // Test with an rpId that is not a valid domain string + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: document.domain + ":somejunk", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(arrivingHereIsGood); + } + function test_assertion_another_invalid_rp_id() { + // Test with another rpId that is not a valid domain string + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: document.domain + ":8888", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(arrivingHereIsGood); + } + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_sameoriginwithancestors.html b/dom/webauthn/tests/test_webauthn_sameoriginwithancestors.html new file mode 100644 index 0000000000..276ac16bdd --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_sameoriginwithancestors.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for MakeCredential for W3C Web Authentication (sameOriginWithAncestors = false)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="u2futil.js"></script> + <script type="text/javascript" src="pkijs/common.js"></script> + <script type="text/javascript" src="pkijs/asn1.js"></script> + <script type="text/javascript" src="pkijs/x509_schema.js"></script> + <script type="text/javascript" src="pkijs/x509_simpl.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>Test Same Origin Policy for W3C Web Authentication (sameOriginWithAncestors = false)</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1694639">Mozilla Bug 1694639</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + add_task(async () => { + await addVirtualAuthenticator(); + }); + + var gTrackedCredential = {}; + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectNotAllowedError(aResult) { + ok(aResult == "NotAllowedError", "Expecting a NotAllowedError, got " + aResult); + } + + function keepThisPublicKeyCredential(aIdentifier) { + return function(aPublicKeyCredential) { + gTrackedCredential[aIdentifier] = { + type: "public-key", + id: new Uint8Array(aPublicKeyCredential.rawId), + transports: [ "usb" ], + } + return Promise.resolve(aPublicKeyCredential); + } + } + + add_task(async function runTests() { + let iframe = document.createElement("iframe"); + iframe.src = "https://example.org"; + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", resolve, {once: true})); + + is(navigator.authentication, undefined, "navigator.authentication does not exist any longer"); + isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); + isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); + isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + + let credm = navigator.credentials; + + let chall = new Uint8Array(16); + window.crypto.getRandomValues(chall); + + let user = {id: new Uint8Array(16), name: "none", displayName: "none"}; + let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256}; + + let rp = {id: document.domain, name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + await credm.create({publicKey: makeCredentialOptions}) + .then(keepThisPublicKeyCredential("basic")) + .catch(arrivingHereIsBad); + + var testFuncs = [ + function (args) { + // Test create when sameOriginWithAncestors = false + let credentialOptions = { + rp: args.rp, user: args.user, challenge: args.challenge, pubKeyCredParams: [args.param] + }; + return this.content.window.navigator.credentials.create({publicKey: credentialOptions}) + .catch(e => Promise.reject(e.name)); + }, + function (args) { + // Test get when sameOriginWithAncestors = false + let publicKeyCredentialRequestOptions = { + challenge: args.challenge, + rpId: args.rp.id, + allowCredentials: [args.trackedCredential.basic] + }; + return this.content.window.navigator.credentials.get({publicKey: publicKeyCredentialRequestOptions}) + .catch(e => Promise.reject(e.name)); + }, + ]; + + let args = { user, param, rp, challenge: chall, trackedCredential: gTrackedCredential } + for(let func of testFuncs) { + await SpecialPowers.spawn(iframe, [args], func) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + } + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_serialization.html b/dom/webauthn/tests/test_webauthn_serialization.html new file mode 100644 index 0000000000..492749434f --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_serialization.html @@ -0,0 +1,304 @@ +<!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", credProps: true, hmacCreateSecret: true, minPinLength: 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.credProps, true, "extensions.credProps should be true"); + is(creationOptions.extensions.hmacCreateSecret, true, "extensions.hmacCreateSecret should be true"); + is(creationOptions.extensions.minPinLength, true, "extensions.minPinLength 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}], + authenticatorSelection: { residentKey: "discouraged" }, + extensions: { credProps: true } + }; + let registrationResponse = await navigator.credentials.create({publicKey}); + let registrationResponseJSON = registrationResponse.toJSON(); + is(Object.keys(registrationResponseJSON).length, 6, "registrationResponseJSON should have 6 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], "internal", "registrationResponseJSON.response.transports[0] should be internal"); + is(registrationResponseJSON.authenticatorAttachment, "platform", "registrationResponseJSON.authenticatorAttachment should be platform"); + is(registrationResponseJSON.clientExtensionResults?.credProps?.rk, false, "registrationResponseJSON.clientExtensionResults.credProps.rk should be false"); + 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, 6, "assertionResponseJSON should have 6 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(assertionResponseJSON.authenticatorAttachment, "platform", "assertionResponseJSON.authenticatorAttachment should be platform"); + 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> diff --git a/dom/webauthn/tests/test_webauthn_store_credential.html b/dom/webauthn/tests/test_webauthn_store_credential.html new file mode 100644 index 0000000000..92782acc47 --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_store_credential.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Tests for Store for W3C Web Authentication</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 for Store for W3C Web Authentication</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + add_task(async () => { + await addVirtualAuthenticator(); + }); + + isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); + isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); + isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + isnot(navigator.credentials.store, undefined, "CredentialManagement store API endpoint must exist"); + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + return Promise.resolve(); + } + + function expectNotSupportedError(aResult) { + ok(aResult.toString().startsWith("NotSupportedError"), "Expecting a NotSupportedError, received: " + aResult); + return Promise.resolve(); + } + + add_task(async function test_store_credential() { + let credentialChallenge = new Uint8Array(16); + window.crypto.getRandomValues(credentialChallenge); + + let rp = {id: document.domain, name: "none"}; + let user = {id: new Uint8Array(64), name: "none", displayName: "none"}; + let params = [ {type: "public-key", alg: "es256"}, {type: "public-key", alg: -7} ] + + let makeCredentialOptions = { + rp, user, challenge: credentialChallenge, pubKeyCredParams: params + }; + + let credential = await navigator.credentials.create({publicKey: makeCredentialOptions}) + .catch(arrivingHereIsBad); + + await navigator.credentials.store(credential) + .then(arrivingHereIsBad) + .catch(expectNotSupportedError); + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/test_webauthn_webdriver_virtual_authenticator.html b/dom/webauthn/tests/test_webauthn_webdriver_virtual_authenticator.html new file mode 100644 index 0000000000..cb00f80e88 --- /dev/null +++ b/dom/webauthn/tests/test_webauthn_webdriver_virtual_authenticator.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Tests for WebDriver Virtual Authenticator Extension for W3C Web Authentication</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 for WebDriver Virtual Authenticator Extension for W3C Web Authentication</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1460986">Mozilla Bug 1460986</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectNotAllowedError(aResult) { + ok(aResult == "NotAllowedError", "Expecting a NotAllowedError, got " + aResult); + } + + function getAssertion(id) { + let chall = new Uint8Array(16); + crypto.getRandomValues(chall); + + let options = { + challenge: chall, + allowCredentials: [ { type: "public-key", id } ], + }; + + return navigator.credentials.get({publicKey: options}); + } + + add_task(async function test_add_and_remove_credential() { + let authenticatorId = await addVirtualAuthenticator(); + let credIdB64 = await addCredential(authenticatorId, document.domain); + let credId = base64ToBytesUrlSafe(credIdB64); + + await getAssertion(credId) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + await removeCredential(authenticatorId, credIdB64); + await getAssertion(credId) + .then(arrivingHereIsBad) + .catch(e => expectNotAllowedError(e.name)); + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2futil.js b/dom/webauthn/tests/u2futil.js new file mode 100644 index 0000000000..e8026e8e59 --- /dev/null +++ b/dom/webauthn/tests/u2futil.js @@ -0,0 +1,511 @@ +// Used by local_addTest() / local_completeTest() +var _countCompletions = 0; +var _expectedCompletions = 0; + +const flag_TUP = 0x01; +const flag_UV = 0x04; +const flag_AT = 0x40; + +const cose_kty = 1; +const cose_kty_ec2 = 2; +const cose_alg = 3; +const cose_alg_ECDSA_w_SHA256 = -7; +const cose_alg_ECDSA_w_SHA512 = -36; +const cose_crv = -1; +const cose_crv_P256 = 1; +const cose_crv_x = -2; +const cose_crv_y = -3; + +var { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +async function addVirtualAuthenticator( + protocol = "ctap2_1", + transport = "internal", + hasResidentKey = true, + hasUserVerification = true, + isUserConsenting = true, + isUserVerified = true +) { + let id = await SpecialPowers.spawnChrome( + [ + protocol, + transport, + hasResidentKey, + hasUserVerification, + isUserConsenting, + isUserVerified, + ], + ( + protocol, + transport, + hasResidentKey, + hasUserVerification, + isUserConsenting, + isUserVerified + ) => { + let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService + ); + let id = webauthnService.addVirtualAuthenticator( + protocol, + transport, + hasResidentKey, + hasUserVerification, + isUserConsenting, + isUserVerified + ); + return id; + } + ); + + SimpleTest.registerCleanupFunction(async () => { + await SpecialPowers.spawnChrome([id], id => { + let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService + ); + webauthnService.removeVirtualAuthenticator(id); + }); + }); + + return id; +} + +function handleEventMessage(event) { + if ("test" in event.data) { + let summary = event.data.test + ": " + event.data.msg; + log(event.data.status + ": " + summary); + ok(event.data.status, summary); + } else if ("done" in event.data) { + SimpleTest.finish(); + } else { + ok(false, "Unexpected message in the test harness: " + event.data); + } +} + +function log(msg) { + console.log(msg); + let logBox = document.getElementById("log"); + if (logBox) { + logBox.textContent += "\n" + msg; + } +} + +function local_is(value, expected, message) { + if (value === expected) { + local_ok(true, message); + } else { + local_ok(false, message + " unexpectedly: " + value + " !== " + expected); + } +} + +function local_isnot(value, expected, message) { + if (value !== expected) { + local_ok(true, message); + } else { + local_ok(false, message + " unexpectedly: " + value + " === " + expected); + } +} + +function local_ok(expression, message) { + let body = { test: this.location.pathname, status: expression, msg: message }; + parent.postMessage(body, "http://mochi.test:8888"); +} + +function local_doesThrow(fn, name) { + let gotException = false; + try { + fn(); + } catch (ex) { + gotException = true; + } + local_ok(gotException, name); +} + +function local_expectThisManyTests(count) { + if (_expectedCompletions > 0) { + local_ok( + false, + "Error: local_expectThisManyTests should only be called once." + ); + } + _expectedCompletions = count; +} + +function local_completeTest() { + _countCompletions += 1; + if (_countCompletions == _expectedCompletions) { + log("All tests completed."); + local_finished(); + } + if (_countCompletions > _expectedCompletions) { + local_ok( + false, + "Error: local_completeTest called more than local_addTest." + ); + } +} + +function local_finished() { + parent.postMessage({ done: true }, "http://mochi.test:8888"); +} + +function string2buffer(str) { + return new Uint8Array(str.length).map((x, i) => str.charCodeAt(i)); +} + +function buffer2string(buf) { + let str = ""; + if (!(buf.constructor === Uint8Array)) { + buf = new Uint8Array(buf); + } + buf.map(function (x) { + return (str += String.fromCharCode(x)); + }); + return str; +} + +function bytesToBase64(u8a) { + let CHUNK_SZ = 0x8000; + let c = []; + let array = new Uint8Array(u8a); + for (let i = 0; i < array.length; i += CHUNK_SZ) { + c.push(String.fromCharCode.apply(null, array.subarray(i, i + CHUNK_SZ))); + } + return window.btoa(c.join("")); +} + +function base64ToBytes(b64encoded) { + return new Uint8Array( + window + .atob(b64encoded) + .split("") + .map(function (c) { + return c.charCodeAt(0); + }) + ); +} + +function bytesToBase64UrlSafe(buf) { + return bytesToBase64(buf) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); +} + +function base64ToBytesUrlSafe(str) { + if (str.length % 4 == 1) { + throw "Improper b64 string"; + } + + var b64 = str.replace(/\-/g, "+").replace(/\_/g, "/"); + while (b64.length % 4 != 0) { + b64 += "="; + } + return base64ToBytes(b64); +} + +function hexEncode(buf) { + return Array.from(buf) + .map(x => ("0" + x.toString(16)).substr(-2)) + .join(""); +} + +function hexDecode(str) { + return new Uint8Array(str.match(/../g).map(x => parseInt(x, 16))); +} + +function hasOnlyKeys(obj, ...keys) { + let okeys = new Set(Object.keys(obj)); + return keys.length == okeys.size && keys.every(k => okeys.has(k)); +} + +function webAuthnDecodeCBORAttestation(aCborAttBuf) { + let attObj = CBOR.decode(aCborAttBuf); + console.log(":: Attestation CBOR Object ::"); + if (!hasOnlyKeys(attObj, "authData", "fmt", "attStmt")) { + return Promise.reject("Invalid CBOR Attestation Object"); + } + if (attObj.fmt == "fido-u2f" && !hasOnlyKeys(attObj.attStmt, "sig", "x5c")) { + return Promise.reject("Invalid CBOR Attestation Statement"); + } + if ( + attObj.fmt == "packed" && + !( + hasOnlyKeys(attObj.attStmt, "alg", "sig") || + hasOnlyKeys(attObj.attStmt, "alg", "sig", "x5c") + ) + ) { + return Promise.reject("Invalid CBOR Attestation Statement"); + } + if (attObj.fmt == "none" && Object.keys(attObj.attStmt).length) { + return Promise.reject("Invalid CBOR Attestation Statement"); + } + + return webAuthnDecodeAuthDataArray(new Uint8Array(attObj.authData)).then( + function (aAuthDataObj) { + attObj.authDataObj = aAuthDataObj; + return Promise.resolve(attObj); + } + ); +} + +function webAuthnDecodeAuthDataArray(aAuthData) { + let rpIdHash = aAuthData.slice(0, 32); + let flags = aAuthData.slice(32, 33); + let counter = aAuthData.slice(33, 37); + + console.log(":: Authenticator Data ::"); + console.log("RP ID Hash: " + hexEncode(rpIdHash)); + console.log("Counter: " + hexEncode(counter) + " Flags: " + flags); + + if ((flags & flag_AT) == 0x00) { + // No Attestation Data, so we're done. + return Promise.resolve({ + rpIdHash, + flags, + counter, + }); + } + + if (aAuthData.length < 38) { + return Promise.reject( + "Authenticator Data flag was set, but not enough data passed in!" + ); + } + + let attData = {}; + attData.aaguid = aAuthData.slice(37, 53); + attData.credIdLen = (aAuthData[53] << 8) + aAuthData[54]; + attData.credId = aAuthData.slice(55, 55 + attData.credIdLen); + + console.log(":: Authenticator Data ::"); + console.log("AAGUID: " + hexEncode(attData.aaguid)); + + let cborPubKey = aAuthData.slice(55 + attData.credIdLen); + var pubkeyObj = CBOR.decode(cborPubKey.buffer); + if ( + !( + cose_kty in pubkeyObj && + cose_alg in pubkeyObj && + cose_crv in pubkeyObj && + cose_crv_x in pubkeyObj && + cose_crv_y in pubkeyObj + ) + ) { + throw "Invalid CBOR Public Key Object"; + } + if (pubkeyObj[cose_kty] != cose_kty_ec2) { + throw "Unexpected key type"; + } + if (pubkeyObj[cose_alg] != cose_alg_ECDSA_w_SHA256) { + throw "Unexpected public key algorithm"; + } + if (pubkeyObj[cose_crv] != cose_crv_P256) { + throw "Unexpected curve"; + } + + let pubKeyBytes = assemblePublicKeyBytesData( + pubkeyObj[cose_crv_x], + pubkeyObj[cose_crv_y] + ); + console.log(":: CBOR Public Key Object Data ::"); + console.log("kty: " + pubkeyObj[cose_kty] + " (EC2)"); + console.log("alg: " + pubkeyObj[cose_alg] + " (ES256)"); + console.log("crv: " + pubkeyObj[cose_crv] + " (P256)"); + console.log("X: " + pubkeyObj[cose_crv_x]); + console.log("Y: " + pubkeyObj[cose_crv_y]); + console.log("Uncompressed (hex): " + hexEncode(pubKeyBytes)); + + return importPublicKey(pubKeyBytes).then(function (aKeyHandle) { + return Promise.resolve({ + rpIdHash, + flags, + counter, + attestationAuthData: attData, + publicKeyBytes: pubKeyBytes, + publicKeyHandle: aKeyHandle, + }); + }); +} + +function importPublicKey(keyBytes) { + if (keyBytes[0] != 0x04 || keyBytes.byteLength != 65) { + throw "Bad public key octet string"; + } + var jwk = { + kty: "EC", + crv: "P-256", + x: bytesToBase64UrlSafe(keyBytes.slice(1, 33)), + y: bytesToBase64UrlSafe(keyBytes.slice(33)), + }; + return crypto.subtle.importKey( + "jwk", + jwk, + { name: "ECDSA", namedCurve: "P-256" }, + true, + ["verify"] + ); +} + +function deriveAppAndChallengeParam(appId, clientData, attestation) { + var appIdBuf = string2buffer(appId); + return Promise.all([ + crypto.subtle.digest("SHA-256", appIdBuf), + crypto.subtle.digest("SHA-256", clientData), + ]).then(function (digests) { + return { + appParam: new Uint8Array(digests[0]), + challengeParam: new Uint8Array(digests[1]), + attestation, + }; + }); +} + +function assemblePublicKeyBytesData(xCoord, yCoord) { + // Produce an uncompressed EC key point. These start with 0x04, and then + // two 32-byte numbers denoting X and Y. + if (xCoord.length != 32 || yCoord.length != 32) { + throw "Coordinates must be 32 bytes long"; + } + let keyBytes = new Uint8Array(65); + keyBytes[0] = 0x04; + xCoord.map((x, i) => (keyBytes[1 + i] = x)); + yCoord.map((x, i) => (keyBytes[33 + i] = x)); + return keyBytes; +} + +function assembleSignedData(appParam, flags, counter, challengeParam) { + let signedData = new Uint8Array(32 + 1 + 4 + 32); + new Uint8Array(appParam).map((x, i) => (signedData[0 + i] = x)); + signedData[32] = new Uint8Array(flags)[0]; + new Uint8Array(counter).map((x, i) => (signedData[33 + i] = x)); + new Uint8Array(challengeParam).map((x, i) => (signedData[37 + i] = x)); + return signedData; +} + +function assembleRegistrationSignedData( + appParam, + challengeParam, + keyHandle, + pubKey +) { + let signedData = new Uint8Array(1 + 32 + 32 + keyHandle.length + 65); + signedData[0] = 0x00; + new Uint8Array(appParam).map((x, i) => (signedData[1 + i] = x)); + new Uint8Array(challengeParam).map((x, i) => (signedData[33 + i] = x)); + new Uint8Array(keyHandle).map((x, i) => (signedData[65 + i] = x)); + new Uint8Array(pubKey).map( + (x, i) => (signedData[65 + keyHandle.length + i] = x) + ); + return signedData; +} + +function sanitizeSigArray(arr) { + // ECDSA signature fields into WebCrypto must be exactly 32 bytes long, so + // this method strips leading padding bytes, if added, and also appends + // padding zeros, if needed. + if (arr.length > 32) { + arr = arr.slice(arr.length - 32); + } + let ret = new Uint8Array(32); + ret.set(arr, ret.length - arr.length); + return ret; +} + +function verifySignature(key, data, derSig) { + if (derSig.byteLength < 68) { + return Promise.reject( + "Invalid signature (length=" + + derSig.byteLength + + "): " + + hexEncode(new Uint8Array(derSig)) + ); + } + + // Copy signature data into the current context. + let derSigCopy = new ArrayBuffer(derSig.byteLength); + new Uint8Array(derSigCopy).set(new Uint8Array(derSig)); + + let sigAsn1 = org.pkijs.fromBER(derSigCopy); + + // pkijs.asn1 seems to erroneously set an error code when calling some + // internal function. The test suite doesn't like dangling globals. + delete window.error; + + let sigR = new Uint8Array( + sigAsn1.result.value_block.value[0].value_block.value_hex + ); + let sigS = new Uint8Array( + sigAsn1.result.value_block.value[1].value_block.value_hex + ); + + // The resulting R and S values from the ASN.1 Sequence must be fit into 32 + // bytes. Sometimes they have leading zeros, sometimes they're too short, it + // all depends on what lib generated the signature. + let R = sanitizeSigArray(sigR); + let S = sanitizeSigArray(sigS); + + console.log("Verifying these bytes: " + bytesToBase64UrlSafe(data)); + + let sigData = new Uint8Array(R.length + S.length); + sigData.set(R); + sigData.set(S, R.length); + + let alg = { name: "ECDSA", hash: "SHA-256" }; + return crypto.subtle.verify(alg, key, sigData, data); +} + +async function addCredential(authenticatorId, rpId) { + let keyPair = await crypto.subtle.generateKey( + { + name: "ECDSA", + namedCurve: "P-256", + }, + true, + ["sign"] + ); + + let credId = new Uint8Array(32); + crypto.getRandomValues(credId); + credId = bytesToBase64UrlSafe(credId); + + let privateKey = await crypto.subtle + .exportKey("pkcs8", keyPair.privateKey) + .then(privateKey => bytesToBase64UrlSafe(privateKey)); + + await SpecialPowers.spawnChrome( + [authenticatorId, credId, rpId, privateKey], + (authenticatorId, credId, rpId, privateKey) => { + let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService + ); + + webauthnService.addCredential( + authenticatorId, + credId, + true, // resident key + rpId, + privateKey, + "VGVzdCBVc2Vy", // "Test User" + 0 // sign count + ); + } + ); + + return credId; +} + +async function removeCredential(authenticatorId, credId) { + await SpecialPowers.spawnChrome( + [authenticatorId, credId], + (authenticatorId, credId) => { + let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService + ); + + webauthnService.removeCredential(authenticatorId, credId); + } + ); +} diff --git a/dom/webauthn/winwebauthn/.gitignore b/dom/webauthn/winwebauthn/.gitignore new file mode 100644 index 0000000000..3e759b75bf --- /dev/null +++ b/dom/webauthn/winwebauthn/.gitignore @@ -0,0 +1,330 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ diff --git a/dom/webauthn/winwebauthn/LICENSE b/dom/webauthn/winwebauthn/LICENSE new file mode 100644 index 0000000000..21071075c2 --- /dev/null +++ b/dom/webauthn/winwebauthn/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/dom/webauthn/winwebauthn/README.md b/dom/webauthn/winwebauthn/README.md new file mode 100644 index 0000000000..7aaa69079b --- /dev/null +++ b/dom/webauthn/winwebauthn/README.md @@ -0,0 +1,26 @@ +# Description + +This project includes Win32 headers for communicating to Windows Hello and external secruity keys as part of WebAuthn and CTAP specification. + +For more details about the standards, please follow these links: +* WebAuthn: https://w3c.github.io/webauthn/ +* CTAP: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html + + +# Having Issues? +If you have any issues in adopting these APIs or need some clarification, please contact fido-dev@microsoft.com. + + +# Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/dom/webauthn/winwebauthn/webauthn.h b/dom/webauthn/winwebauthn/webauthn.h new file mode 100644 index 0000000000..71fbc4b8b1 --- /dev/null +++ b/dom/webauthn/winwebauthn/webauthn.h @@ -0,0 +1,1145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#ifndef __WEBAUTHN_H_
+#define __WEBAUTHN_H_
+
+#pragma once
+
+#include <winapifamily.h>
+
+#pragma region Desktop Family or OneCore Family
+#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP | WINAPI_PARTITION_SYSTEM)
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef WINAPI
+#define WINAPI __stdcall
+#endif
+
+#ifndef INITGUID
+#define INITGUID
+#include <guiddef.h>
+#undef INITGUID
+#else
+#include <guiddef.h>
+#endif
+
+//+------------------------------------------------------------------------------------------
+// API Version Information.
+// Caller should check for WebAuthNGetApiVersionNumber to check the presence of relevant APIs
+// and features for their usage.
+//-------------------------------------------------------------------------------------------
+
+#define WEBAUTHN_API_VERSION_1 1
+// WEBAUTHN_API_VERSION_1 : Baseline Version
+// Data Structures and their sub versions:
+// - WEBAUTHN_RP_ENTITY_INFORMATION : 1
+// - WEBAUTHN_USER_ENTITY_INFORMATION : 1
+// - WEBAUTHN_CLIENT_DATA : 1
+// - WEBAUTHN_COSE_CREDENTIAL_PARAMETER : 1
+// - WEBAUTHN_COSE_CREDENTIAL_PARAMETERS : Not Applicable
+// - WEBAUTHN_CREDENTIAL : 1
+// - WEBAUTHN_CREDENTIALS : Not Applicable
+// - WEBAUTHN_CREDENTIAL_EX : 1
+// - WEBAUTHN_CREDENTIAL_LIST : Not Applicable
+// - WEBAUTHN_EXTENSION : Not Applicable
+// - WEBAUTHN_EXTENSIONS : Not Applicable
+// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 3
+// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 4
+// - WEBAUTHN_COMMON_ATTESTATION : 1
+// - WEBAUTHN_CREDENTIAL_ATTESTATION : 3
+// - WEBAUTHN_ASSERTION : 1
+// Extensions:
+// - WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET
+// APIs:
+// - WebAuthNGetApiVersionNumber
+// - WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable
+// - WebAuthNAuthenticatorMakeCredential
+// - WebAuthNAuthenticatorGetAssertion
+// - WebAuthNFreeCredentialAttestation
+// - WebAuthNFreeAssertion
+// - WebAuthNGetCancellationId
+// - WebAuthNCancelCurrentOperation
+// - WebAuthNGetErrorName
+// - WebAuthNGetW3CExceptionDOMError
+// Transports:
+// - WEBAUTHN_CTAP_TRANSPORT_USB
+// - WEBAUTHN_CTAP_TRANSPORT_NFC
+// - WEBAUTHN_CTAP_TRANSPORT_BLE
+// - WEBAUTHN_CTAP_TRANSPORT_INTERNAL
+
+#define WEBAUTHN_API_VERSION_2 2
+// WEBAUTHN_API_VERSION_2 : Delta From WEBAUTHN_API_VERSION_1
+// Added Extensions:
+// - WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_PROTECT
+//
+
+#define WEBAUTHN_API_VERSION_3 3
+// WEBAUTHN_API_VERSION_3 : Delta From WEBAUTHN_API_VERSION_2
+// Data Structures and their sub versions:
+// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 4
+// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 5
+// - WEBAUTHN_CREDENTIAL_ATTESTATION : 4
+// - WEBAUTHN_ASSERTION : 2
+// Added Extensions:
+// - WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_BLOB
+// - WEBAUTHN_EXTENSIONS_IDENTIFIER_MIN_PIN_LENGTH
+//
+
+#define WEBAUTHN_API_VERSION_4 4
+// WEBAUTHN_API_VERSION_4 : Delta From WEBAUTHN_API_VERSION_3
+// Data Structures and their sub versions:
+// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 5
+// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 6
+// - WEBAUTHN_ASSERTION : 3
+// - WEBAUTHN_CREDENTIAL_DETAILS : 1
+// APIs:
+// - WebAuthNGetPlatformCredentialList
+// - WebAuthNFreePlatformCredentialList
+// - WebAuthNDeletePlatformCredential
+//
+
+#define WEBAUTHN_API_VERSION_5 5
+// WEBAUTHN_API_VERSION_5 : Delta From WEBAUTHN_API_VERSION_4
+// Data Structures and their sub versions:
+// - WEBAUTHN_CREDENTIAL_DETAILS : 2
+// Extension Changes:
+// - Enabled LARGE_BLOB Support
+//
+
+#define WEBAUTHN_API_VERSION_6 6
+// WEBAUTHN_API_VERSION_6 : Delta From WEBAUTHN_API_VERSION_5
+// Data Structures and their sub versions:
+// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 6
+// - WEBAUTHN_CREDENTIAL_ATTESTATION : 5
+// - WEBAUTHN_ASSERTION : 4
+// Transports:
+// - WEBAUTHN_CTAP_TRANSPORT_HYBRID
+
+#define WEBAUTHN_API_VERSION_7 7
+// WEBAUTHN_API_VERSION_7 : Delta From WEBAUTHN_API_VERSION_6
+// Data Structures and their sub versions:
+// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 7
+// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 7
+// - WEBAUTHN_CREDENTIAL_ATTESTATION : 6
+// - WEBAUTHN_ASSERTION : 5
+
+#define WEBAUTHN_API_CURRENT_VERSION WEBAUTHN_API_VERSION_7
+
+//+------------------------------------------------------------------------------------------
+// Information about an RP Entity
+//-------------------------------------------------------------------------------------------
+
+#define WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION 1
+
+typedef struct _WEBAUTHN_RP_ENTITY_INFORMATION {
+ // Version of this structure, to allow for modifications in the future.
+ // This field is required and should be set to CURRENT_VERSION above.
+ DWORD dwVersion;
+
+ // Identifier for the RP. This field is required.
+ PCWSTR pwszId;
+
+ // Contains the friendly name of the Relying Party, such as "Acme Corporation", "Widgets Inc" or "Awesome Site".
+ // This field is required.
+ PCWSTR pwszName;
+
+ // Optional URL pointing to RP's logo.
+ PCWSTR pwszIcon;
+} WEBAUTHN_RP_ENTITY_INFORMATION, *PWEBAUTHN_RP_ENTITY_INFORMATION;
+typedef const WEBAUTHN_RP_ENTITY_INFORMATION *PCWEBAUTHN_RP_ENTITY_INFORMATION;
+
+//+------------------------------------------------------------------------------------------
+// Information about an User Entity
+//-------------------------------------------------------------------------------------------
+#define WEBAUTHN_MAX_USER_ID_LENGTH 64
+
+#define WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION 1
+
+typedef struct _WEBAUTHN_USER_ENTITY_INFORMATION {
+ // Version of this structure, to allow for modifications in the future.
+ // This field is required and should be set to CURRENT_VERSION above.
+ DWORD dwVersion;
+
+ // Identifier for the User. This field is required.
+ DWORD cbId;
+ _Field_size_bytes_(cbId)
+ PBYTE pbId;
+
+ // Contains a detailed name for this account, such as "john.p.smith@example.com".
+ PCWSTR pwszName;
+
+ // Optional URL that can be used to retrieve an image containing the user's current avatar,
+ // or a data URI that contains the image data.
+ PCWSTR pwszIcon;
+
+ // For User: Contains the friendly name associated with the user account by the Relying Party, such as "John P. Smith".
+ PCWSTR pwszDisplayName;
+} WEBAUTHN_USER_ENTITY_INFORMATION, *PWEBAUTHN_USER_ENTITY_INFORMATION;
+typedef const WEBAUTHN_USER_ENTITY_INFORMATION *PCWEBAUTHN_USER_ENTITY_INFORMATION;
+
+//+------------------------------------------------------------------------------------------
+// Information about client data.
+//-------------------------------------------------------------------------------------------
+
+#define WEBAUTHN_HASH_ALGORITHM_SHA_256 L"SHA-256"
+#define WEBAUTHN_HASH_ALGORITHM_SHA_384 L"SHA-384"
+#define WEBAUTHN_HASH_ALGORITHM_SHA_512 L"SHA-512"
+
+#define WEBAUTHN_CLIENT_DATA_CURRENT_VERSION 1
+
+typedef struct _WEBAUTHN_CLIENT_DATA {
+ // Version of this structure, to allow for modifications in the future.
+ // This field is required and should be set to CURRENT_VERSION above.
+ DWORD dwVersion;
+
+ // Size of the pbClientDataJSON field.
+ DWORD cbClientDataJSON;
+ // UTF-8 encoded JSON serialization of the client data.
+ _Field_size_bytes_(cbClientDataJSON)
+ PBYTE pbClientDataJSON;
+
+ // Hash algorithm ID used to hash the pbClientDataJSON field.
+ LPCWSTR pwszHashAlgId;
+} WEBAUTHN_CLIENT_DATA, *PWEBAUTHN_CLIENT_DATA;
+typedef const WEBAUTHN_CLIENT_DATA *PCWEBAUTHN_CLIENT_DATA;
+
+//+------------------------------------------------------------------------------------------
+// Information about credential parameters.
+//-------------------------------------------------------------------------------------------
+
+#define WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY L"public-key"
+
+#define WEBAUTHN_COSE_ALGORITHM_ECDSA_P256_WITH_SHA256 -7
+#define WEBAUTHN_COSE_ALGORITHM_ECDSA_P384_WITH_SHA384 -35
+#define WEBAUTHN_COSE_ALGORITHM_ECDSA_P521_WITH_SHA512 -36
+
+#define WEBAUTHN_COSE_ALGORITHM_RSASSA_PKCS1_V1_5_WITH_SHA256 -257
+#define WEBAUTHN_COSE_ALGORITHM_RSASSA_PKCS1_V1_5_WITH_SHA384 -258
+#define WEBAUTHN_COSE_ALGORITHM_RSASSA_PKCS1_V1_5_WITH_SHA512 -259
+
+#define WEBAUTHN_COSE_ALGORITHM_RSA_PSS_WITH_SHA256 -37
+#define WEBAUTHN_COSE_ALGORITHM_RSA_PSS_WITH_SHA384 -38
+#define WEBAUTHN_COSE_ALGORITHM_RSA_PSS_WITH_SHA512 -39
+
+#define WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION 1
+
+typedef struct _WEBAUTHN_COSE_CREDENTIAL_PARAMETER {
+ // Version of this structure, to allow for modifications in the future.
+ DWORD dwVersion;
+
+ // Well-known credential type specifying a credential to create.
+ LPCWSTR pwszCredentialType;
+
+ // Well-known COSE algorithm specifying the algorithm to use for the credential.
+ LONG lAlg;
+} WEBAUTHN_COSE_CREDENTIAL_PARAMETER, *PWEBAUTHN_COSE_CREDENTIAL_PARAMETER;
+typedef const WEBAUTHN_COSE_CREDENTIAL_PARAMETER *PCWEBAUTHN_COSE_CREDENTIAL_PARAMETER;
+
+typedef struct _WEBAUTHN_COSE_CREDENTIAL_PARAMETERS {
+ DWORD cCredentialParameters;
+ _Field_size_(cCredentialParameters)
+ PWEBAUTHN_COSE_CREDENTIAL_PARAMETER pCredentialParameters;
+} WEBAUTHN_COSE_CREDENTIAL_PARAMETERS, *PWEBAUTHN_COSE_CREDENTIAL_PARAMETERS;
+typedef const WEBAUTHN_COSE_CREDENTIAL_PARAMETERS *PCWEBAUTHN_COSE_CREDENTIAL_PARAMETERS;
+
+//+------------------------------------------------------------------------------------------
+// Information about credential.
+//-------------------------------------------------------------------------------------------
+#define WEBAUTHN_CREDENTIAL_CURRENT_VERSION 1
+
+typedef struct _WEBAUTHN_CREDENTIAL {
+ // Version of this structure, to allow for modifications in the future.
+ DWORD dwVersion;
+
+ // Size of pbID.
+ DWORD cbId;
+ // Unique ID for this particular credential.
+ _Field_size_bytes_(cbId)
+ PBYTE pbId;
+
+ // Well-known credential type specifying what this particular credential is.
+ LPCWSTR pwszCredentialType;
+} WEBAUTHN_CREDENTIAL, *PWEBAUTHN_CREDENTIAL;
+typedef const WEBAUTHN_CREDENTIAL *PCWEBAUTHN_CREDENTIAL;
+
+typedef struct _WEBAUTHN_CREDENTIALS {
+ DWORD cCredentials;
+ _Field_size_(cCredentials)
+ PWEBAUTHN_CREDENTIAL pCredentials;
+} WEBAUTHN_CREDENTIALS, *PWEBAUTHN_CREDENTIALS;
+typedef const WEBAUTHN_CREDENTIALS *PCWEBAUTHN_CREDENTIALS;
+
+//+------------------------------------------------------------------------------------------
+// Information about credential with extra information, such as, dwTransports
+//-------------------------------------------------------------------------------------------
+
+#define WEBAUTHN_CTAP_TRANSPORT_USB 0x00000001
+#define WEBAUTHN_CTAP_TRANSPORT_NFC 0x00000002
+#define WEBAUTHN_CTAP_TRANSPORT_BLE 0x00000004
+#define WEBAUTHN_CTAP_TRANSPORT_TEST 0x00000008
+#define WEBAUTHN_CTAP_TRANSPORT_INTERNAL 0x00000010
+#define WEBAUTHN_CTAP_TRANSPORT_HYBRID 0x00000020
+#define WEBAUTHN_CTAP_TRANSPORT_FLAGS_MASK 0x0000003F
+
+#define WEBAUTHN_CREDENTIAL_EX_CURRENT_VERSION 1
+
+typedef struct _WEBAUTHN_CREDENTIAL_EX {
+ // Version of this structure, to allow for modifications in the future.
+ DWORD dwVersion;
+
+ // Size of pbID.
+ DWORD cbId;
+ // Unique ID for this particular credential.
+ _Field_size_bytes_(cbId)
+ PBYTE pbId;
+
+ // Well-known credential type specifying what this particular credential is.
+ LPCWSTR pwszCredentialType;
+
+ // Transports. 0 implies no transport restrictions.
+ DWORD dwTransports;
+} WEBAUTHN_CREDENTIAL_EX, *PWEBAUTHN_CREDENTIAL_EX;
+typedef const WEBAUTHN_CREDENTIAL_EX *PCWEBAUTHN_CREDENTIAL_EX;
+
+//+------------------------------------------------------------------------------------------
+// Information about credential list with extra information
+//-------------------------------------------------------------------------------------------
+
+typedef struct _WEBAUTHN_CREDENTIAL_LIST {
+ DWORD cCredentials;
+ _Field_size_(cCredentials)
+ PWEBAUTHN_CREDENTIAL_EX *ppCredentials;
+} WEBAUTHN_CREDENTIAL_LIST, *PWEBAUTHN_CREDENTIAL_LIST;
+typedef const WEBAUTHN_CREDENTIAL_LIST *PCWEBAUTHN_CREDENTIAL_LIST;
+
+//+------------------------------------------------------------------------------------------
+// Information about linked devices
+//-------------------------------------------------------------------------------------------
+
+#define CTAPCBOR_HYBRID_STORAGE_LINKED_DATA_VERSION_1 1
+#define CTAPCBOR_HYBRID_STORAGE_LINKED_DATA_CURRENT_VERSION CTAPCBOR_HYBRID_STORAGE_LINKED_DATA_VERSION_1
+
+typedef struct _CTAPCBOR_HYBRID_STORAGE_LINKED_DATA
+{
+ // Version
+ DWORD dwVersion;
+
+ // Contact Id
+ DWORD cbContactId;
+ _Field_size_bytes_(cbContactId)
+ PBYTE pbContactId;
+
+ // Link Id
+ DWORD cbLinkId;
+ _Field_size_bytes_(cbLinkId)
+ PBYTE pbLinkId;
+
+ // Link secret
+ DWORD cbLinkSecret;
+ _Field_size_bytes_(cbLinkSecret)
+ PBYTE pbLinkSecret;
+
+ // Authenticator Public Key
+ DWORD cbPublicKey;
+ _Field_size_bytes_(cbPublicKey)
+ PBYTE pbPublicKey;
+
+ // Authenticator Name
+ PCWSTR pwszAuthenticatorName;
+
+ // Tunnel server domain
+ WORD wEncodedTunnelServerDomain;
+} CTAPCBOR_HYBRID_STORAGE_LINKED_DATA, *PCTAPCBOR_HYBRID_STORAGE_LINKED_DATA;
+typedef const CTAPCBOR_HYBRID_STORAGE_LINKED_DATA *PCCTAPCBOR_HYBRID_STORAGE_LINKED_DATA;
+
+//+------------------------------------------------------------------------------------------
+// Credential Information for WebAuthNGetPlatformCredentialList API
+//-------------------------------------------------------------------------------------------
+
+#define WEBAUTHN_CREDENTIAL_DETAILS_VERSION_1 1
+#define WEBAUTHN_CREDENTIAL_DETAILS_VERSION_2 2
+#define WEBAUTHN_CREDENTIAL_DETAILS_CURRENT_VERSION WEBAUTHN_CREDENTIAL_DETAILS_VERSION_2
+
+typedef struct _WEBAUTHN_CREDENTIAL_DETAILS {
+ // Version of this structure, to allow for modifications in the future.
+ DWORD dwVersion;
+
+ // Size of pbCredentialID.
+ DWORD cbCredentialID;
+ _Field_size_bytes_(cbCredentialID)
+ PBYTE pbCredentialID;
+
+ // RP Info
+ PWEBAUTHN_RP_ENTITY_INFORMATION pRpInformation;
+
+ // User Info
+ PWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation;
+
+ // Removable or not.
+ BOOL bRemovable;
+
+ //
+ // The following fields have been added in WEBAUTHN_CREDENTIAL_DETAILS_VERSION_2
+ //
+
+ // Backed Up or not.
+ BOOL bBackedUp;
+} WEBAUTHN_CREDENTIAL_DETAILS, *PWEBAUTHN_CREDENTIAL_DETAILS;
+typedef const WEBAUTHN_CREDENTIAL_DETAILS *PCWEBAUTHN_CREDENTIAL_DETAILS;
+
+typedef struct _WEBAUTHN_CREDENTIAL_DETAILS_LIST {
+ DWORD cCredentialDetails;
+ _Field_size_(cCredentialDetails)
+ PWEBAUTHN_CREDENTIAL_DETAILS *ppCredentialDetails;
+} WEBAUTHN_CREDENTIAL_DETAILS_LIST, *PWEBAUTHN_CREDENTIAL_DETAILS_LIST;
+typedef const WEBAUTHN_CREDENTIAL_DETAILS_LIST *PCWEBAUTHN_CREDENTIAL_DETAILS_LIST;
+
+#define WEBAUTHN_GET_CREDENTIALS_OPTIONS_VERSION_1 1
+#define WEBAUTHN_GET_CREDENTIALS_OPTIONS_CURRENT_VERSION WEBAUTHN_GET_CREDENTIALS_OPTIONS_VERSION_1
+
+typedef struct _WEBAUTHN_GET_CREDENTIALS_OPTIONS {
+ // Version of this structure, to allow for modifications in the future.
+ DWORD dwVersion;
+
+ // Optional.
+ LPCWSTR pwszRpId;
+
+ // Optional. BrowserInPrivate Mode. Defaulting to FALSE.
+ BOOL bBrowserInPrivateMode;
+} WEBAUTHN_GET_CREDENTIALS_OPTIONS, *PWEBAUTHN_GET_CREDENTIALS_OPTIONS;
+typedef const WEBAUTHN_GET_CREDENTIALS_OPTIONS *PCWEBAUTHN_GET_CREDENTIALS_OPTIONS;
+
+//+------------------------------------------------------------------------------------------
+// PRF values.
+//-------------------------------------------------------------------------------------------
+
+#define WEBAUTHN_CTAP_ONE_HMAC_SECRET_LENGTH 32
+
+// SALT values below by default are converted into RAW Hmac-Secret values as per PRF extension.
+// - SHA-256(UTF8Encode("WebAuthn PRF") || 0x00 || Value)
+//
+// Set WEBAUTHN_AUTHENTICATOR_HMAC_SECRET_VALUES_FLAG in dwFlags in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS,
+// if caller wants to provide RAW Hmac-Secret SALT values directly. In that case,
+// values if provided MUST be of WEBAUTHN_CTAP_ONE_HMAC_SECRET_LENGTH size.
+
+typedef struct _WEBAUTHN_HMAC_SECRET_SALT {
+ // Size of pbFirst.
+ DWORD cbFirst;
+ _Field_size_bytes_(cbFirst)
+ PBYTE pbFirst; // Required
+
+ // Size of pbSecond.
+ DWORD cbSecond;
+ _Field_size_bytes_(cbSecond)
+ PBYTE pbSecond;
+} WEBAUTHN_HMAC_SECRET_SALT, *PWEBAUTHN_HMAC_SECRET_SALT;
+typedef const WEBAUTHN_HMAC_SECRET_SALT *PCWEBAUTHN_HMAC_SECRET_SALT;
+
+typedef struct _WEBAUTHN_CRED_WITH_HMAC_SECRET_SALT {
+ // Size of pbCredID.
+ DWORD cbCredID;
+ _Field_size_bytes_(cbCredID)
+ PBYTE pbCredID; // Required
+
+ // PRF Values for above credential
+ PWEBAUTHN_HMAC_SECRET_SALT pHmacSecretSalt; // Required
+} WEBAUTHN_CRED_WITH_HMAC_SECRET_SALT, *PWEBAUTHN_CRED_WITH_HMAC_SECRET_SALT;
+typedef const WEBAUTHN_CRED_WITH_HMAC_SECRET_SALT *PCWEBAUTHN_CRED_WITH_HMAC_SECRET_SALT;
+
+typedef struct _WEBAUTHN_HMAC_SECRET_SALT_VALUES {
+ PWEBAUTHN_HMAC_SECRET_SALT pGlobalHmacSalt;
+
+ DWORD cCredWithHmacSecretSaltList;
+ _Field_size_(cCredWithHmacSecretSaltList)
+ PWEBAUTHN_CRED_WITH_HMAC_SECRET_SALT pCredWithHmacSecretSaltList;
+} WEBAUTHN_HMAC_SECRET_SALT_VALUES, *PWEBAUTHN_HMAC_SECRET_SALT_VALUES;
+typedef const WEBAUTHN_HMAC_SECRET_SALT_VALUES *PCWEBAUTHN_HMAC_SECRET_SALT_VALUES;
+
+//+------------------------------------------------------------------------------------------
+// Hmac-Secret extension
+//-------------------------------------------------------------------------------------------
+
+#define WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET L"hmac-secret"
+// Below type definitions is for WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET
+// MakeCredential Input Type: BOOL.
+// - pvExtension must point to a BOOL with the value TRUE.
+// - cbExtension must contain the sizeof(BOOL).
+// MakeCredential Output Type: BOOL.
+// - pvExtension will point to a BOOL with the value TRUE if credential
+// was successfully created with HMAC_SECRET.
+// - cbExtension will contain the sizeof(BOOL).
+// GetAssertion Input Type: Not Supported
+// GetAssertion Output Type: Not Supported
+
+//+------------------------------------------------------------------------------------------
+// credProtect extension
+//-------------------------------------------------------------------------------------------
+
+#define WEBAUTHN_USER_VERIFICATION_ANY 0
+#define WEBAUTHN_USER_VERIFICATION_OPTIONAL 1
+#define WEBAUTHN_USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST 2
+#define WEBAUTHN_USER_VERIFICATION_REQUIRED 3
+
+typedef struct _WEBAUTHN_CRED_PROTECT_EXTENSION_IN {
+ // One of the above WEBAUTHN_USER_VERIFICATION_* values
+ DWORD dwCredProtect;
+ // Set the following to TRUE to require authenticator support for the credProtect extension
+ BOOL bRequireCredProtect;
+} WEBAUTHN_CRED_PROTECT_EXTENSION_IN, *PWEBAUTHN_CRED_PROTECT_EXTENSION_IN;
+typedef const WEBAUTHN_CRED_PROTECT_EXTENSION_IN *PCWEBAUTHN_CRED_PROTECT_EXTENSION_IN;
+
+
+#define WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_PROTECT L"credProtect"
+// Below type definitions is for WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_PROTECT
+// MakeCredential Input Type: WEBAUTHN_CRED_PROTECT_EXTENSION_IN.
+// - pvExtension must point to a WEBAUTHN_CRED_PROTECT_EXTENSION_IN struct
+// - cbExtension will contain the sizeof(WEBAUTHN_CRED_PROTECT_EXTENSION_IN).
+// MakeCredential Output Type: DWORD.
+// - pvExtension will point to a DWORD with one of the above WEBAUTHN_USER_VERIFICATION_* values
+// if credential was successfully created with CRED_PROTECT.
+// - cbExtension will contain the sizeof(DWORD).
+// GetAssertion Input Type: Not Supported
+// GetAssertion Output Type: Not Supported
+
+//+------------------------------------------------------------------------------------------
+// credBlob extension
+//-------------------------------------------------------------------------------------------
+
+typedef struct _WEBAUTHN_CRED_BLOB_EXTENSION {
+ // Size of pbCredBlob.
+ DWORD cbCredBlob;
+ _Field_size_bytes_(cbCredBlob)
+ PBYTE pbCredBlob;
+} WEBAUTHN_CRED_BLOB_EXTENSION, *PWEBAUTHN_CRED_BLOB_EXTENSION;
+typedef const WEBAUTHN_CRED_BLOB_EXTENSION *PCWEBAUTHN_CRED_BLOB_EXTENSION;
+
+
+#define WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_BLOB L"credBlob"
+// Below type definitions is for WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_BLOB
+// MakeCredential Input Type: WEBAUTHN_CRED_BLOB_EXTENSION.
+// - pvExtension must point to a WEBAUTHN_CRED_BLOB_EXTENSION struct
+// - cbExtension must contain the sizeof(WEBAUTHN_CRED_BLOB_EXTENSION).
+// MakeCredential Output Type: BOOL.
+// - pvExtension will point to a BOOL with the value TRUE if credBlob was successfully created
+// - cbExtension will contain the sizeof(BOOL).
+// GetAssertion Input Type: BOOL.
+// - pvExtension must point to a BOOL with the value TRUE to request the credBlob.
+// - cbExtension must contain the sizeof(BOOL).
+// GetAssertion Output Type: WEBAUTHN_CRED_BLOB_EXTENSION.
+// - pvExtension will point to a WEBAUTHN_CRED_BLOB_EXTENSION struct if the authenticator
+// returns the credBlob in the signed extensions
+// - cbExtension will contain the sizeof(WEBAUTHN_CRED_BLOB_EXTENSION).
+
+//+------------------------------------------------------------------------------------------
+// minPinLength extension
+//-------------------------------------------------------------------------------------------
+
+#define WEBAUTHN_EXTENSIONS_IDENTIFIER_MIN_PIN_LENGTH L"minPinLength"
+// Below type definitions is for WEBAUTHN_EXTENSIONS_IDENTIFIER_MIN_PIN_LENGTH
+// MakeCredential Input Type: BOOL.
+// - pvExtension must point to a BOOL with the value TRUE to request the minPinLength.
+// - cbExtension must contain the sizeof(BOOL).
+// MakeCredential Output Type: DWORD.
+// - pvExtension will point to a DWORD with the minimum pin length if returned by the authenticator
+// - cbExtension will contain the sizeof(DWORD).
+// GetAssertion Input Type: Not Supported
+// GetAssertion Output Type: Not Supported
+
+//+------------------------------------------------------------------------------------------
+// Information about Extensions.
+//-------------------------------------------------------------------------------------------
+typedef struct _WEBAUTHN_EXTENSION {
+ LPCWSTR pwszExtensionIdentifier;
+ DWORD cbExtension;
+ PVOID pvExtension;
+} WEBAUTHN_EXTENSION, *PWEBAUTHN_EXTENSION;
+typedef const WEBAUTHN_EXTENSION *PCWEBAUTHN_EXTENSION;
+
+typedef struct _WEBAUTHN_EXTENSIONS {
+ DWORD cExtensions;
+ _Field_size_(cExtensions)
+ PWEBAUTHN_EXTENSION pExtensions;
+} WEBAUTHN_EXTENSIONS, *PWEBAUTHN_EXTENSIONS;
+typedef const WEBAUTHN_EXTENSIONS *PCWEBAUTHN_EXTENSIONS;
+
+//+------------------------------------------------------------------------------------------
+// Options.
+//-------------------------------------------------------------------------------------------
+
+#define WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY 0
+#define WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM 1
+#define WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM 2
+#define WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM_U2F_V2 3
+
+#define WEBAUTHN_USER_VERIFICATION_REQUIREMENT_ANY 0
+#define WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED 1
+#define WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED 2
+#define WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED 3
+
+#define WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_ANY 0
+#define WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE 1
+#define WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT 2
+#define WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT 3
+
+#define WEBAUTHN_ENTERPRISE_ATTESTATION_NONE 0
+#define WEBAUTHN_ENTERPRISE_ATTESTATION_VENDOR_FACILITATED 1
+#define WEBAUTHN_ENTERPRISE_ATTESTATION_PLATFORM_MANAGED 2
+
+#define WEBAUTHN_LARGE_BLOB_SUPPORT_NONE 0
+#define WEBAUTHN_LARGE_BLOB_SUPPORT_REQUIRED 1
+#define WEBAUTHN_LARGE_BLOB_SUPPORT_PREFERRED 2
+
+#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_1 1
+#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_2 2
+#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_3 3
+#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_4 4
+#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_5 5
+#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_6 6
+#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_7 7
+#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_CURRENT_VERSION WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_7
+
+typedef struct _WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS {
+ // Version of this structure, to allow for modifications in the future.
+ DWORD dwVersion;
+
+ // Time that the operation is expected to complete within.
+ // This is used as guidance, and can be overridden by the platform.
+ DWORD dwTimeoutMilliseconds;
+
+ // Credentials used for exclusion.
+ WEBAUTHN_CREDENTIALS CredentialList;
+
+ // Optional extensions to parse when performing the operation.
+ WEBAUTHN_EXTENSIONS Extensions;
+
+ // Optional. Platform vs Cross-Platform Authenticators.
+ DWORD dwAuthenticatorAttachment;
+
+ // Optional. Require key to be resident or not. Defaulting to FALSE.
+ BOOL bRequireResidentKey;
+
+ // User Verification Requirement.
+ DWORD dwUserVerificationRequirement;
+
+ // Attestation Conveyance Preference.
+ DWORD dwAttestationConveyancePreference;
+
+ // Reserved for future Use
+ DWORD dwFlags;
+
+ //
+ // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_2
+ //
+
+ // Cancellation Id - Optional - See WebAuthNGetCancellationId
+ GUID *pCancellationId;
+
+ //
+ // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_3
+ //
+
+ // Exclude Credential List. If present, "CredentialList" will be ignored.
+ PWEBAUTHN_CREDENTIAL_LIST pExcludeCredentialList;
+
+ //
+ // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_4
+ //
+
+ // Enterprise Attestation
+ DWORD dwEnterpriseAttestation;
+
+ // Large Blob Support: none, required or preferred
+ //
+ // NTE_INVALID_PARAMETER when large blob required or preferred and
+ // bRequireResidentKey isn't set to TRUE
+ DWORD dwLargeBlobSupport;
+
+ // Optional. Prefer key to be resident. Defaulting to FALSE. When TRUE,
+ // overrides the above bRequireResidentKey.
+ BOOL bPreferResidentKey;
+
+ //
+ // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_5
+ //
+
+ // Optional. BrowserInPrivate Mode. Defaulting to FALSE.
+ BOOL bBrowserInPrivateMode;
+
+ //
+ // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_6
+ //
+
+ // Enable PRF
+ BOOL bEnablePrf;
+
+ //
+ // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_7
+ //
+
+ // Optional. Linked Device Connection Info.
+ PCTAPCBOR_HYBRID_STORAGE_LINKED_DATA pLinkedDevice;
+
+ // Size of pbJsonExt
+ DWORD cbJsonExt;
+ _Field_size_bytes_(cbJsonExt)
+ PBYTE pbJsonExt;
+} WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS, *PWEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS;
+typedef const WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS *PCWEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS;
+
+#define WEBAUTHN_CRED_LARGE_BLOB_OPERATION_NONE 0
+#define WEBAUTHN_CRED_LARGE_BLOB_OPERATION_GET 1
+#define WEBAUTHN_CRED_LARGE_BLOB_OPERATION_SET 2
+#define WEBAUTHN_CRED_LARGE_BLOB_OPERATION_DELETE 3
+
+#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_1 1
+#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_2 2
+#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_3 3
+#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_4 4
+#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_5 5
+#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_6 6
+#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_7 7
+#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_CURRENT_VERSION WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_7
+
+/*
+ Information about flags.
+*/
+
+#define WEBAUTHN_AUTHENTICATOR_HMAC_SECRET_VALUES_FLAG 0x00100000
+
+typedef struct _WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS {
+ // Version of this structure, to allow for modifications in the future.
+ DWORD dwVersion;
+
+ // Time that the operation is expected to complete within.
+ // This is used as guidance, and can be overridden by the platform.
+ DWORD dwTimeoutMilliseconds;
+
+ // Allowed Credentials List.
+ WEBAUTHN_CREDENTIALS CredentialList;
+
+ // Optional extensions to parse when performing the operation.
+ WEBAUTHN_EXTENSIONS Extensions;
+
+ // Optional. Platform vs Cross-Platform Authenticators.
+ DWORD dwAuthenticatorAttachment;
+
+ // User Verification Requirement.
+ DWORD dwUserVerificationRequirement;
+
+ // Flags
+ DWORD dwFlags;
+
+ //
+ // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_2
+ //
+
+ // Optional identifier for the U2F AppId. Converted to UTF8 before being hashed. Not lower cased.
+ PCWSTR pwszU2fAppId;
+
+ // If the following is non-NULL, then, set to TRUE if the above pwszU2fAppid was used instead of
+ // PCWSTR pwszRpId;
+ BOOL *pbU2fAppId;
+
+ //
+ // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_3
+ //
+
+ // Cancellation Id - Optional - See WebAuthNGetCancellationId
+ GUID *pCancellationId;
+
+ //
+ // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_4
+ //
+
+ // Allow Credential List. If present, "CredentialList" will be ignored.
+ PWEBAUTHN_CREDENTIAL_LIST pAllowCredentialList;
+
+ //
+ // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_5
+ //
+
+ DWORD dwCredLargeBlobOperation;
+
+ // Size of pbCredLargeBlob
+ DWORD cbCredLargeBlob;
+ _Field_size_bytes_(cbCredLargeBlob)
+ PBYTE pbCredLargeBlob;
+
+ //
+ // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_6
+ //
+
+ // PRF values which will be converted into HMAC-SECRET values according to WebAuthn Spec.
+ PWEBAUTHN_HMAC_SECRET_SALT_VALUES pHmacSecretSaltValues;
+
+ // Optional. BrowserInPrivate Mode. Defaulting to FALSE.
+ BOOL bBrowserInPrivateMode;
+
+ //
+ // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_7
+ //
+
+ // Optional. Linked Device Connection Info.
+ PCTAPCBOR_HYBRID_STORAGE_LINKED_DATA pLinkedDevice;
+
+ // Optional. Allowlist MUST contain 1 credential applicable for Hybrid transport.
+ BOOL bAutoFill;
+
+ // Size of pbJsonExt
+ DWORD cbJsonExt;
+ _Field_size_bytes_(cbJsonExt)
+ PBYTE pbJsonExt;
+} WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS, *PWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS;
+typedef const WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS *PCWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS;
+
+
+//+------------------------------------------------------------------------------------------
+// Attestation Info.
+//
+//-------------------------------------------------------------------------------------------
+#define WEBAUTHN_ATTESTATION_DECODE_NONE 0
+#define WEBAUTHN_ATTESTATION_DECODE_COMMON 1
+// WEBAUTHN_ATTESTATION_DECODE_COMMON supports format types
+// L"packed"
+// L"fido-u2f"
+
+#define WEBAUTHN_ATTESTATION_VER_TPM_2_0 L"2.0"
+
+typedef struct _WEBAUTHN_X5C {
+ // Length of X.509 encoded certificate
+ DWORD cbData;
+ // X.509 encoded certificate bytes
+ _Field_size_bytes_(cbData)
+ PBYTE pbData;
+} WEBAUTHN_X5C, *PWEBAUTHN_X5C;
+
+// Supports either Self or Full Basic Attestation
+
+// Note, new fields will be added to the following data structure to
+// support additional attestation format types, such as, TPM.
+// When fields are added, the dwVersion will be incremented.
+//
+// Therefore, your code must make the following check:
+// "if (dwVersion >= WEBAUTHN_COMMON_ATTESTATION_CURRENT_VERSION)"
+
+#define WEBAUTHN_COMMON_ATTESTATION_CURRENT_VERSION 1
+
+typedef struct _WEBAUTHN_COMMON_ATTESTATION {
+ // Version of this structure, to allow for modifications in the future.
+ DWORD dwVersion;
+
+ // Hash and Padding Algorithm
+ //
+ // The following won't be set for "fido-u2f" which assumes "ES256".
+ PCWSTR pwszAlg;
+ LONG lAlg; // COSE algorithm
+
+ // Signature that was generated for this attestation.
+ DWORD cbSignature;
+ _Field_size_bytes_(cbSignature)
+ PBYTE pbSignature;
+
+ // Following is set for Full Basic Attestation. If not, set then, this is Self Attestation.
+ // Array of X.509 DER encoded certificates. The first certificate is the signer, leaf certificate.
+ DWORD cX5c;
+ _Field_size_(cX5c)
+ PWEBAUTHN_X5C pX5c;
+
+ // Following are also set for tpm
+ PCWSTR pwszVer; // L"2.0"
+ DWORD cbCertInfo;
+ _Field_size_bytes_(cbCertInfo)
+ PBYTE pbCertInfo;
+ DWORD cbPubArea;
+ _Field_size_bytes_(cbPubArea)
+ PBYTE pbPubArea;
+} WEBAUTHN_COMMON_ATTESTATION, *PWEBAUTHN_COMMON_ATTESTATION;
+typedef const WEBAUTHN_COMMON_ATTESTATION *PCWEBAUTHN_COMMON_ATTESTATION;
+
+#define WEBAUTHN_ATTESTATION_TYPE_PACKED L"packed"
+#define WEBAUTHN_ATTESTATION_TYPE_U2F L"fido-u2f"
+#define WEBAUTHN_ATTESTATION_TYPE_TPM L"tpm"
+#define WEBAUTHN_ATTESTATION_TYPE_NONE L"none"
+
+#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_1 1
+#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_2 2
+#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_3 3
+#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_4 4
+#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5 5
+#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_6 6
+#define WEBAUTHN_CREDENTIAL_ATTESTATION_CURRENT_VERSION WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_6
+
+typedef struct _WEBAUTHN_CREDENTIAL_ATTESTATION {
+ // Version of this structure, to allow for modifications in the future.
+ DWORD dwVersion;
+
+ // Attestation format type
+ PCWSTR pwszFormatType;
+
+ // Size of cbAuthenticatorData.
+ DWORD cbAuthenticatorData;
+ // Authenticator data that was created for this credential.
+ _Field_size_bytes_(cbAuthenticatorData)
+ PBYTE pbAuthenticatorData;
+
+ // Size of CBOR encoded attestation information
+ //0 => encoded as CBOR null value.
+ DWORD cbAttestation;
+ //Encoded CBOR attestation information
+ _Field_size_bytes_(cbAttestation)
+ PBYTE pbAttestation;
+
+ DWORD dwAttestationDecodeType;
+ // Following depends on the dwAttestationDecodeType
+ // WEBAUTHN_ATTESTATION_DECODE_NONE
+ // NULL - not able to decode the CBOR attestation information
+ // WEBAUTHN_ATTESTATION_DECODE_COMMON
+ // PWEBAUTHN_COMMON_ATTESTATION;
+ PVOID pvAttestationDecode;
+
+ // The CBOR encoded Attestation Object to be returned to the RP.
+ DWORD cbAttestationObject;
+ _Field_size_bytes_(cbAttestationObject)
+ PBYTE pbAttestationObject;
+
+ // The CredentialId bytes extracted from the Authenticator Data.
+ // Used by Edge to return to the RP.
+ DWORD cbCredentialId;
+ _Field_size_bytes_(cbCredentialId)
+ PBYTE pbCredentialId;
+
+ //
+ // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_2
+ //
+
+ WEBAUTHN_EXTENSIONS Extensions;
+
+ //
+ // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_3
+ //
+
+ // One of the WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to
+ // the transport that was used.
+ DWORD dwUsedTransport;
+
+ //
+ // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_4
+ //
+
+ BOOL bEpAtt;
+ BOOL bLargeBlobSupported;
+ BOOL bResidentKey;
+
+ //
+ // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5
+ //
+
+ BOOL bPrfEnabled;
+
+ //
+ // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_6
+ //
+
+ DWORD cbUnsignedExtensionOutputs;
+ _Field_size_bytes_(cbUnsignedExtensionOutputs)
+ PBYTE pbUnsignedExtensionOutputs;
+} WEBAUTHN_CREDENTIAL_ATTESTATION, *PWEBAUTHN_CREDENTIAL_ATTESTATION;
+typedef const WEBAUTHN_CREDENTIAL_ATTESTATION *PCWEBAUTHN_CREDENTIAL_ATTESTATION;
+
+
+//+------------------------------------------------------------------------------------------
+// authenticatorGetAssertion output.
+//-------------------------------------------------------------------------------------------
+
+#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_NONE 0
+#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_SUCCESS 1
+#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_NOT_SUPPORTED 2
+#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_INVALID_DATA 3
+#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_INVALID_PARAMETER 4
+#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_NOT_FOUND 5
+#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_MULTIPLE_CREDENTIALS 6
+#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_LACK_OF_SPACE 7
+#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_PLATFORM_ERROR 8
+#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_AUTHENTICATOR_ERROR 9
+
+#define WEBAUTHN_ASSERTION_VERSION_1 1
+#define WEBAUTHN_ASSERTION_VERSION_2 2
+#define WEBAUTHN_ASSERTION_VERSION_3 3
+#define WEBAUTHN_ASSERTION_VERSION_4 4
+#define WEBAUTHN_ASSERTION_VERSION_5 5
+#define WEBAUTHN_ASSERTION_CURRENT_VERSION WEBAUTHN_ASSERTION_VERSION_5
+
+typedef struct _WEBAUTHN_ASSERTION {
+ // Version of this structure, to allow for modifications in the future.
+ DWORD dwVersion;
+
+ // Size of cbAuthenticatorData.
+ DWORD cbAuthenticatorData;
+ // Authenticator data that was created for this assertion.
+ _Field_size_bytes_(cbAuthenticatorData)
+ PBYTE pbAuthenticatorData;
+
+ // Size of pbSignature.
+ DWORD cbSignature;
+ // Signature that was generated for this assertion.
+ _Field_size_bytes_(cbSignature)
+ PBYTE pbSignature;
+
+ // Credential that was used for this assertion.
+ WEBAUTHN_CREDENTIAL Credential;
+
+ // Size of User Id
+ DWORD cbUserId;
+ // UserId
+ _Field_size_bytes_(cbUserId)
+ PBYTE pbUserId;
+
+ //
+ // Following fields have been added in WEBAUTHN_ASSERTION_VERSION_2
+ //
+
+ WEBAUTHN_EXTENSIONS Extensions;
+
+ // Size of pbCredLargeBlob
+ DWORD cbCredLargeBlob;
+ _Field_size_bytes_(cbCredLargeBlob)
+ PBYTE pbCredLargeBlob;
+
+ DWORD dwCredLargeBlobStatus;
+
+ //
+ // Following fields have been added in WEBAUTHN_ASSERTION_VERSION_3
+ //
+
+ PWEBAUTHN_HMAC_SECRET_SALT pHmacSecret;
+
+ //
+ // Following fields have been added in WEBAUTHN_ASSERTION_VERSION_4
+ //
+
+ // One of the WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to
+ // the transport that was used.
+ DWORD dwUsedTransport;
+
+ //
+ // Following fields have been added in WEBAUTHN_ASSERTION_VERSION_5
+ //
+
+ DWORD cbUnsignedExtensionOutputs;
+ _Field_size_bytes_(cbUnsignedExtensionOutputs)
+ PBYTE pbUnsignedExtensionOutputs;
+} WEBAUTHN_ASSERTION, *PWEBAUTHN_ASSERTION;
+typedef const WEBAUTHN_ASSERTION *PCWEBAUTHN_ASSERTION;
+
+//+------------------------------------------------------------------------------------------
+// APIs.
+//-------------------------------------------------------------------------------------------
+
+DWORD
+WINAPI
+WebAuthNGetApiVersionNumber();
+
+HRESULT
+WINAPI
+WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable(
+ _Out_ BOOL *pbIsUserVerifyingPlatformAuthenticatorAvailable);
+
+
+HRESULT
+WINAPI
+WebAuthNAuthenticatorMakeCredential(
+ _In_ HWND hWnd,
+ _In_ PCWEBAUTHN_RP_ENTITY_INFORMATION pRpInformation,
+ _In_ PCWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation,
+ _In_ PCWEBAUTHN_COSE_CREDENTIAL_PARAMETERS pPubKeyCredParams,
+ _In_ PCWEBAUTHN_CLIENT_DATA pWebAuthNClientData,
+ _In_opt_ PCWEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS pWebAuthNMakeCredentialOptions,
+ _Outptr_result_maybenull_ PWEBAUTHN_CREDENTIAL_ATTESTATION *ppWebAuthNCredentialAttestation);
+
+
+HRESULT
+WINAPI
+WebAuthNAuthenticatorGetAssertion(
+ _In_ HWND hWnd,
+ _In_ LPCWSTR pwszRpId,
+ _In_ PCWEBAUTHN_CLIENT_DATA pWebAuthNClientData,
+ _In_opt_ PCWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS pWebAuthNGetAssertionOptions,
+ _Outptr_result_maybenull_ PWEBAUTHN_ASSERTION *ppWebAuthNAssertion);
+
+void
+WINAPI
+WebAuthNFreeCredentialAttestation(
+ _In_opt_ PWEBAUTHN_CREDENTIAL_ATTESTATION pWebAuthNCredentialAttestation);
+
+void
+WINAPI
+WebAuthNFreeAssertion(
+ _In_ PWEBAUTHN_ASSERTION pWebAuthNAssertion);
+
+HRESULT
+WINAPI
+WebAuthNGetCancellationId(
+ _Out_ GUID* pCancellationId);
+
+HRESULT
+WINAPI
+WebAuthNCancelCurrentOperation(
+ _In_ const GUID* pCancellationId);
+
+// Returns NTE_NOT_FOUND when credentials are not found.
+HRESULT
+WINAPI
+WebAuthNGetPlatformCredentialList(
+ _In_ PCWEBAUTHN_GET_CREDENTIALS_OPTIONS pGetCredentialsOptions,
+ _Outptr_result_maybenull_ PWEBAUTHN_CREDENTIAL_DETAILS_LIST *ppCredentialDetailsList);
+
+void
+WINAPI
+WebAuthNFreePlatformCredentialList(
+ _In_ PWEBAUTHN_CREDENTIAL_DETAILS_LIST pCredentialDetailsList);
+
+HRESULT
+WINAPI
+WebAuthNDeletePlatformCredential(
+ _In_ DWORD cbCredentialId,
+ _In_reads_bytes_(cbCredentialId) const BYTE *pbCredentialId
+ );
+
+//
+// Returns the following Error Names:
+// L"Success" - S_OK
+// L"InvalidStateError" - NTE_EXISTS
+// L"ConstraintError" - HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
+// NTE_NOT_SUPPORTED,
+// NTE_TOKEN_KEYSET_STORAGE_FULL
+// L"NotSupportedError" - NTE_INVALID_PARAMETER
+// L"NotAllowedError" - NTE_DEVICE_NOT_FOUND,
+// NTE_NOT_FOUND,
+// HRESULT_FROM_WIN32(ERROR_CANCELLED),
+// NTE_USER_CANCELLED,
+// HRESULT_FROM_WIN32(ERROR_TIMEOUT)
+// L"UnknownError" - All other hr values
+//
+PCWSTR
+WINAPI
+WebAuthNGetErrorName(
+ _In_ HRESULT hr);
+
+HRESULT
+WINAPI
+WebAuthNGetW3CExceptionDOMError(
+ _In_ HRESULT hr);
+
+
+#ifdef __cplusplus
+} // Balance extern "C" above
+#endif
+
+#endif // WINAPI_FAMILY_PARTITION
+#pragma endregion
+
+#endif // __WEBAUTHN_H_
|