diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp index 8165a7d91d..13b612db5d 100644 --- a/assignment-client/src/avatars/MixerAvatar.cpp +++ b/assignment-client/src/avatars/MixerAvatar.cpp @@ -24,30 +24,22 @@ #include "MixerAvatar.h" #include "AvatarLogging.h" -#include -#include -#include -#include -#include -#include -#include - - void MixerAvatar::fetchAvatarFST() { _verifyState = kNoncertified; _certificateIdFromURL.clear(); _certificateIdFromFST.clear(); _marketplaceIdFromURL.clear(); + _marketplaceIdFromFST.clear(); auto resourceManager = DependencyManager::get(); QUrl avatarURL = getSkeletonModelURL(); if (avatarURL.isEmpty()) { return; } - auto avatarURLString = avatarURL.toDisplayString(); - // Match UUID + version + //auto avatarURLString = avatarURL.toDisplayString(); + // Match UUID + (optionally) URL cert static const QRegularExpression marketIdRegex{ - "^https://metaverse.highfidelity.com/api/v.+/commerce/entity_edition/([-0-9a-z]{36}).*?(certificate_id=([\\w/+%]+)).*$" + "^https://metaverse.highfidelity.com/api/v.+/commerce/entity_edition/([-0-9a-z]{36})(.*?certificate_id=([\\w/+%]+)|.*).*$" }; auto marketIdMatch = marketIdRegex.match(avatarURL.toDisplayString()); if (marketIdMatch.hasMatch()) { @@ -86,45 +78,6 @@ void MixerAvatar::fstRequestComplete() { } else { _avatarFSTContents = fstRequest->getData(); _verifyState = kReceivedFST; - generateFSTHash(); - QString& marketplacePublicKey = EntityItem::_marketplacePublicKey; - bool staticVerification = validateFSTHash(marketplacePublicKey); - _verifyState = staticVerification ? kStaticValidation : kVerificationFailed; - - if (_verifyState == kStaticValidation) { - static const QString POP_MARKETPLACE_API{ "/api/v1/commerce/proof_of_purchase_status/transfer" }; - auto& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest; - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); - requestURL.setPath(POP_MARKETPLACE_API); - networkRequest.setUrl(requestURL); - - QJsonObject request; - request["certificate_id"] = _certificateIdFromFST; - _verifyState = kRequestingOwner; - QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); - networkReply->setParent(this); - connect(networkReply, &QNetworkReply::finished, [this, networkReply]() { - QMutexLocker certifyLocker(&_avatarCertifyLock); - if (networkReply->error() == QNetworkReply::NoError) { - _dynamicMarketResponse = networkReply->readAll(); - _verifyState = kOwnerResponse; - } else { - auto jsonData = QJsonDocument::fromJson(networkReply->readAll())["data"]; - if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) { - qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << ":" - << jsonData.toObject()["message"].toString(); - _verifyState = kError; - } - } - networkReply->deleteLater(); - }); - } else { - _verifyState = kVerificationFailed; - qCDebug(avatars) << "Avatar" << getDisplayName() << "FAILED static certification"; - } } _avatarRequest->deleteLater(); _avatarRequest = nullptr; @@ -184,6 +137,9 @@ QByteArray MixerAvatar::canonicalJson(const QString fstFile) { scripts.append(lineMatch.captured(2).trimmed()); } else { certifiedItems[key] = QJsonValue(lineMatch.captured(2)); + if (key == "marketplaceID") { + _marketplaceIdFromFST = lineMatch.captured(2); + } } } } @@ -207,10 +163,53 @@ void MixerAvatar::processCertifyEvents() { } switch (_verifyState) { + + case kReceivedFST: + { + generateFSTHash(); + QString& marketplacePublicKey = EntityItem::_marketplacePublicKey; + bool staticVerification = validateFSTHash(marketplacePublicKey); + _verifyState = staticVerification ? kStaticValidation : kVerificationFailed; + + if (_verifyState == kStaticValidation) { + static const QString POP_MARKETPLACE_API { "/api/v1/commerce/proof_of_purchase_status/transfer" }; + auto& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest; + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); + requestURL.setPath(POP_MARKETPLACE_API); + networkRequest.setUrl(requestURL); + + QJsonObject request; + request["certificate_id"] = _certificateIdFromFST; + _verifyState = kRequestingOwner; + QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); + networkReply->setParent(this); + connect(networkReply, &QNetworkReply::finished, [this, networkReply]() { + QMutexLocker certifyLocker(&_avatarCertifyLock); + if (networkReply->error() == QNetworkReply::NoError) { + _dynamicMarketResponse = networkReply->readAll(); + _verifyState = kOwnerResponse; + } else { + auto jsonData = QJsonDocument::fromJson(networkReply->readAll())["data"]; + if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) { + qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << ":" + << jsonData.toObject()["message"].toString(); + _verifyState = kError; + } + } + networkReply->deleteLater(); + }); + } else { + _verifyState = kVerificationFailedPending; + qCDebug(avatars) << "Avatar" << getDisplayName() << "FAILED static certification"; + } + } + case kOwnerResponse: { QJsonDocument responseJson = QJsonDocument::fromJson(_dynamicMarketResponse.toUtf8()); - _verifyState = kChallengeClient; QString ownerPublicKey; bool ownerValid = false; qCDebug(avatars) << "Marketplace response for avatar" << getDisplayName() << ":" << _dynamicMarketResponse; @@ -227,10 +226,15 @@ void MixerAvatar::processCertifyEvents() { } } if (ownerValid && !ownerPublicKey.isEmpty()) { - _ownerPublicKey = "-----BEGIN PUBLIC KEY-----\n" - + ownerPublicKey - + "\n-----END PUBLIC KEY-----\n"; - challengeOwner(); + if (ownerPublicKey.startsWith("-----BEGIN ")){ + _ownerPublicKey = ownerPublicKey; + } else { + _ownerPublicKey = "-----BEGIN PUBLIC KEY-----\n" + + ownerPublicKey + + "\n-----END PUBLIC KEY-----\n"; + } + sendOwnerChallenge(); + _verifyState = kChallengeClient; } else { _verifyState = kError; } @@ -244,35 +248,37 @@ void MixerAvatar::processCertifyEvents() { case kChallengeResponse: { - int avatarIDLength; - int signedNonceLength; if (_challengeResponse.length() < 8) { _verifyState = kError; break; } - QDataStream responseStream(_challengeResponse); - responseStream.setByteOrder(QDataStream::LittleEndian); - responseStream >> avatarIDLength >> signedNonceLength; + int avatarIDLength; + int signedNonceLength; + { + QDataStream responseStream(_challengeResponse); + responseStream.setByteOrder(QDataStream::LittleEndian); + responseStream >> avatarIDLength >> signedNonceLength; + } QByteArray avatarID(_challengeResponse.data() + 2 * sizeof(int), avatarIDLength); QByteArray signedNonce(_challengeResponse.data() + 2 * sizeof(int) + avatarIDLength, signedNonceLength); - QCryptographicHash nonceHash(QCryptographicHash::Sha256); - nonceHash.addData(_challengeNonce); - bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, nonceHash.result(), + bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, _challengeNonceHash, QByteArray::fromBase64(signedNonce)); - _verifyState = challengeResult ? kVerificationSucceeded : kVerificationFailed; - if (_verifyState == kVerificationFailed) { + _verifyState = challengeResult ? kVerificationSucceeded : kVerificationFailedPending; + if (_verifyState == kVerificationFailedPending) { qCDebug(avatars) << "Dynamic verification FAILED for " << getDisplayName() << getSessionUUID(); + } else { + qCDebug(avatars) << "Dynamic verification SUCCEEDED for " << getDisplayName() << getSessionUUID(); } } } // close switch } -void MixerAvatar::challengeOwner() { +void MixerAvatar::sendOwnerChallenge() { auto nodeList = DependencyManager::get(); - QByteArray avatarID = ("{" + _marketplaceIdFromURL + "}").toUtf8(); + QByteArray avatarID = ("{" + _marketplaceIdFromFST + "}").toUtf8(); QByteArray nonce = QUuid::createUuid().toByteArray(); auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnership, @@ -283,7 +289,9 @@ void MixerAvatar::challengeOwner() { challengeOwnershipPacket->write(nonce); nodeList->sendPacket(std::move(challengeOwnershipPacket), *(nodeList->nodeWithUUID(getSessionUUID())) ); - _challengeNonce = nonce; + QCryptographicHash nonceHash(QCryptographicHash::Sha256); + nonceHash.addData(nonce); + _challengeNonceHash = nonceHash.result(); static constexpr int CHALLENGE_TIMEOUT_MS = 10 * 1000; // 10 s _challengeTimeout.setInterval(CHALLENGE_TIMEOUT_MS); diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index 57896b2876..1d5dbc62ae 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -25,7 +25,7 @@ public: void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; } void fetchAvatarFST(); - bool isCertifyFailed() const { return _verifyState == kVerificationFailed; } + bool isCertifyFailed() const { return _verifyState == kVerificationFailed || _verifyState == kVerificationFailedPending; } void processCertifyEvents(); void handleChallengeResponse(ReceivedMessage * response); @@ -34,26 +34,28 @@ private: // Avatar certification/verification: enum VerifyState { kNoncertified, kRequestingFST, kReceivedFST, kStaticValidation, kRequestingOwner, kOwnerResponse, - kChallengeClient, kChallengeResponse, kVerified, kVerificationFailed, kVerificationSucceeded, kError }; + kChallengeClient, kChallengeResponse, kVerified, kVerificationFailedPending, kVerificationFailed, + kVerificationSucceeded, kError }; Q_ENUM(VerifyState); VerifyState _verifyState { kNoncertified }; QMutex _avatarCertifyLock; ResourceRequest* _avatarRequest { nullptr }; QString _marketplaceIdFromURL; + QString _marketplaceIdFromFST; QByteArray _avatarFSTContents; QByteArray _certificateHash; QString _certificateIdFromURL; QString _certificateIdFromFST; QString _dynamicMarketResponse; QString _ownerPublicKey; - QByteArray _challengeNonce; + QByteArray _challengeNonceHash; QByteArray _challengeResponse; QTimer _challengeTimeout; bool generateFSTHash(); bool validateFSTHash(const QString& publicKey); QByteArray canonicalJson(const QString fstFile); - void challengeOwner(); + void sendOwnerChallenge(); private slots: void fstRequestComplete();