already_AddRefed<Promise> WebAuthentication::GetAssertion(const ArrayBufferViewOrArrayBuffer& aChallenge, const AssertionOptions& aOptions) { MOZ_ASSERT(mParent); nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); if (!global) { return nullptr; } // 4.1.2.1 If timeoutSeconds was specified, check if its value lies within a // reasonable range as defined by the platform and if not, correct it to the // closest value lying within that range. double adjustedTimeout = 30.0; if (aOptions.mTimeoutSeconds.WasPassed()) { adjustedTimeout = aOptions.mTimeoutSeconds.Value(); adjustedTimeout = std::max(15.0, adjustedTimeout); adjustedTimeout = std::min(120.0, adjustedTimeout); } // 4.1.2.2 Let promise be a new Promise. Return promise and start a timer for // adjustedTimeout seconds. RefPtr<AssertionRequest> requestMonitor = new AssertionRequest(); requestMonitor->SetDeadline(TimeDuration::FromSeconds(adjustedTimeout)); ErrorResult rv; RefPtr<Promise> promise = Promise::Create(global, rv); nsresult initRv = InitLazily(); if (NS_FAILED(initRv)) { promise->MaybeReject(initRv); return promise.forget(); } if (mOrigin.EqualsLiteral("null")) { // 4.1.2.3 If callerOrigin is an opaque origin, reject promise with a // DOMException whose name is "NotAllowedError", and terminate this algorithm promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR); return promise.forget(); } nsCString rpId; if (!aOptions.mRpId.WasPassed()) { // 4.1.2.3.a If rpId is not specified, then set rpId to callerOrigin, and // rpIdHash to the SHA-256 hash of rpId. rpId.Assign(NS_ConvertUTF16toUTF8(mOrigin)); } else { // 4.1.2.3.b If rpId is specified, then invoke the procedure used for // relaxing the same-origin restriction by setting the document.domain // attribute, using rpId as the given value but without changing the current // document’s domain. If no errors are thrown, set rpId to the value of host // as computed by this procedure, and rpIdHash to the SHA-256 hash of rpId. // Otherwise, reject promise with a DOMException whose name is // "SecurityError", and terminate this algorithm. 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(AbstractThread::MainThread(), __func__, [promise] (AssertionPtr aAssertion) { promise->MaybeResolve(aAssertion); }, [promise] (nsresult aErrorCode) { promise->MaybeReject(aErrorCode); }); return promise.forget(); }
NS_IMETHODIMP U2FSignTask::Run() { nsNSSShutDownPreventionLock locker; if (isAlreadyShutDown()) { ReturnError(ErrorCode::OTHER_ERROR); return NS_ERROR_FAILURE; } // Search the requests for one a token can fulfill for (size_t i = 0; i < mRegisteredKeys.Length(); i += 1) { RegisteredKey request(mRegisteredKeys[i]); // Check for required attributes if (!(request.mVersion.WasPassed() && request.mKeyHandle.WasPassed())) { continue; } // Do not permit an individual RegisteredKey to assert a different AppID if (request.mAppId.WasPassed() && mAppId != request.mAppId.Value()) { continue; } // Assemble a clientData object CryptoBuffer clientData; nsresult rv = AssembleClientData(mOrigin, kGetAssertion, mChallenge, clientData); if (NS_WARN_IF(NS_FAILED(rv))) { ReturnError(ErrorCode::OTHER_ERROR); return NS_ERROR_FAILURE; } // Hash the AppID and the ClientData into the AppParam and ChallengeParam SECStatus srv; nsCString cAppId = NS_ConvertUTF16toUTF8(mAppId); CryptoBuffer appParam; CryptoBuffer challengeParam; if (!appParam.SetLength(SHA256_LENGTH, fallible) || !challengeParam.SetLength(SHA256_LENGTH, fallible)) { ReturnError(ErrorCode::OTHER_ERROR); return NS_ERROR_FAILURE; } srv = PK11_HashBuf(SEC_OID_SHA256, appParam.Elements(), reinterpret_cast<const uint8_t*>(cAppId.BeginReading()), cAppId.Length()); if (srv != SECSuccess) { ReturnError(ErrorCode::OTHER_ERROR); return NS_ERROR_FAILURE; } srv = PK11_HashBuf(SEC_OID_SHA256, challengeParam.Elements(), clientData.Elements(), clientData.Length()); if (srv != SECSuccess) { ReturnError(ErrorCode::OTHER_ERROR); return NS_ERROR_FAILURE; } // Decode the key handle CryptoBuffer keyHandle; rv = keyHandle.FromJwkBase64(request.mKeyHandle.Value()); if (NS_WARN_IF(NS_FAILED(rv))) { ReturnError(ErrorCode::OTHER_ERROR); return NS_ERROR_FAILURE; } // Get the signature from the token CryptoBuffer signatureData; bool signSuccess = false; // We ignore mTransports, as it is intended to be used for sorting the // available devices by preference, but is not an exclusion factor. for (size_t a = 0; a < mAuthenticators.Length() && !signSuccess; ++a) { Authenticator token(mAuthenticators[a]); bool isCompatible = false; bool isRegistered = false; rv = token->IsCompatibleVersion(request.mVersion.Value(), &isCompatible); if (NS_FAILED(rv)) { ReturnError(ErrorCode::OTHER_ERROR); return NS_ERROR_FAILURE; } if (!isCompatible) { continue; } rv = token->IsRegistered(keyHandle.Elements(), keyHandle.Length(), &isRegistered); if (NS_FAILED(rv)) { ReturnError(ErrorCode::OTHER_ERROR); return NS_ERROR_FAILURE; } if (isCompatible && isRegistered) { uint8_t* buffer; uint32_t bufferlen; nsresult rv = token->Sign(appParam.Elements(), appParam.Length(), challengeParam.Elements(), challengeParam.Length(), keyHandle.Elements(), keyHandle.Length(), &buffer, &bufferlen); if (NS_FAILED(rv)) { ReturnError(ErrorCode::OTHER_ERROR); return NS_ERROR_FAILURE; } MOZ_ASSERT(buffer); signatureData.Assign(buffer, bufferlen); free(buffer); signSuccess = true; } } if (!signSuccess) { // Try another request continue; } // Assemble a response object to return nsString clientDataBase64, signatureDataBase64; nsresult rvClientData = clientData.ToJwkBase64(clientDataBase64); nsresult rvSignatureData = signatureData.ToJwkBase64(signatureDataBase64); if (NS_WARN_IF(NS_FAILED(rvClientData)) || NS_WARN_IF(NS_FAILED(rvSignatureData))) { ReturnError(ErrorCode::OTHER_ERROR); return NS_ERROR_FAILURE; } SignResponse response; response.mKeyHandle.Construct(request.mKeyHandle.Value()); response.mClientData.Construct(clientDataBase64); response.mSignatureData.Construct(signatureDataBase64); response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OK)); ErrorResult result; mCallback->Call(response, result); NS_WARN_IF(result.Failed()); // Useful exceptions already got reported. result.SuppressException(); return NS_OK; } // Nothing could satisfy ReturnError(ErrorCode::DEVICE_INELIGIBLE); return NS_ERROR_FAILURE; }
already_AddRefed<Promise> WebAuthentication::MakeCredential(JSContext* aCx, const Account& aAccount, const Sequence<ScopedCredentialParameters>& aCryptoParameters, const ArrayBufferViewOrArrayBuffer& aChallenge, const ScopedCredentialOptions& aOptions) { MOZ_ASSERT(mParent); nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); if (!global) { return nullptr; } ErrorResult rv; RefPtr<Promise> promise = Promise::Create(global, rv); nsresult initRv = InitLazily(); if (NS_FAILED(initRv)) { promise->MaybeReject(initRv); return promise.forget(); } // 4.1.1.1 If timeoutSeconds was specified, check if its value lies within a // reasonable range as defined by the platform and if not, correct it to the // closest value lying within that range. double adjustedTimeout = 30.0; if (aOptions.mTimeoutSeconds.WasPassed()) { adjustedTimeout = aOptions.mTimeoutSeconds.Value(); adjustedTimeout = std::max(15.0, adjustedTimeout); adjustedTimeout = std::min(120.0, adjustedTimeout); } // 4.1.1.2 Let promise be a new Promise. Return promise and start a timer for // adjustedTimeout seconds. RefPtr<CredentialRequest> requestMonitor = new CredentialRequest(); requestMonitor->SetDeadline(TimeDuration::FromSeconds(adjustedTimeout)); if (mOrigin.EqualsLiteral("null")) { // 4.1.1.3 If callerOrigin is an opaque origin, reject promise with a // DOMException whose name is "NotAllowedError", and terminate this // algorithm MOZ_LOG(gWebauthLog, LogLevel::Debug, ("Rejecting due to opaque origin")); promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR); return promise.forget(); } nsCString rpId; if (!aOptions.mRpId.WasPassed()) { // 4.1.1.3.a If rpId is not specified, then set rpId to callerOrigin, and // rpIdHash to the SHA-256 hash of rpId. rpId.Assign(NS_ConvertUTF16toUTF8(mOrigin)); } else { // 4.1.1.3.b If rpId is specified, then invoke the procedure used for // relaxing the same-origin restriction by setting the document.domain // attribute, using rpId as the given value but without changing the current // document’s domain. If no errors are thrown, set rpId to the value of host // as computed by this procedure, and rpIdHash to the SHA-256 hash of rpId. // Otherwise, reject promise with a DOMException whose name is // "SecurityError", and terminate this algorithm. 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(AbstractThread::MainThread(), __func__, [promise] (CredentialPtr aInfo) { promise->MaybeResolve(aInfo); }, [promise] (nsresult aErrorCode) { promise->MaybeReject(aErrorCode); }); return promise.forget(); }