Passkeys: Pass extension JSON data to browser (#10615)

This commit is contained in:
Sami Vänttinen 2024-04-25 13:29:51 +03:00 committed by GitHub
parent 880621c1fb
commit 5b123e7944
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 51 additions and 11 deletions

View File

@ -141,6 +141,8 @@ QByteArray BrowserCbor::cborEncodeExtensionData(const QJsonObject& extensions) c
QCborStreamWriter writer(&result); QCborStreamWriter writer(&result);
writer.startMap(extensions.keys().count()); writer.startMap(extensions.keys().count());
// https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension
if (extensions["credProps"].toBool()) { if (extensions["credProps"].toBool()) {
writer.append("credProps"); writer.append("credProps");
writer.startMap(1); writer.startMap(1);
@ -149,6 +151,7 @@ QByteArray BrowserCbor::cborEncodeExtensionData(const QJsonObject& extensions) c
writer.endMap(); writer.endMap();
} }
// https://w3c.github.io/webauthn/#sctn-uvm-extension
if (extensions["uvm"].toBool()) { if (extensions["uvm"].toBool()) {
writer.append("uvm"); writer.append("uvm");
@ -167,6 +170,7 @@ QByteArray BrowserCbor::cborEncodeExtensionData(const QJsonObject& extensions) c
writer.endArray(); writer.endArray();
writer.endArray(); writer.endArray();
} }
writer.endMap(); writer.endMap();
return result; return result;

View File

@ -106,6 +106,7 @@ PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJso
QJsonObject responseObject; QJsonObject responseObject;
responseObject["attestationObject"] = browserMessageBuilder()->getBase64FromArray(attestationObject); responseObject["attestationObject"] = browserMessageBuilder()->getBase64FromArray(attestationObject);
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientDataJson); responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientDataJson);
responseObject["clientExtensionResults"] = credentialCreationOptions["clientExtensionResults"];
// PublicKeyCredential // PublicKeyCredential
QJsonObject publicKeyCredential; QJsonObject publicKeyCredential;
@ -142,6 +143,7 @@ QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& asse
QJsonObject responseObject; QJsonObject responseObject;
responseObject["authenticatorData"] = browserMessageBuilder()->getBase64FromArray(authenticatorData); responseObject["authenticatorData"] = browserMessageBuilder()->getBase64FromArray(authenticatorData);
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientDataJson); responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientDataJson);
responseObject["clientExtensionResults"] = assertionOptions["clientExtensionResults"];
responseObject["signature"] = browserMessageBuilder()->getBase64FromArray(signature); responseObject["signature"] = browserMessageBuilder()->getBase64FromArray(signature);
responseObject["userHandle"] = userHandle; responseObject["userHandle"] = userHandle;

View File

