Passkeys: Add publicKey to register response (#12757)

This commit is contained in:
Sami Vänttinen 2026-01-17 15:01:13 +02:00 committed by Janek Bevendorff
parent d6c708e5a7
commit ab7c4f87f7
3 changed files with 53 additions and 31 deletions

View file

@ -79,7 +79,7 @@ PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJso
// Credential private key
const auto alg = getAlgorithmFromPublicKey(credentialCreationOptions);
const auto privateKey = buildCredentialPrivateKey(alg, testingVariables.first, testingVariables.second);
const auto privateKey = buildCredentialPrivateKey(alg, testingVariables);
if (privateKey.cborEncodedPublicKey.isEmpty() && privateKey.privateKeyPem.isEmpty()) {
// Key creation failed
return {};
@ -103,6 +103,9 @@ PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJso
// Additions for extension side functions
responseObject["authenticatorData"] = browserMessageBuilder()->getBase64FromArray(authenticatorData);
// PublicKey
responseObject["publicKey"] = browserMessageBuilder()->getBase64FromArray(privateKey.spkiPublicKey);
responseObject["publicKeyAlgorithm"] = alg;
// PublicKeyCredential
@ -224,8 +227,7 @@ QByteArray BrowserPasskeys::buildAuthenticatorData(const QString& rpId, const QS
}
// See: https://w3c.github.io/webauthn/#sctn-encoded-credPubKey-examples
AttestationKeyPair
BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFirst, const QString& predefinedSecond)
AttestationKeyPair BrowserPasskeys::buildCredentialPrivateKey(int alg, const TestingVariables& testingVariables)
{
// Only support -7, P256 (EC), -8 (EdDSA) and -257 (RSA) for now
if (alg != WebAuthnAlgorithms::ES256 && alg != WebAuthnAlgorithms::RS256 && alg != WebAuthnAlgorithms::EDDSA) {
@ -234,21 +236,31 @@ BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFir
QByteArray firstPart;
QByteArray secondPart;
QByteArray spki;
QByteArray pem;
if (!predefinedFirst.isEmpty() && !predefinedSecond.isEmpty()) {
firstPart = browserMessageBuilder()->getArrayFromBase64(predefinedFirst);
secondPart = browserMessageBuilder()->getArrayFromBase64(predefinedSecond);
if (!testingVariables.first.isEmpty() && !testingVariables.second.isEmpty()) {
firstPart = browserMessageBuilder()->getArrayFromBase64(testingVariables.first);
secondPart = browserMessageBuilder()->getArrayFromBase64(testingVariables.second);
} else {
if (alg == WebAuthnAlgorithms::ES256) {
try {
Botan::ECDSA_PrivateKey privateKey(*randomGen()->getRng(), Botan::EC_Group("secp256r1"));
// Use predefined data if found (only for testing private key creation)
const auto keyData = !testingVariables.data.isEmpty()
? Botan::BigInt(testingVariables.data.toStdString())
: Botan::BigInt(0);
Botan::ECDSA_PrivateKey privateKey(*randomGen()->getRng(), Botan::EC_Group("secp256r1"), keyData);
const auto& publicPoint = privateKey.public_point();
auto x = publicPoint.get_affine_x();
auto y = publicPoint.get_affine_y();
firstPart = bigIntToQByteArray(x);
secondPart = bigIntToQByteArray(y);
auto publicKey =
Botan::ECDSA_PublicKey(privateKey.algorithm_identifier(), privateKey.public_key_bits());
auto publicKeySpki = publicKey.subject_public_key();
spki = browserMessageBuilder()->getQByteArray(publicKeySpki.data(), publicKeySpki.size());
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
pem = QByteArray::fromStdString(privateKeyPem);
} catch (std::exception& e) {
@ -263,6 +275,10 @@ BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFir
firstPart = bigIntToQByteArray(modulus);
secondPart = bigIntToQByteArray(exponent);
auto publicKey = Botan::RSA_PublicKey(privateKey.algorithm_identifier(), privateKey.public_key_bits());
auto publicKeySpki = publicKey.subject_public_key();
spki = browserMessageBuilder()->getQByteArray(publicKeySpki.data(), publicKeySpki.size());
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
pem = QByteArray::fromStdString(privateKeyPem);
} catch (std::exception& e) {
@ -271,17 +287,22 @@ BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFir
}
} else if (alg == WebAuthnAlgorithms::EDDSA) {
try {
Botan::Ed25519_PrivateKey key(*randomGen()->getRng());
auto publicKey = key.get_public_key();
Botan::Ed25519_PrivateKey privateKey(*randomGen()->getRng());
auto publicKeyBits = privateKey.get_public_key();
#ifdef WITH_XC_BOTAN3
auto privateKey = key.raw_private_key_bits();
auto privateKeyBits = privateKey.raw_private_key_bits();
#else
auto privateKey = key.get_private_key();
auto privateKeyBits = privateKey.get_private_key();
#endif
firstPart = browserMessageBuilder()->getQByteArray(publicKey.data(), publicKey.size());
secondPart = browserMessageBuilder()->getQByteArray(privateKey.data(), privateKey.size());
firstPart = browserMessageBuilder()->getQByteArray(publicKeyBits.data(), publicKeyBits.size());
secondPart = browserMessageBuilder()->getQByteArray(privateKeyBits.data(), privateKeyBits.size());
auto privateKeyPem = Botan::PKCS8::PEM_encode(key);
auto publicKey =
Botan::Ed25519_PublicKey(privateKey.algorithm_identifier(), privateKey.public_key_bits());
auto publicKeySpki = publicKey.subject_public_key();
spki = browserMessageBuilder()->getQByteArray(publicKeySpki.data(), publicKeySpki.size());
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
pem = QByteArray::fromStdString(privateKeyPem);
} catch (std::exception& e) {
qWarning("BrowserWebAuthn::buildCredentialPrivateKey: Could not create EdDSA private key: %s",
@ -299,6 +320,7 @@ BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFir
AttestationKeyPair attestationKeyPair;
attestationKeyPair.cborEncodedPublicKey = result;
attestationKeyPair.privateKeyPem = pem;
attestationKeyPair.spkiPublicKey = spki;
return attestationKeyPair;
}

View file

@ -61,6 +61,7 @@ struct AttestationKeyPair
{
QByteArray cborEncodedPublicKey;
QByteArray privateKeyPem;
QByteArray spkiPublicKey;
};
// Predefined variables used for testing the class
@ -69,6 +70,7 @@ struct TestingVariables
QString credentialId;
QString first;
QString second;
QString data;
};
class BrowserPasskeys : public QObject
@ -81,7 +83,7 @@ public:
static BrowserPasskeys* instance();
PublicKeyCredential buildRegisterPublicKeyCredential(const QJsonObject& credentialCreationOptions,
const TestingVariables& predefinedVariables = {});
const TestingVariables& testingVariables = {});
QJsonObject buildGetPublicKeyCredential(const QJsonObject& assertionOptions,
const QString& credentialId,
const QString& userHandle,
@ -110,11 +112,9 @@ private:
const QString& extensions,
const QString& credentialId,
const QByteArray& cborEncodedPublicKey,
const TestingVariables& predefinedVariables = {});
const TestingVariables& testingVariables = {});
QByteArray buildAuthenticatorData(const QString& rpId, const QString& extensions);
AttestationKeyPair buildCredentialPrivateKey(int alg,
const QString& predefinedFirst = QString(),
const QString& predefinedSecond = QString());
AttestationKeyPair buildCredentialPrivateKey(int alg, const TestingVariables& testingVariables = {});
QByteArray
buildSignature(const QByteArray& authenticatorData, const QByteArray& clientData, const QString& privateKeyPem);
QJsonObject parseAuthData(const QByteArray& authData) const;

View file

@ -77,7 +77,7 @@ const QString PublicKeyCredential = R"(
"id": "yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8",
"rawId": "cabcc52799707294f060c39d5d29b11796f9718425a813336db53f77ea052cef",
"response": {
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFAAAAAP2xQbJdhEQ-ijVGmMIFpQIAIMq8xSeZcHKU8GDDnV0psReW-XGEJagTM221P3fqBSzvpQECAyYgASFYIAbsrzRbYpFhbRlZA6ZQKsoxxJWoaeXwh-XUuDLNCIXdIlgg4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M",
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFAAAAAP2xQbJdhEQ-ijVGmMIFpQIAIMq8xSeZcHKU8GDDnV0psReW-XGEJagTM221P3fqBSzvpQECAyYgASFYIHK1iVimeR02UYipyiEKrKhhfhJRMew8EbDWGKtMZ2wUIlggbtZ70X11SLx17QFDWVAR3_qqk5OqrRS--Whc7hyw9YU",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibFZlSHpWeFdzcjhNUXhNa1pGMHRpNkZYaGRnTWxqcUt6Z0EtcV96azJNbmlpM2VKNDdWRjk3c3FVb1lrdFZDODVXQVoxdUlBU20tYV9sREZad3NMZnciLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"
},
"type": "public-key"
@ -188,8 +188,8 @@ void TestPasskeys::testDecodeResponseData()
QCOMPARE(publicKey["1"], 2);
QCOMPARE(publicKey["3"], -7);
QCOMPARE(publicKey["-1"], 1);
QCOMPARE(publicKey["-2"], QString("BuyvNFtikWFtGVkDplAqyjHElahp5fCH5dS4Ms0Ihd0"));
QCOMPARE(publicKey["-3"], QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M"));
QCOMPARE(publicKey["-2"], QString("crWJWKZ5HTZRiKnKIQqsqGF-ElEx7DwRsNYYq0xnbBQ"));
QCOMPARE(publicKey["-3"], QString("btZ70X11SLx17QFDWVAR3_qqk5OqrRS--Whc7hyw9YU"));
}
void TestPasskeys::testLoadingECPrivateKeyFromPem()
@ -276,10 +276,9 @@ void TestPasskeys::testCreatingAttestationObjectWithEC()
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
TestingVariables testingVariables = {id, predefinedFirst, predefinedSecond};
TestingVariables testingVariables = {id, predefinedFirst, predefinedSecond, QString()};
const auto alg = browserPasskeys()->getAlgorithmFromPublicKey(credentialCreationOptions);
const auto credentialPrivateKey =
browserPasskeys()->buildCredentialPrivateKey(alg, predefinedFirst, predefinedSecond);
const auto credentialPrivateKey = browserPasskeys()->buildCredentialPrivateKey(alg, testingVariables);
auto result = browserPasskeys()->buildAttestationObject(
credentialCreationOptions, "", id, credentialPrivateKey.cborEncodedPublicKey, testingVariables);
QCOMPARE(
@ -344,10 +343,9 @@ void TestPasskeys::testCreatingAttestationObjectWithRSA()
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
TestingVariables testingVariables = {id, predefinedModulus, predefinedExponent};
TestingVariables testingVariables = {id, predefinedModulus, predefinedExponent, QString()};
const auto alg = browserPasskeys()->getAlgorithmFromPublicKey(credentialCreationOptions);
auto credentialPrivateKey =
browserPasskeys()->buildCredentialPrivateKey(alg, predefinedModulus, predefinedExponent);
auto credentialPrivateKey = browserPasskeys()->buildCredentialPrivateKey(alg, testingVariables);
auto result = browserPasskeys()->buildAttestationObject(
credentialCreationOptions, "", id, credentialPrivateKey.cborEncodedPublicKey, testingVariables);
@ -380,8 +378,7 @@ void TestPasskeys::testRegister()
{
// Predefined values for a desired outcome
const auto predefinedId = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
const auto predefinedX = QString("BuyvNFtikWFtGVkDplAqyjHElahp5fCH5dS4Ms0Ihd0");
const auto predefinedY = QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M");
const auto predefinedData = QString("0x4B0E8AB07B1E62CCD4CB7B9D5BC9DE7B6EED7A3C8A3D466DB12897755E3D7E6D");
const auto origin = QString("https://webauthn.io");
const auto testDataPublicKey = browserMessageBuilder()->getJsonObject(PublicKeyCredential.toUtf8());
const auto testDataResponse = testDataPublicKey["response"];
@ -392,7 +389,7 @@ void TestPasskeys::testRegister()
publicKeyCredentialOptions, origin, &credentialCreationOptions);
QVERIFY(creationResult == 0);
TestingVariables testingVariables = {predefinedId, predefinedX, predefinedY};
TestingVariables testingVariables = {predefinedId, QString(), QString(), predefinedData};
auto result = browserPasskeys()->buildRegisterPublicKeyCredential(credentialCreationOptions, testingVariables);
auto publicKeyCredential = result.response;
QCOMPARE(publicKeyCredential["type"], QString("public-key"));
@ -402,6 +399,9 @@ void TestPasskeys::testRegister()
auto response = publicKeyCredential["response"].toObject();
auto attestationObject = response["attestationObject"].toString();
auto clientDataJson = response["clientDataJSON"].toString();
QCOMPARE(response["publicKey"],
QString("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcrWJWKZ5HTZRiKnKIQqsqGF-"
"ElEx7DwRsNYYq0xnbBRu1nvRfXVIvHXtAUNZUBHf-qqTk6qtFL75aFzuHLD1hQ"));
QCOMPARE(attestationObject, testDataResponse["attestationObject"].toString());
// Parse clientDataJSON