bool ReadAndEncodeAttribute(SECKEYPrivateKey* aKey, CK_ATTRIBUTE_TYPE aAttribute, Optional<nsString>& aDst) { ScopedSECItem item(::SECITEM_AllocItem(nullptr, nullptr, 0)); if (!item) { return false; } if (PK11_ReadRawAttribute(PK11_TypePrivKey, aKey, aAttribute, item) != SECSuccess) { return false; } CryptoBuffer buffer; if (!buffer.Assign(item)) { return false; } if (NS_FAILED(buffer.ToJwkBase64(aDst.Value()))) { return false; } return true; }
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; }
nsresult CryptoKey::PublicKeyToSpki(SECKEYPublicKey* aPubKey, CryptoBuffer& aRetVal, const nsNSSShutDownPreventionLock& /*proofOfLock*/) { ScopedCERTSubjectPublicKeyInfo spki(SECKEY_CreateSubjectPublicKeyInfo(aPubKey)); if (!spki) { return NS_ERROR_DOM_OPERATION_ERR; } // Per WebCrypto spec we must export ECDH SPKIs with the algorithm OID // id-ecDH (1.3.132.112). NSS doesn't know about that OID and there is // no way to specify the algorithm to use when exporting a public key. if (aPubKey->keyType == ecKey) { SECStatus rv = SECITEM_CopyItem(spki->arena, &spki->algorithm.algorithm, &SEC_OID_DATA_EC_DH); if (rv != SECSuccess) { return NS_ERROR_DOM_OPERATION_ERR; } } const SEC_ASN1Template* tpl = SEC_ASN1_GET(CERT_SubjectPublicKeyInfoTemplate); ScopedSECItem spkiItem(SEC_ASN1EncodeItem(nullptr, nullptr, spki, tpl)); aRetVal.Assign(spkiItem.get()); return NS_OK; }
nsresult CryptoKey::PublicDhKeyToRaw(SECKEYPublicKey* aPubKey, CryptoBuffer& aRetVal, const nsNSSShutDownPreventionLock& /*proofOfLock*/) { aRetVal.Assign(&aPubKey->u.dh.publicValue); return NS_OK; }
nsresult CryptoKey::PublicECKeyToRaw(SECKEYPublicKey* aPubKey, CryptoBuffer& aRetVal, const nsNSSShutDownPreventionLock& /*proofOfLock*/) { if (!aRetVal.Assign(&aPubKey->u.ec.publicValue)) { return NS_ERROR_DOM_OPERATION_ERR; } return NS_OK; }
nsresult CryptoKey::PrivateKeyToPkcs8(SECKEYPrivateKey* aPrivKey, CryptoBuffer& aRetVal, const nsNSSShutDownPreventionLock& /*proofOfLock*/) { ScopedSECItem pkcs8Item(PK11_ExportDERPrivateKeyInfo(aPrivKey, nullptr)); if (!pkcs8Item.get()) { return NS_ERROR_DOM_INVALID_ACCESS_ERR; } aRetVal.Assign(pkcs8Item.get()); return NS_OK; }
nsresult CryptoKey::PublicKeyToSpki(SECKEYPublicKey* aPubKey, CryptoBuffer& aRetVal, const nsNSSShutDownPreventionLock& /*proofOfLock*/) { ScopedSECItem spkiItem(PK11_DEREncodePublicKey(aPubKey)); if (!spkiItem.get()) { return NS_ERROR_DOM_INVALID_ACCESS_ERR; } aRetVal.Assign(spkiItem.get()); return NS_OK; }
static nsresult AssembleClientData(const nsAString& aOrigin, const nsAString& aTyp, const nsAString& aChallenge, CryptoBuffer& aClientData) { ClientData clientDataObject; clientDataObject.mTyp.Construct(aTyp); // "Typ" from the U2F specification clientDataObject.mChallenge.Construct(aChallenge); clientDataObject.mOrigin.Construct(aOrigin); nsAutoString json; if (NS_WARN_IF(!clientDataObject.ToJSON(json))) { return NS_ERROR_FAILURE; } if (NS_WARN_IF(!aClientData.Assign(NS_ConvertUTF16toUTF8(json)))) { return NS_ERROR_FAILURE; } return NS_OK; }
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; }
nsresult CryptoKey::PublicKeyToSpki(SECKEYPublicKey* aPubKey, CryptoBuffer& aRetVal, const nsNSSShutDownPreventionLock& /*proofOfLock*/) { ScopedCERTSubjectPublicKeyInfo spki; // NSS doesn't support exporting DH public keys. if (aPubKey->keyType == dhKey) { // Mimic the behavior of SECKEY_CreateSubjectPublicKeyInfo() and create // a new arena for the SPKI object. ScopedPLArenaPool arena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE)); if (!arena) { return NS_ERROR_DOM_OPERATION_ERR; } spki = PORT_ArenaZNew(arena, CERTSubjectPublicKeyInfo); if (!spki) { return NS_ERROR_DOM_OPERATION_ERR; } // Assign |arena| to |spki| and null the variable afterwards so that the // arena created above that holds the SPKI object is free'd when |spki| // goes out of scope, not when |arena| does. spki->arena = arena.forget(); nsresult rv = PublicDhKeyToSpki(aPubKey, spki); NS_ENSURE_SUCCESS(rv, rv); } else { spki = SECKEY_CreateSubjectPublicKeyInfo(aPubKey); if (!spki) { return NS_ERROR_DOM_OPERATION_ERR; } } // Per WebCrypto spec we must export ECDH SPKIs with the algorithm OID // id-ecDH (1.3.132.112) and DH SPKIs with OID dhKeyAgreement // (1.2.840.113549.1.3.1). NSS doesn't know about these OIDs and there is // no way to specify the algorithm to use when exporting a public key. if (aPubKey->keyType == ecKey || aPubKey->keyType == dhKey) { const SECItem* oidData = nullptr; if (aPubKey->keyType == ecKey) { oidData = &SEC_OID_DATA_EC_DH; } else if (aPubKey->keyType == dhKey) { oidData = &SEC_OID_DATA_DH_KEY_AGREEMENT; } else { MOZ_ASSERT(false); } SECStatus rv = SECITEM_CopyItem(spki->arena, &spki->algorithm.algorithm, oidData); if (rv != SECSuccess) { return NS_ERROR_DOM_OPERATION_ERR; } } const SEC_ASN1Template* tpl = SEC_ASN1_GET(CERT_SubjectPublicKeyInfoTemplate); ScopedSECItem spkiItem(SEC_ASN1EncodeItem(nullptr, nullptr, spki, tpl)); aRetVal.Assign(spkiItem.get()); return NS_OK; }
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(); }
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(); }
// 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(), &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); }
// 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, &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); }