@ -101,13 +101,14 @@ int BrowserPasskeysClient::getCredentialCreationOptions(const QJsonObject& publi
// Extensions // Extensions
auto extensionObject = publicKeyOptions["extensions"].toObject(); auto extensionObject = publicKeyOptions["extensions"].toObject();
const auto extensionData = passkeyUtils()->buildExtensionData(extensionObject); const auto extensionData = passkeyUtils()->buildExtensionData(extensionObject);
const auto extensions = browserMessageBuilder()->getBase64FromArray(extensionData); const auto extensions = browserMessageBuilder()->getBase64FromArray(extensionData.extensionData);
// Construct the final object // Construct the final object
QJsonObject credentialCreationOptions; QJsonObject credentialCreationOptions;
credentialCreationOptions["attestation"] = attestation; // Set this, even if only "none" is supported credentialCreationOptions["attestation"] = attestation; // Set this, even if only "none" is supported
credentialCreationOptions["authenticatorAttachment"] = authenticatorAttachment; credentialCreationOptions["authenticatorAttachment"] = authenticatorAttachment;
credentialCreationOptions["clientDataJSON"] = passkeyUtils()->buildClientDataJson(publicKeyOptions, origin, false); credentialCreationOptions["clientDataJSON"] = passkeyUtils()->buildClientDataJson(publicKeyOptions, origin, false);
credentialCreationOptions["clientExtensionResults"] = extensionData.extensionObject;
credentialCreationOptions["credTypesAndPubKeyAlgs"] = pubKeyCredParams; credentialCreationOptions["credTypesAndPubKeyAlgs"] = pubKeyCredParams;
credentialCreationOptions["excludeCredentials"] = publicKeyOptions["excludeCredentials"]; credentialCreationOptions["excludeCredentials"] = publicKeyOptions["excludeCredentials"];
credentialCreationOptions["extensions"] = extensions; credentialCreationOptions["extensions"] = extensions;
@ -148,7 +149,7 @@ int BrowserPasskeysClient::getAssertionOptions(const QJsonObject& publicKeyOptio
// Extensions // Extensions
auto extensionObject = publicKeyOptions["extensions"].toObject(); auto extensionObject = publicKeyOptions["extensions"].toObject();
const auto extensionData = passkeyUtils()->buildExtensionData(extensionObject); const auto extensionData = passkeyUtils()->buildExtensionData(extensionObject);
const auto extensions = browserMessageBuilder()->getBase64FromArray(extensionData); const auto extensions = browserMessageBuilder()->getBase64FromArray(extensionData.extensionData);
// clientDataJson // clientDataJson
const auto clientDataJson = passkeyUtils()->buildClientDataJson(publicKeyOptions, origin, true); const auto clientDataJson = passkeyUtils()->buildClientDataJson(publicKeyOptions, origin, true);
@ -163,6 +164,7 @@ int BrowserPasskeysClient::getAssertionOptions(const QJsonObject& publicKeyOptio
QJsonObject assertionOptions; QJsonObject assertionOptions;
assertionOptions["allowCredentials"] = publicKeyOptions["allowCredentials"]; assertionOptions["allowCredentials"] = publicKeyOptions["allowCredentials"];
assertionOptions["clientDataJson"] = clientDataJson; assertionOptions["clientDataJson"] = clientDataJson;
assertionOptions["clientExtensionResults"] = extensionData.extensionObject;
assertionOptions["extensions"] = extensions; assertionOptions["extensions"] = extensions;
assertionOptions["rpId"] = rpId; assertionOptions["rpId"] = rpId;
assertionOptions["userPresence"] = true; assertionOptions["userPresence"] = true;

View File

@ -305,9 +305,8 @@ bool PasskeyUtils::isUserVerificationRequired(const QJsonObject& authenticatorSe
&& BrowserPasskeys::SUPPORT_USER_VERIFICATION); && BrowserPasskeys::SUPPORT_USER_VERIFICATION);
} }
QByteArray PasskeyUtils::buildExtensionData(QJsonObject& extensionObject) const ExtensionResult PasskeyUtils::buildExtensionData(QJsonObject& extensionObject) const
{ {
// Only supports "credProps" and "uvm" for now
const QStringList allowedKeys = {"credProps", "uvm"}; const QStringList allowedKeys = {"credProps", "uvm"};
// Remove unsupported keys // Remove unsupported keys
@ -317,9 +316,36 @@ QByteArray PasskeyUtils::buildExtensionData(QJsonObject& extensionObject) const
} }
} }
// Create response object
QJsonObject extensionJSON;
// https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension
if (extensionObject.contains("credProps") && extensionObject["credProps"].toBool()) {
extensionJSON["credProps"] = QJsonObject({{"rk", true}});
}
// https://w3c.github.io/webauthn/#sctn-uvm-extension
if (extensionObject.contains("uvm") && extensionObject["uvm"].toBool()) {
QJsonArray uvmResponse;
QJsonArray uvmArray = {
1, // userVerificationMethod (USER_VERIFY_PRESENCE_INTERNAL "presence_internal", 0x00000001)
1, // keyProtectionType (KEY_PROTECTION_SOFTWARE "software", 0x0001)
1, // matcherProtectionType (MATCHER_PROTECTION_SOFTWARE "software", 0x0001)
};
uvmResponse.append(uvmArray);
extensionJSON["uvm"] = uvmResponse;
}
if (extensionJSON.isEmpty()) {
return {};
}
auto extensionData = m_browserCbor.cborEncodeExtensionData(extensionObject); auto extensionData = m_browserCbor.cborEncodeExtensionData(extensionObject);
if (!extensionData.isEmpty()) { if (!extensionData.isEmpty()) {
return extensionData; ExtensionResult result;
result.extensionData = extensionData;
result.extensionObject = extensionJSON;
return result;
} }
return {}; return {};

