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