#include #include "common/Crypto.h" #include "common/OidcCrypto.h" #include "common/OidcJwt.h" #include "common/OidcUserClaims.h" #include #include #include #include #include #include #include using namespace visione; namespace { const std::string kWorkspaceRoot = std::filesystem::path(__FILE__).parent_path().parent_path().string(); Json::Value loadJson(const std::string &path) { std::ifstream input(path); if (!input.is_open()) { throw std::runtime_error("fixture_open_failed"); } Json::Value value; input >> value; return value; } oidc::Jwk loadCoreJwk(size_t index) { const auto root = loadJson(kWorkspaceRoot + "/secrets/oidc/demo-rp-jwks.json"); if (!root["core"].isArray() || root["core"].size() <= index) { throw std::runtime_error("fixture_jwk_missing"); } return oidc::parseJwk(root["core"][static_cast(index)]); } std::string compactJwe(const Json::Value &header, const std::string &cek, const std::string &iv, const std::string &ciphertext, const std::string &tag, const oidc::Jwk &recipientKey) { const auto publicPem = crypto::pemFromRsaJwk(recipientKey.n, recipientKey.e); BIO *bio = BIO_new_mem_buf(publicPem.data(), static_cast(publicPem.size())); if (!bio) { throw std::runtime_error("bio_new_failed"); } EVP_PKEY *pkey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr); BIO_free(bio); if (!pkey) { throw std::runtime_error("pem_read_failed"); } EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(pkey, nullptr); if (!ctx) { EVP_PKEY_free(pkey); throw std::runtime_error("ctx_new_failed"); } if (EVP_PKEY_encrypt_init(ctx) != 1 || EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) != 1 || EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha1()) != 1 || EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha1()) != 1) { EVP_PKEY_CTX_free(ctx); EVP_PKEY_free(pkey); throw std::runtime_error("encrypt_init_failed"); } size_t encryptedLen = 0; if (EVP_PKEY_encrypt(ctx, nullptr, &encryptedLen, reinterpret_cast(cek.data()), cek.size()) != 1) { EVP_PKEY_CTX_free(ctx); EVP_PKEY_free(pkey); throw std::runtime_error("encrypt_size_failed"); } std::string encryptedKey(encryptedLen, '\0'); if (EVP_PKEY_encrypt(ctx, reinterpret_cast(encryptedKey.data()), &encryptedLen, reinterpret_cast(cek.data()), cek.size()) != 1) { EVP_PKEY_CTX_free(ctx); EVP_PKEY_free(pkey); throw std::runtime_error("encrypt_failed"); } encryptedKey.resize(encryptedLen); EVP_PKEY_CTX_free(ctx); EVP_PKEY_free(pkey); Json::StreamWriterBuilder writer; writer["indentation"] = ""; const auto encodedHeader = oidc::base64UrlEncode(Json::writeString(writer, header)); return encodedHeader + "." + oidc::base64UrlEncode(encryptedKey) + "." + oidc::base64UrlEncode(iv) + "." + oidc::base64UrlEncode(ciphertext) + "." + oidc::base64UrlEncode(tag); } std::string encryptAesCbcHmac(const EVP_CIPHER *cipher, const EVP_MD *md, const std::string &macKey, const std::string &encKey, const std::string &aad, const std::string &plaintext, const std::string &iv, size_t tagSize, std::string &outCiphertext, std::string &outTag) { EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); if (!ctx) { throw std::runtime_error("cipher_ctx_new_failed"); } if (EVP_EncryptInit_ex(ctx, cipher, nullptr, reinterpret_cast(encKey.data()), reinterpret_cast(iv.data())) != 1) { EVP_CIPHER_CTX_free(ctx); throw std::runtime_error("encrypt_init_failed"); } std::string ciphertext(plaintext.size() + EVP_MAX_BLOCK_LENGTH, '\0'); int outLen1 = 0; if (EVP_EncryptUpdate(ctx, reinterpret_cast(ciphertext.data()), &outLen1, reinterpret_cast(plaintext.data()), plaintext.size()) != 1) { EVP_CIPHER_CTX_free(ctx); throw std::runtime_error("encrypt_update_failed"); } int outLen2 = 0; if (EVP_EncryptFinal_ex( ctx, reinterpret_cast(ciphertext.data()) + outLen1, &outLen2) != 1) { EVP_CIPHER_CTX_free(ctx); throw std::runtime_error("encrypt_final_failed"); } EVP_CIPHER_CTX_free(ctx); ciphertext.resize(outLen1 + outLen2); const uint64_t bitLength = aad.size() * 8; std::string al(8, '\0'); for (int i = 0; i < 8; ++i) { al[7 - i] = static_cast((bitLength >> (i * 8)) & 0xff); } std::string macInput = aad + iv + ciphertext + al; unsigned char hmac[EVP_MAX_MD_SIZE]; unsigned int hmacLen = 0; if (!HMAC(md, macKey.data(), static_cast(macKey.size()), reinterpret_cast(macInput.data()), macInput.size(), hmac, &hmacLen)) { throw std::runtime_error("hmac_failed"); } outCiphertext = ciphertext; outTag.assign(reinterpret_cast(hmac), tagSize); return ciphertext; } } // namespace DROGON_TEST(OidcJwtParseSignVerifyAndKidLookup) { const auto signingJwk = loadCoreJwk(1); Json::Value header; header["alg"] = "RS256"; header["typ"] = "JWT"; header["kid"] = signingJwk.kid; Json::Value payload; payload["iss"] = "visione-test"; payload["sub"] = "user-123"; payload["nonce"] = "nonce-456"; payload["roles"].append("citizen"); const auto token = oidc::signJwt(header, payload, signingJwk); const auto parsed = oidc::parseJwt(token); CHECK(parsed.header["kid"].asString() == signingJwk.kid); CHECK(parsed.payload["sub"].asString() == "user-123"); CHECK(oidc::verifyJwtSignature(token, signingJwk)); std::vector keys{loadCoreJwk(0), signingJwk}; const auto found = oidc::findJwkByKid(keys, signingJwk.kid); REQUIRE(found.has_value()); CHECK(found->kid == signingJwk.kid); } DROGON_TEST(OidcJwtLeftHashMatchesJwtCppComputation) { const std::string accessToken = "access-token-fixture"; const auto expected = oidc::leftHashBase64Url(accessToken); CHECK(expected == "xb80u-_Gg2ezXboVjrZ35g"); } DROGON_TEST(OidcCryptoDecryptsA256CbcHs512) { const auto recipientJwk = loadCoreJwk(0); Json::Value header; header["alg"] = "RSA-OAEP"; header["enc"] = "A256CBC-HS512"; header["kid"] = recipientJwk.kid; std::string cek(64, '\0'); std::string iv(16, '\0'); for (size_t i = 0; i < cek.size(); ++i) { cek[i] = static_cast(i + 1); } for (size_t i = 0; i < iv.size(); ++i) { iv[i] = static_cast(0x41 + i); } const std::string plaintext = R"({"sub":"user-123","given_name":"Mario"})"; Json::StreamWriterBuilder writer; writer["indentation"] = ""; const auto aad = oidc::base64UrlEncode(Json::writeString(writer, header)); std::string ciphertext; std::string tag; encryptAesCbcHmac(EVP_aes_256_cbc(), EVP_sha512(), cek.substr(0, 32), cek.substr(32, 32), aad, plaintext, iv, 32, ciphertext, tag); const auto jwe = compactJwe(header, cek, iv, ciphertext, tag, recipientJwk); CHECK(oidc::decryptJwe(jwe, recipientJwk) == plaintext); } DROGON_TEST(OidcCryptoDecryptsA128CbcHs256AndRejectsInvalidCases) { const auto recipientJwk = loadCoreJwk(0); Json::Value header; header["alg"] = "RSA-OAEP"; header["enc"] = "A128CBC-HS256"; header["kid"] = recipientJwk.kid; std::string cek(32, '\0'); std::string iv(16, '\0'); for (size_t i = 0; i < cek.size(); ++i) { cek[i] = static_cast(0x30 + i); } for (size_t i = 0; i < iv.size(); ++i) { iv[i] = static_cast(0x51 + i); } const std::string plaintext = R"({"sub":"user-abc","email":"mario@example.com"})"; Json::StreamWriterBuilder writer; writer["indentation"] = ""; const auto aad = oidc::base64UrlEncode(Json::writeString(writer, header)); std::string ciphertext; std::string tag; encryptAesCbcHmac(EVP_aes_128_cbc(), EVP_sha256(), cek.substr(0, 16), cek.substr(16, 16), aad, plaintext, iv, 16, ciphertext, tag); const auto validJwe = compactJwe(header, cek, iv, ciphertext, tag, recipientJwk); CHECK(oidc::decryptJwe(validJwe, recipientJwk) == plaintext); auto missingKidHeader = header; missingKidHeader.removeMember("kid"); const auto missingKidJwe = compactJwe(missingKidHeader, cek, iv, ciphertext, tag, recipientJwk); const auto parsedMissingKid = oidc::parseJwe(missingKidJwe); CHECK(parsedMissingKid.header.get("kid", "").asString().empty()); auto unsupportedHeader = header; unsupportedHeader["enc"] = "A192GCM"; const auto unsupportedJwe = compactJwe(unsupportedHeader, cek, iv, ciphertext, tag, recipientJwk); CHECK_THROWS_AS(oidc::decryptJwe(unsupportedJwe, recipientJwk), std::runtime_error); std::string invalidTag = tag; invalidTag[0] ^= 0x01; const auto invalidTagJwe = compactJwe(header, cek, iv, ciphertext, invalidTag, recipientJwk); CHECK_THROWS_AS(oidc::decryptJwe(invalidTagJwe, recipientJwk), std::runtime_error); } DROGON_TEST(OidcMergeClaimsPrefersUserinfoAndNormalizesFiscalCode) { Json::Value idTokenClaims; idTokenClaims["sub"] = "provider-subject"; idTokenClaims["given_name"] = "Old"; idTokenClaims["family_name"] = "Value"; Json::Value userinfoClaims; userinfoClaims["given_name"] = "Mario"; userinfoClaims["family_name"] = "Rossi"; userinfoClaims["email"] = "mario.rossi@example.com"; userinfoClaims["https://attributes.eid.gov.it/fiscal_number"] = "TINIT-RSSMRA80A01H501U"; const auto merged = oidc::mergeUserClaims("http://127.0.0.1:8002/oidc/op", idTokenClaims, userinfoClaims); REQUIRE(merged.has_value()); CHECK(merged->subject == "provider-subject"); CHECK(merged->firstName == "Mario"); CHECK(merged->lastName == "Rossi"); CHECK(merged->email == "mario.rossi@example.com"); CHECK(merged->fiscalCode == "RSSMRA80A01H501U"); }