View File

@ -30,6 +30,12 @@
#define DEFAULT_DISCOURAGED_TIMEOUT 120000 #define DEFAULT_DISCOURAGED_TIMEOUT 120000
#define PASSKEYS_SUCCESS 0 #define PASSKEYS_SUCCESS 0
struct ExtensionResult
{
QByteArray extensionData;
QJsonObject extensionObject;
};
class PasskeyUtils : public QObject class PasskeyUtils : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -51,7 +57,7 @@ public:
bool isResidentKeyRequired(const QJsonObject& authenticatorSelection) const; bool isResidentKeyRequired(const QJsonObject& authenticatorSelection) const;
bool isUserVerificationRequired(const QJsonObject& authenticatorSelection) const; bool isUserVerificationRequired(const QJsonObject& authenticatorSelection) const;
bool isOriginAllowedWithLocalhost(bool allowLocalhostWithPasskeys, const QString& origin) const; bool isOriginAllowedWithLocalhost(bool allowLocalhostWithPasskeys, const QString& origin) const;
QByteArray buildExtensionData(QJsonObject& extensionObject) const; ExtensionResult buildExtensionData(QJsonObject& extensionObject) const;
QJsonObject buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const; QJsonObject buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const;
QStringList getAllowedCredentialsFromAssertionOptions(const QJsonObject& assertionOptions) const; QStringList getAllowedCredentialsFromAssertionOptions(const QJsonObject& assertionOptions) const;
QString getCredentialIdFromEntry(const Entry* entry) const; QString getCredentialIdFromEntry(const Entry* entry) const;

View File

@ -459,9 +459,9 @@ void TestPasskeys::testExtensions()
auto result = passkeyUtils()->buildExtensionData(extensions); auto result = passkeyUtils()->buildExtensionData(extensions);
BrowserCbor cbor; BrowserCbor cbor;
auto extensionJson = cbor.getJsonFromCborData(result); auto extensionJson = cbor.getJsonFromCborData(result.extensionData);
auto uvmArray = extensionJson["uvm"].toArray(); auto uvmArray = result.extensionObject["uvm"].toArray();
QCOMPARE(extensionJson["credProps"].toObject()["rk"].toBool(), true); QCOMPARE(result.extensionObject["credProps"].toObject()["rk"].toBool(), true);
QCOMPARE(uvmArray.size(), 1); QCOMPARE(uvmArray.size(), 1);
QCOMPARE(uvmArray.first().toArray().size(), 3); QCOMPARE(uvmArray.first().toArray().size(), 3);
@ -470,10 +470,10 @@ void TestPasskeys::testExtensions()
auto partialData = passkeyUtils()->buildExtensionData(partial); auto partialData = passkeyUtils()->buildExtensionData(partial);
auto faultyData = passkeyUtils()->buildExtensionData(faulty); auto faultyData = passkeyUtils()->buildExtensionData(faulty);
auto partialJson = cbor.getJsonFromCborData(partialData); auto partialJson = cbor.getJsonFromCborData(partialData.extensionData);
QCOMPARE(partialJson["uvm"].toArray().size(), 1); QCOMPARE(partialJson["uvm"].toArray().size(), 1);
auto faultyJson = cbor.getJsonFromCborData(faultyData); auto faultyJson = cbor.getJsonFromCborData(faultyData.extensionData);
QCOMPARE(faultyJson.size(), 0); QCOMPARE(faultyJson.size(), 0);
} }