From bfe05670c7389cafd5c683372bbaf7bf918ece57 Mon Sep 17 00:00:00 2001 From: Mart Somermaa Date: Fri, 15 Oct 2021 19:58:45 +0300 Subject: [PATCH] feat(authtoken): change the authentication token format from JWT to custom JSON + signature WE2-585 Signed-off-by: Mart Somermaa --- CMakeLists.txt | 2 +- README.md | 36 ++--- .../command-handlers/authenticate.cpp | 138 +++++------------- .../command-handlers/authenticate.hpp | 5 +- .../command-handlers/signauthutils.cpp | 2 +- tests/input-output-mode/test.py | 28 +--- tests/tests/getcommandhandler-mock.cpp | 12 +- tests/tests/main.cpp | 13 +- 8 files changed, 71 insertions(+), 165 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cb469c6d..cffee862 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ elseif($ENV{CI_PIPELINE_IID}) else() set(BUILD_NUMBER 0) endif() -project(web-eid VERSION 1.0.2.${BUILD_NUMBER}) +project(web-eid VERSION 2.0.0.${BUILD_NUMBER}) set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") set(MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION_TWEAK}) diff --git a/README.md b/README.md index c49183b9..4d71f84d 100644 --- a/README.md +++ b/README.md @@ -73,29 +73,28 @@ members: ### Authenticate -Authentication command creates the OpenID X509 ID Token and signs it with the -authentication key. +Authentication command creates the [Web eID authentication token](https://web-eid.gitlab.io/web-standards-proposals/web-eid-auth-token-format-and-js-api-spec.pdf) +and signs it with the authentication key. -Authentication command requires the nonce, origin URL and Base64-encoded origin -certificate as JSON-encoded command-line arguments: - - web-eid -c authenticate '{"nonce": "12345678901234567890123456789012345678901234", "origin": "https://ria.ee", "origin-cert": "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"}' +Authentication command requires the challenge nonce and origin URL as +JSON-encoded command-line arguments: -Origin certificate is optional and may be null. + web-eid -c authenticate '{"challenge-nonce": "12345678901234567890123456789012345678901234", "origin": "https://ria.ee"}' The result will be written to standard output as a JSON-encoded message that -either contains the OpenID X509 ID Token or an error code. Successful output +either contains the authentication token or an error code. Successful output example: - {"auth-token": "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlFQXpDQ0EyV2dBd0lCQWdJUU9Xa0JXWE5ESm0xYnlGZDNYc1drdmpBS0JnZ3Foa2pPUFFRREJEQmdNUXN3Q1FZRFZRUUdFd0pGUlRFYk1Ca0dBMVVFQ2d3U1Uwc2dTVVFnVTI5c2RYUnBiMjV6SUVGVE1SY3dGUVlEVlFSaERBNU9WRkpGUlMweE1EYzBOekF4TXpFYk1Ca0dBMVVFQXd3U1ZFVlRWQ0J2WmlCRlUxUkZTVVF5TURFNE1CNFhEVEU0TVRBeE9EQTVOVEEwTjFvWERUSXpNVEF4TnpJeE5UazFPVm93ZnpFTE1Ba0dBMVVFQmhNQ1JVVXhLakFvQmdOVkJBTU1JVXJEbFVWUFVrY3NTa0ZCU3kxTFVrbFRWRXBCVGl3ek9EQXdNVEE0TlRjeE9ERVFNQTRHQTFVRUJBd0hTc09WUlU5U1J6RVdNQlFHQTFVRUtnd05Ta0ZCU3kxTFVrbFRWRXBCVGpFYU1CZ0dBMVVFQlJNUlVFNVBSVVV0TXpnd01ERXdPRFUzTVRnd2RqQVFCZ2NxaGtqT1BRSUJCZ1VyZ1FRQUlnTmlBQVI1azFsWHp2U2VJOU8vMXMxcFp2amhFVzhuSXRKb0cwRUJGeG1MRVk2UzdraTF2RjJRM1RFRHg2ZE56dEkxWHR4OTZjczhyNHpZVHdkaVFvRGc3azNkaVV1UjluVFdHeFFFTU8xRkRvNFk5ZkFtaVBHV1QrK0d1T1ZvWlFZM1h4aWpnZ0hETUlJQnZ6QUpCZ05WSFJNRUFqQUFNQTRHQTFVZER3RUIvd1FFQXdJRGlEQkhCZ05WSFNBRVFEQStNRElHQ3lzR0FRUUJnNUVoQVFJQk1DTXdJUVlJS3dZQkJRVUhBZ0VXRldoMGRIQnpPaTh2ZDNkM0xuTnJMbVZsTDBOUVV6QUlCZ1lFQUk5NkFRSXdId1lEVlIwUkJCZ3dGb0VVTXpnd01ERXdPRFUzTVRoQVpXVnpkR2t1WldVd0hRWURWUjBPQkJZRUZPUXN2VFFKRUJWTU1TbWh5Wlg1YmliWUp1YkFNR0VHQ0NzR0FRVUZCd0VEQkZVd1V6QlJCZ1lFQUk1R0FRVXdSekJGRmo5b2RIUndjem92TDNOckxtVmxMMlZ1TDNKbGNHOXphWFJ2Y25rdlkyOXVaR2wwYVc5dWN5MW1iM0l0ZFhObExXOW1MV05sY25ScFptbGpZWFJsY3k4VEFrVk9NQ0FHQTFVZEpRRUIvd1FXTUJRR0NDc0dBUVVGQndNQ0JnZ3JCZ0VGQlFjREJEQWZCZ05WSFNNRUdEQVdnQlRBaEprcHhFNmZPd0kwOXBuaENsWUFDQ2srZXpCekJnZ3JCZ0VGQlFjQkFRUm5NR1V3TEFZSUt3WUJCUVVITUFHR0lHaDBkSEE2THk5aGFXRXVaR1Z0Ynk1emF5NWxaUzlsYzNSbGFXUXlNREU0TURVR0NDc0dBUVVGQnpBQ2hpbG9kSFJ3T2k4dll5NXpheTVsWlM5VVpYTjBYMjltWDBWVFZFVkpSREl3TVRndVpHVnlMbU55ZERBS0JnZ3Foa2pPUFFRREJBT0Jpd0F3Z1ljQ1FnSDFVc21NZHRMWnRpNTFGcTJRUjR3VWtBd3BzbmhzQlYySFFxVVhGWUJKN0VYbkxDa2FYamRaS2tIcEFCZk0wUUV4N1VVaGFJNGk1M2ppSjdFMVk3V09BQUpCRFg0ejYxcG5pSEphcEkxYmtNSWlKUS90aTdoYThmZEpTTVNwQWRzNUN5SEl5SGtReldsVnk4NmY5bUE3RXUzb1JPLzFxK2VGVXpEYk5OM1Z2eTdnUVdRPSJdfQ.eyJhdWQiOlsiaHR0cHM6Ly9yaWEuZWUiLCJ1cm46Y2VydDpzaGEtMjU2OjZmMGRmMjQ0ZTRhODU2Yjk0YjNiM2I0NzU4MmEwYTUxYTMyZDY3NGRiYzcxMDcyMTFlZDIzZDRiZWM2ZDljNzIiXSwiZXhwIjoiMTU4Njg3MTE2OSIsImlhdCI6IjE1ODY4NzA4NjkiLCJpc3MiOiJ3ZWItZWlkIGFwcCB2MC45LjAtMS1nZTZlODlmYSIsIm5vbmNlIjoiMTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2NzgiLCJzdWIiOiJKw5VFT1JHLEpBQUstS1JJU1RKQU4sMzgwMDEwODU3MTgifQ.0Y5CdMiSZ14rOnd7sbp-XeBQ7qrJVd21yTmAbiRnzAXtwqW8ZROg4jL4J7bpQ2fwyUz4-dVwLoVRVnxfJY82b8NXuxXrDb-8MXXmVYrMW0q0kPbEzqFbEnPYHjNnKAN0"} + { + "unverifiedCertificate": "MIIEAzCCA2WgAwIBAgIQHWbVWxCkcYxbzz9nBzGrDzAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE4MTAyMzE1MzM1OVoXDTIzMTAyMjIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ/u+9IncarVpgrACN6aRgUiT9lWC9H7llnxoEXe8xoCI982Md8YuJsVfRdeG5jwVfXe0N6KkHLFRARspst8qnACULkqFNat/Kj+XRwJ2UANeJ3Gl5XBr+tnLNuDf/UiR6jggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFOTddHnA9rJtbLwhBNyn0xZTQGCMMGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQgHYElkX4vn821JR41akI/lpexCnJFUf4GiOMbTfzAxpZma333R8LNrmI4zbzDp03hvMTzH49g1jcbGnaCcbboS8DAJBObenUp++L5VqldHwKAps61nM4V+TiLqD0jILnTzl+pV+LexNL3uGzUfvvDNLHnF9t6ygi8+Bsjsu3iHHyM1haKM=", + "algorithm": "ES384", + "signature": "j8KBTYCXZ8OLuL6eoitRlSmiqw6oIsIJmDm6SttGYvEaJUkBS5kLeCeaokQm5u5viLEJy9iUDONEVlcnLgHIlOZUoEozPNw+AzjI9n7n/D25koYrzmGvMsHX1AKbwqAc", + "format": "web-eid:1.0", + "appVersion": "https://web-eid.eu/web-eid-app/releases/2.0.0+0" + } -The OpenID X509 ID Token is a standard JSON Web Token that can be validated -with e.g. the [JWT.IO online validator](https://jwt.io/). The full -specification of the format is available in the [Web eID system architecture -document](https://github.com/web-eid/web-eid-system-architecture-doc#token-format). -Note that the `aud` field of the token contains an array that contains the -origin URL and, in case the origin certificate is provided, also the -origin certificate SHA-256 fingerprint as second element. +The full specification of the format is available in the [Web eID system +architecture document](https://github.com/web-eid/web-eid-system-architecture-doc#token-format). ### Sign @@ -124,7 +123,10 @@ either contains the Base64-encoded signature and the signature algorithm used (see the description of the `supported-signature-algos` field above in section _Get certificate_), or an error code. Successful output example: - {"signature-algo": {"hash-algo": "SHA-384", "padding-algo": "NONE", "crypto-algo": "ECC"}, "signature": "oIw20YRlryXgAhGbHEKBCzQetVAE/S2VjqEQ1h+Kc9Scujcl37oOCmAgoHmEkG4Fpmp/z2waGw8ciJ1yXNpgzIaLhtyytFnFmcwR3zp6OKZTqHuEvTEAxZkxC6gLCxJh"} + { + "signature-algo": {"hash-algo": "SHA-384", "padding-algo": "NONE", "crypto-algo": "ECC"}, + "signature": "oIw20YRlryXgAhGbHEKBCzQetVAE/S2VjqEQ1h+Kc9Scujcl37oOCmAgoHmEkG4Fpmp/z2waGw8ciJ1yXNpgzIaLhtyytFnFmcwR3zp6OKZTqHuEvTEAxZkxC6gLCxJh" + } ## Changing the user interface language diff --git a/src/controller/command-handlers/authenticate.cpp b/src/controller/command-handlers/authenticate.cpp index 9b459a65..eedd85d8 100644 --- a/src/controller/command-handlers/authenticate.cpp +++ b/src/controller/command-handlers/authenticate.cpp @@ -38,55 +38,25 @@ using namespace electronic_id; namespace { -const auto JWT_BASE64_OPTIONS = QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals; +// Use common base64-encoding defaults. +constexpr auto BASE64_OPTIONS = QByteArray::Base64Encoding | QByteArray::KeepTrailingEquals; -QByteArray jsonDocToBase64(const QJsonDocument& doc) +QVariantMap createAuthenticationToken(const QString& signatureAlgorithm, + const QByteArray& certificateDer, const QByteArray& signature) { - return QString(doc.toJson(QJsonDocument::JsonFormat::Compact)) - .toUtf8() - .toBase64(JWT_BASE64_OPTIONS); -} - -QByteArray createAuthenticationToken(const QSslCertificate& certificate, - const QByteArray& certificateDer, - const QString& signatureAlgorithm, const QString& nonce, - const QString& origin, - const QSslCertificate& originCertificate) -{ - const auto tokenHeader = QJsonDocument(QJsonObject { - {"typ", "JWT"}, - {"alg", signatureAlgorithm}, - {"x5c", QJsonArray({QString(certificateDer.toBase64())})}, - }); - - const QString sub = - certificate.subjectInfo("SN").isEmpty() && certificate.subjectInfo("GN").isEmpty() - ? certificate.subjectInfo(QSslCertificate::CommonName).join(',') - : QStringLiteral("%1,%2").arg(certificate.subjectInfo("SN").join(','), - certificate.subjectInfo("GN").join(',')); - auto tokenPayload = QJsonObject { - {"iat", QString::number(QDateTime::currentDateTimeUtc().toSecsSinceEpoch())}, - {"exp", - QString::number(QDateTime::currentDateTimeUtc().addSecs(5 * 60).toSecsSinceEpoch())}, - {"sub", sub}, - {"nonce", nonce}, - {"iss", QStringLiteral("web-eid app %1").arg(qApp->applicationVersion())}, + return QVariantMap { + {"unverifiedCertificate", QString(certificateDer.toBase64(BASE64_OPTIONS))}, + {"algorithm", signatureAlgorithm}, + {"signature", QString(signature)}, + {"format", QStringLiteral("web-eid:1.0")}, + {"appVersion", + QStringLiteral("https://web-eid.eu/web-eid-app/releases/%1") + .arg(qApp->applicationVersion())}, }; - - auto aud = QJsonArray({origin}); - if (!originCertificate.isNull()) { - const auto originCertFingerprint = - QString(originCertificate.digest(QCryptographicHash::Sha256).toHex()); - // urn:cert:sha-256 as per https://tools.ietf.org/id/draft-seantek-certspec-00.html - aud.append("urn:cert:sha-256:" + originCertFingerprint); - } - tokenPayload[QStringLiteral("aud")] = aud; - - return jsonDocToBase64(tokenHeader) + '.' + jsonDocToBase64(QJsonDocument(tokenPayload)); } -QByteArray signToken(const ElectronicID& eid, const QByteArray& token, - const pcsc_cpp::byte_vector& pin) +QByteArray createSignature(const QString& origin, const QString& challengeNonce, + const ElectronicID& eid, const pcsc_cpp::byte_vector& pin) { static const auto SIGNATURE_ALGO_TO_HASH = std::map { @@ -103,18 +73,21 @@ QByteArray signToken(const ElectronicID& eid, const QByteArray& token, const auto hashAlgo = SIGNATURE_ALGO_TO_HASH.at(eid.authSignatureAlgorithm()); - const auto tokenHashQBytearray = QCryptographicHash::hash(token, hashAlgo); - const auto tokenHash = - pcsc_cpp::byte_vector {tokenHashQBytearray.cbegin(), tokenHashQBytearray.cend()}; + // Take the hash of the origin and nonce to ensure field separation. + const auto originHash = QCryptographicHash::hash(origin.toUtf8(), hashAlgo); + const auto challengeNonceHash = QCryptographicHash::hash(challengeNonce.toUtf8(), hashAlgo); - const auto signature = eid.signWithAuthKey(pin, tokenHash); + // The value that is signed is hash(origin)+hash(challenge). + const auto hashToBeSignedQBytearray = + QCryptographicHash::hash(originHash + challengeNonceHash, hashAlgo); + const auto hashToBeSigned = + pcsc_cpp::byte_vector {hashToBeSignedQBytearray.cbegin(), hashToBeSignedQBytearray.cend()}; - const auto signatureBase64 = - QByteArray::fromRawData(reinterpret_cast(signature.data()), - int(signature.size())) - .toBase64(JWT_BASE64_OPTIONS); + const auto signature = eid.signWithAuthKey(pin, hashToBeSigned); - return token + '.' + signatureBase64; + return QByteArray::fromRawData(reinterpret_cast(signature.data()), + int(signature.size())) + .toBase64(BASE64_OPTIONS); } } // namespace @@ -122,24 +95,22 @@ QByteArray signToken(const ElectronicID& eid, const QByteArray& token, Authenticate::Authenticate(const CommandWithArguments& cmd) : CertificateReader(cmd) { const auto arguments = cmd.second; - requireArgumentsAndOptionalLang({"nonce", "origin", "origin-cert"}, arguments, - "\"nonce\": \"\", " - "\"origin\": \"\", " - "\"origin-cert\": \"\""); + requireArgumentsAndOptionalLang({"challenge-nonce", "origin"}, arguments, + "\"challenge-nonce\": \"\", " + "\"origin\": \"\""); - nonce = validateAndGetArgument(QStringLiteral("nonce"), arguments); + challengeNonce = validateAndGetArgument(QStringLiteral("challenge-nonce"), arguments); // nonce must contain at least 256 bits of entropy and is usually Base64-encoded, so the // required byte length is 44, the length of 32 Base64-encoded bytes. - if (nonce.length() < 44) { + if (challengeNonce.length() < 44) { THROW(CommandHandlerInputDataError, - "Challenge nonce argument 'nonce' must be at least 44 characters long"); + "Challenge nonce argument 'challenge-nonce' must be at least 44 characters long"); } - if (nonce.length() > 128) { + if (challengeNonce.length() > 128) { THROW(CommandHandlerInputDataError, - "Challenge nonce argument 'nonce' cannot be longer than 128 characters"); + "Challenge nonce argument 'challenge-nonce' cannot be longer than 128 characters"); } validateAndStoreOrigin(arguments); - validateAndStoreOriginCertificate(arguments); } QVariantMap Authenticate::onConfirm(WebEidUI* window, @@ -148,21 +119,19 @@ QVariantMap Authenticate::onConfirm(WebEidUI* window, const auto signatureAlgorithm = QString::fromStdString(cardCertAndPin.cardInfo->eid().authSignatureAlgorithm()); - const auto token = - createAuthenticationToken(cardCertAndPin.certificate, cardCertAndPin.certificateBytesInDer, - signatureAlgorithm, nonce, origin.url(), originCertificate); - auto pin = getPin(cardCertAndPin.cardInfo->eid().smartcard(), window); try { - const auto signedToken = signToken(cardCertAndPin.cardInfo->eid(), token, pin); + const auto signature = + createSignature(origin.url(), challengeNonce, cardCertAndPin.cardInfo->eid(), pin); // Erase the PIN memory. - // TODO: Use a scope guard. Verify that the buffers are actually zeroed - // and no copies remain. + // TODO: Use a scope guard. Verify that the buffers are actually zeroed and no copies + // remain. std::fill(pin.begin(), pin.end(), '\0'); - return {{QStringLiteral("auth-token"), QString(signedToken)}}; + return createAuthenticationToken(signatureAlgorithm, cardCertAndPin.certificateBytesInDer, + signature); } catch (const VerifyPinFailed& failure) { switch (failure.status()) { @@ -185,30 +154,3 @@ void Authenticate::connectSignals(const WebEidUI* window) connect(this, &Authenticate::verifyPinFailed, window, &WebEidUI::onVerifyPinFailed); } - -void Authenticate::validateAndStoreOriginCertificate(const QVariantMap& args) -{ - originCertificate = parseAndValidateCertificate(QStringLiteral("origin-cert"), args, true); - if (originCertificate.isNull()) { - return; - } - - const auto certHostnames = originCertificate.subjectInfo(QSslCertificate::CommonName); - if (certHostnames.size() != 1) { - // TODO: add support for multi-domain certificates - THROW(CommandHandlerInputDataError, - "Origin certificate does not contain exactly 1 host name (it contains " - + std::to_string(certHostnames.size()) + ")"); - } - if (origin.host() != certHostnames[0] - // Certificate hostname may be a wildcard, e.g. *.ria.ee, use QDir::match() for glob - // matching. Origin hostname may be either e.g. www.ria.ee that matches *.ria.ee directly, - && !QDir::match(certHostnames[0], origin.host()) - // or ria.ee that needs an extra dot prefix to match. - && !QDir::match(certHostnames[0], '.' + origin.host())) { - THROW(CommandHandlerInputDataError, - "Origin host name '" + origin.host().toStdString() - + "' does not match origin certificate host name '" - + certHostnames[0].toStdString() + "'"); - } -} diff --git a/src/controller/command-handlers/authenticate.hpp b/src/controller/command-handlers/authenticate.hpp index b82a8d91..b95f3c37 100644 --- a/src/controller/command-handlers/authenticate.hpp +++ b/src/controller/command-handlers/authenticate.hpp @@ -40,8 +40,5 @@ class Authenticate : public CertificateReader const qint8 retriesLeft); private: - void validateAndStoreOriginCertificate(const QVariantMap& args); - - QString nonce; - QSslCertificate originCertificate; + QString challengeNonce; }; diff --git a/src/controller/command-handlers/signauthutils.cpp b/src/controller/command-handlers/signauthutils.cpp index 722be265..71864356 100644 --- a/src/controller/command-handlers/signauthutils.cpp +++ b/src/controller/command-handlers/signauthutils.cpp @@ -41,7 +41,7 @@ void requireArgumentsAndOptionalLang(QStringList argNames, const QVariantMap& ar // QMap::keys() also returns a list containing all the keys in the map in ascending order. if (argCopy.keys() != argNames) { THROW(CommandHandlerInputDataError, - "Argument must be '{" + argDescriptions + "Arguments must be '{" + argDescriptions + ", \"lang\": \"\"}'"); } } diff --git a/tests/input-output-mode/test.py b/tests/input-output-mode/test.py index cb86b601..ce4b2798 100644 --- a/tests/input-output-mode/test.py +++ b/tests/input-output-mode/test.py @@ -50,33 +50,13 @@ def test_2_authenticate(self): message = { 'command': 'authenticate', 'arguments': { - 'nonce': '1' * 44, - 'origin': 'https://ria.ee', - 'origin-cert': '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' + 'challenge-nonce': '12345678123456781234567812345678912356789123', + 'origin': 'https://ria.ee' } } response = self.exchange_message_with_app(message) - self.assertIn('auth-token', response) - self.assert_aud_field_present(response) - # TODO: use a JWT library to validate the authentication token. - - def test_2_authenticate_without_origin_cert(self): - message = { - 'command': 'authenticate', - 'arguments': { - 'nonce': '1' * 44, - 'origin': 'https://ria.ee', - 'origin-cert': None - } - } - response = self.exchange_message_with_app(message) - self.assertIn('auth-token', response) - self.assert_aud_field_present(response) - - def assert_aud_field_present(self, response): - authtoken = response['auth-token'] - payload = b64decode(authtoken.split('.')[1] + '===') - self.assertIn(b'aud', payload) + self.assertIn('signature', response) + # TODO: use a crypto library to validate the token signature. def test_3_sign(self): self.assertTrue(hasattr(self.__class__, 'signingCertificate')) diff --git a/tests/tests/getcommandhandler-mock.cpp b/tests/tests/getcommandhandler-mock.cpp index 99b2608d..c6dd29aa 100644 --- a/tests/tests/getcommandhandler-mock.cpp +++ b/tests/tests/getcommandhandler-mock.cpp @@ -33,18 +33,8 @@ using namespace std::string_literals; const QVariantMap AUTHENTICATE_COMMAND_ARGUMENT = { - {"nonce", "12345678912345678912345678912345678912345678"}, + {"challenge-nonce", "12345678912345678912345678912345678912345678"}, {"origin", "https://ria.ee"}, - {"origin-cert", - "MIIBkTCCATegAwIBAgIUHO7Fd2Vd7ie30o8xYHrUOUlLfU0wCgYIKoZIzj0EAwIw" - "HjELMAkGA1UEBhMCZWUxDzANBgNVBAMMBnJpYS5lZTAeFw0xOTEyMTcyMTA1NTda" - "Fw0yMDEyMTYyMTA1NTdaMB4xCzAJBgNVBAYTAmVlMQ8wDQYDVQQDDAZyaWEuZWUw" - "WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ+8dFJ3OuJwRW+23o5jwjmH6/dX4l/" - "hDfCfKys3WLcLp0NtmDRzQqV5N6oOjjjxkp0Ryyk14JSl/CFM45Elj9ao1MwUTAd" - "BgNVHQ4EFgQU93MZD9u2Um1ciNn3+B/Ed4BXVe8wHwYDVR0jBBgwFoAU93MZD9u2" - "Um1ciNn3+B/Ed4BXVe8wDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBF" - "AiBmfEe2K+kBdcg2VpDQkUj65gSxuRJubWyKqMp3PbhOzQIhAMEwHl8MXKojadt6" - "/jYGfoB1Qgp0McItzDpecJ7yxpkc"}, }; const QVariantMap GET_CERTIFICATE_COMMAND_ARGUMENT = {{"origin", "https://dummy-origin"}}; diff --git a/tests/tests/main.cpp b/tests/tests/main.cpp index abdde281..0c1c8d51 100644 --- a/tests/tests/main.cpp +++ b/tests/tests/main.cpp @@ -68,7 +68,7 @@ private slots: void getCertificate_expiredCertificateHasExpectedCertificateSubject(); void getCertificate_outputsSupportedAlgos(); - void authenticate_validArgumentsResultInValidJwt(); + void authenticate_validArgumentsResultInValidToken(); void fromPunycode_decodesEeDomain(); @@ -177,7 +177,7 @@ void WebEidTests::getCertificate_outputsSupportedAlgos() QCOMPARE(controller->result()["supported-signature-algos"].toList()[0].toMap(), ES224_ALGO); } -void WebEidTests::authenticate_validArgumentsResultInValidJwt() +void WebEidTests::authenticate_validArgumentsResultInValidToken() { // arrange initCard(false); @@ -193,13 +193,8 @@ void WebEidTests::authenticate_validArgumentsResultInValidJwt() const auto certInfo = getCertAndPinInfoFromSignalSpy(authenticateSpy); QCOMPARE(certInfo.subject, QStringLiteral("M\u00C4NNIK,MARI-LIIS,61709210125")); - QCOMPARE( - QString(controller->result()["auth-token"].toString().toUtf8()).left(316), - QStringLiteral( - "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlHUnpDQ0JDK2dBd0lCQWdJUVJBN1gwUHlSRj" - "I5WjFNMTJEZ3QreHpBTkJna3Foa2lHOXcwQkFRc0ZBREJyTVFzd0NRWURWUVFHRXdKRlJURWlNQ0FHQTFVRUNn" - "d1pRVk1nVTJWeWRHbG1hWFJ6WldWeWFXMXBjMnRsYzJ0MWN6RVhNQlVHQTFVRVlRd09UbFJTUlVVdE1UQTNORG" - "N3TVRNeEh6QWRCZ05WQkFNTUZsUkZVMVFnYjJZZ1JWTlVSVWxFTFZOTElE")); + QCOMPARE(controller->result()["unverifiedCertificate"].toString().left(25), + QStringLiteral("MIIGRzCCBC+gAwIBAgIQRA7X0")); } void WebEidTests::fromPunycode_decodesEeDomain()