From 1b76e52715e6cb4cd8d2625abd5dc43de20c63c1 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 9 Jan 2026 11:12:33 +0200 Subject: [PATCH 01/65] Added CDocV1 tests Signed-off-by: Lauris Kaplinski --- test/data/ec-secp384r1-cert.der | Bin 0 -> 660 bytes test/data/rsa_2048_cert.der | Bin 0 -> 995 bytes test/data/test_data2.txt | 1 + test/data/test_data3.txt | 3 +++ 4 files changed, 4 insertions(+) create mode 100644 test/data/ec-secp384r1-cert.der create mode 100644 test/data/rsa_2048_cert.der create mode 100644 test/data/test_data2.txt create mode 100644 test/data/test_data3.txt diff --git a/test/data/ec-secp384r1-cert.der b/test/data/ec-secp384r1-cert.der new file mode 100644 index 0000000000000000000000000000000000000000..b806ac8b425807091071c74302895ef99334d37b GIT binary patch literal 660 zcmXqLVwzyk#3Z(WnTe5!NhHNg^7Y(3_s^<4{^X!_Xa18+J7OKIN6v(S=fY`TwM*d3^YI-W*$Ya!&6=TgWPqz938!Ny@Nc1L%baQbc`(w3=9n{ zOwA1~3-0xQBU29ib5}m^-u*XE@SG4sSHKTaAk4`4pM}+c8Au^VJ#z+wK`N6W z!`dk}Tjk%Zx4Cz6|Hn%=fAiERvgR$hlM^>}spWzL5u4NX<~9fw+)P;gzbiTI^R_Pw zm<*JROsnrTM_ihBlCQqu^evNTUnef{Zf_EATivW*{jGU_$~>z@^OlQL&)6-{D!^{H F001_W)M@|# literal 0 HcmV?d00001 diff --git a/test/data/rsa_2048_cert.der b/test/data/rsa_2048_cert.der new file mode 100644 index 0000000000000000000000000000000000000000..4c0463f8b79859db14d0a8420596f0d57ddd1b88 GIT binary patch literal 995 zcmXqLV!m(C#B_WCGZP~dlZemt_xo0hONw~02paJ3`X!l^ar}eR9F8XcO5TBM{iy4AkW|sFGoKeV+#WV zLjwy_b3+S50RujeS{5F5s9HlY0}+sj77s6qE<-5;36MCeuwa0nzpJY*k|_pq;=D#? zKr<~3%uURVjibbQjX_*dm(4$i8Z<2 zibt|s?T_7>Q*8KLIgKaV&H2@qzsqLLmiVIC;{5*g(%Vu0<&y1o_c0k2RqU{O|E%-t zgc*;G1$RDJQ7U@!X{LYX&pDeizR1;opBKoq$hCeWd(MgGtFKxFTTR4*-a6#@E_6Gz z?eLEe-`EzYE>ky*w439-)O`DsuNevjed2rOOKy2l{dtj||DODwG=6dEu(ExfiNTEe->5)W*~(e?7*}N40c9_3?-xXJZ5X1pSLt^RVKDimug^5J<+mgk0*23 zcb~KO=FaVD=-IU9-Cu*#E*ZBiR(5Bq{}biZk@K-GlM4NNbyAwl1U0c+ZpoVaZ^oo{ z3n@)_ShudwdEc+O@{Z{h2mV|S+3jjvFEh`}|LFcgrft8ru6X_+PckUCw6rc-b7zcs zZBm-bmipNS-|H`XUcKC46Q{A4-Sz0^Uu!2$vpC7I=l+sq-Twa*%2PHSI=!;|X~#j` z7YVPf=0@MS(aE-0D&W59q5F4_%n$v~@UGZsl9;DxqVx;HedpfC+ Date: Fri, 9 Jan 2026 16:35:28 +0200 Subject: [PATCH 02/65] Added CDoc1 tests and cleaned up testing Signed-off-by: Lauris Kaplinski --- cdoc/CDocCipher.cpp | 10 +- cdoc/CDocCipher.h | 7 +- cdoc/RcptInfo.h | 5 +- cdoc/cdoc-tool.cpp | 20 +- test/CMakeLists.txt | 5 +- test/libcdoc_boost.cpp | 412 ++++++++++++++++++----------------------- 6 files changed, 202 insertions(+), 257 deletions(-) diff --git a/cdoc/CDocCipher.cpp b/cdoc/CDocCipher.cpp index 7d0fe9c7..df775e3b 100644 --- a/cdoc/CDocCipher.cpp +++ b/cdoc/CDocCipher.cpp @@ -227,9 +227,9 @@ struct ToolNetwork : public libcdoc::NetworkBackend { }; -int CDocCipher::writer_push(CDocWriter& writer, const vector& keys, const vector& files) +int CDocCipher::writer_push(CDocWriter& writer, const vector& rcpts, const vector& files) { - for (const libcdoc::Recipient& rcpt : keys) { + for (const libcdoc::Recipient& rcpt : rcpts) { int64_t result = writer.addRecipient(rcpt); if (result != libcdoc::OK) return result; } @@ -262,7 +262,7 @@ int CDocCipher::writer_push(CDocWriter& writer, const vector& keys, c #define PUSH true static bool -fill_recipients_from_rcpt_info(ToolConf& conf, ToolCrypto& crypto, std::vector& rcpts, RecipientInfoIdMap& crypto_rcpts, const RecipientInfoVector& recipients) +fill_recipients_from_rcpt_info(ToolConf& conf, ToolCrypto& crypto, std::vector& rcpts, RecipientInfoIdMap& crypto_rcpts, const std::vector& recipients) { int idx = 0; for (const auto& rcpt : recipients) { @@ -329,7 +329,7 @@ fill_recipients_from_rcpt_info(ToolConf& conf, ToolCrypto& crypto, std::vector& recipients) { RecipientInfoIdMap crypto_rcpts; ToolCrypto crypto(crypto_rcpts); @@ -550,7 +550,7 @@ int CDocCipher::Decrypt(const unique_ptr& rdr, unsigned int lock_idx } int -CDocCipher::ReEncrypt(ToolConf& conf, int lock_idx_base_1, const std::string& lock_label, const RcptInfo& lock_info, RecipientInfoVector& recipients) +CDocCipher::ReEncrypt(ToolConf& conf, int lock_idx_base_1, const std::string& lock_label, const RcptInfo& lock_info, std::vector& recipients) { // Decryption part RecipientInfoIdMap dec_info; diff --git a/cdoc/CDocCipher.h b/cdoc/CDocCipher.h index 84614bb4..bd3d8333 100644 --- a/cdoc/CDocCipher.h +++ b/cdoc/CDocCipher.h @@ -30,8 +30,7 @@ namespace libcdoc { -typedef typename std::map RecipientInfoIdMap; -typedef typename std::vector RecipientInfoVector; +typedef typename std::map RecipientInfoIdMap; class CDocCipher { @@ -40,12 +39,12 @@ class CDocCipher CDocCipher(const CDocCipher&) = delete; CDocCipher(CDocCipher&&) = delete; - int Encrypt(ToolConf& conf, RecipientInfoVector& recipients); + int Encrypt(ToolConf& conf, std::vector& recipients); int Decrypt(ToolConf& conf, int idx_base_1, const RcptInfo& recipient); int Decrypt(ToolConf& conf, const std::string& label, const RcptInfo& recipient); - int ReEncrypt(ToolConf& conf, int lock_idx_base_1, const std::string& lock_label, const RcptInfo& lock_info, RecipientInfoVector& recipients); + int ReEncrypt(ToolConf& conf, int lock_idx_base_1, const std::string& lock_label, const RcptInfo& lock_info, std::vector& recipients); void Locks(const char* file) const; diff --git a/cdoc/RcptInfo.h b/cdoc/RcptInfo.h index 86168c6a..c9ecd53f 100644 --- a/cdoc/RcptInfo.h +++ b/cdoc/RcptInfo.h @@ -47,6 +47,9 @@ struct RcptInfo { }; Type type; + // Locks label + std::string label; + // Certificate for encryption std::vector cert; // Pin or password std::vector secret; @@ -56,8 +59,6 @@ struct RcptInfo { std::string key_file_name; // ID code for shares server std::string id; - // Locks label - std::string label; }; } diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index 12adf66d..ad76cd88 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -44,7 +44,7 @@ static void print_usage(ostream& ofs) ofs << "cdoc-tool encrypt --rcpt RECIPIENT [--rcpt...] [-v1] [--genlabel] --out OUTPUTFILE FILE [FILE...]" << endl; ofs << " Encrypt files for one or more recipients" << endl; ofs << " RECIPIENT has to be one of the following:" << endl; - ofs << " [label]:cert:CERTIFICATE_HEX - public key from certificate" << endl; + ofs << " [label]:cert:CERTIFICATE_FILE - public key from certificate" << endl; ofs << " [label]:pkey:SECRET_KEY_HEX - public key" << endl; ofs << " [label]:pfkey:PUB_KEY_FILE - path to DER file with EC (secp384r1 curve) public key" << endl; ofs << " [label]:skey:SECRET_KEY_HEX - AES key" << endl; @@ -140,7 +140,7 @@ parse_common(ToolConf& conf, int arg_idx, int argc, char *argv[]) } static int -parse_rcpt(ToolConf& conf, RecipientInfoVector& rcpts, int& arg_idx, int argc, char *argv[]) +parse_rcpt(ToolConf& conf, std::vector& rcpts, int& arg_idx, int argc, char *argv[]) { string_view arg(argv[arg_idx]); if ((arg != "--rcpt") || ((arg_idx + 1) >= argc)) return 0; @@ -285,7 +285,7 @@ static int ParseAndEncrypt(int argc, char *argv[]) LOG_INFO("Encrypting"); ToolConf conf; - RecipientInfoVector rcpts; + std::vector rcpts; // // Parse all arguments into ToolConf structure @@ -325,7 +325,7 @@ static int ParseAndEncrypt(int argc, char *argv[]) } if (!conf.gen_label) { // If labels must not be generated then is there any Recipient without provided label? - auto rcpt_wo_label{ find_if(rcpts.cbegin(), rcpts.cend(), [](RecipientInfoVector::const_reference rcpt) -> bool {return rcpt.label.empty();}) }; + auto rcpt_wo_label{ find_if(rcpts.cbegin(), rcpts.cend(), [](std::vector::const_reference rcpt) -> bool {return rcpt.label.empty();}) }; if (rcpt_wo_label != rcpts.cend()) { if (rcpts.size() > 1) { LOG_ERROR("Not all Recipients have label"); @@ -352,7 +352,7 @@ static int ParseAndEncrypt(int argc, char *argv[]) // CDOC1 is supported only for encryption with certificate. if (conf.cdocVersion == 1) { - auto rcpt_type_non_cert{ find_if(rcpts.cbegin(), rcpts.cend(), [](RecipientInfoVector::const_reference rcpt) -> bool {return rcpt.type != RcptInfo::CERT;}) }; + auto rcpt_type_non_cert{ find_if(rcpts.cbegin(), rcpts.cend(), [](std::vector::const_reference rcpt) -> bool {return rcpt.type != RcptInfo::CERT;}) }; if (rcpt_type_non_cert != rcpts.cend()) { LOG_ERROR("CDOC version 1 container can be used for encryption with certificate only."); return 1; @@ -539,7 +539,7 @@ static int ParseAndDecrypt(int argc, char *argv[]) } CDocCipher cipher; - RcptInfo rcpt {RcptInfo::ANY, {}, ldata.secret, ldata.slot, ldata.key_id, ldata.key_label}; + RcptInfo rcpt {RcptInfo::ANY, {}, {}, ldata.secret, ldata.slot, ldata.key_id, ldata.key_label}; if (ldata.lock_idx != -1) { return cipher.Decrypt(conf, ldata.lock_idx, rcpt); } else { @@ -550,7 +550,7 @@ static int ParseAndDecrypt(int argc, char *argv[]) static int ParseAndReEncrypt(int argc, char *argv[]) { ToolConf conf; - RecipientInfoVector rcpts; + std::vector rcpts; LockData ldata; int arg_idx = 0; @@ -596,7 +596,7 @@ static int ParseAndReEncrypt(int argc, char *argv[]) if (!conf.gen_label) { // If labels must not be generated then is there any Recipient without provided label? - auto rcpt_wo_label{ find_if(rcpts.cbegin(), rcpts.cend(), [](RecipientInfoVector::const_reference rcpt) -> bool {return rcpt.label.empty();}) }; + auto rcpt_wo_label{ find_if(rcpts.cbegin(), rcpts.cend(), [](std::vector::const_reference rcpt) -> bool {return rcpt.label.empty();}) }; if (rcpt_wo_label != rcpts.cend()) { if (rcpts.size() > 1) { LOG_ERROR("Not all Recipients have label"); @@ -619,7 +619,7 @@ static int ParseAndReEncrypt(int argc, char *argv[]) // CDOC1 is supported only for encryption with certificate. if (conf.cdocVersion == 1) { - auto rcpt_type_non_cert{ find_if(rcpts.cbegin(), rcpts.cend(), [](RecipientInfoVector::const_reference rcpt) -> bool {return rcpt.type != RcptInfo::CERT;}) }; + auto rcpt_type_non_cert{ find_if(rcpts.cbegin(), rcpts.cend(), [](std::vector::const_reference rcpt) -> bool {return rcpt.type != RcptInfo::CERT;}) }; if (rcpt_type_non_cert != rcpts.cend()) { LOG_ERROR("CDOC version 1 container can be used for encryption with certificate only."); return 1; @@ -627,7 +627,7 @@ static int ParseAndReEncrypt(int argc, char *argv[]) } CDocCipher cipher; - RcptInfo rcpt {RcptInfo::ANY, {}, ldata.secret, ldata.slot, ldata.key_id, ldata.key_label}; + RcptInfo rcpt {RcptInfo::ANY, {}, {}, ldata.secret, ldata.slot, ldata.key_id, ldata.key_label}; if (ldata.lock_idx != -1) { return cipher.ReEncrypt(conf, ldata.lock_idx, ldata.lock_label, rcpt, rcpts); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4b60ec69..1e9f06c1 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,9 +3,6 @@ add_executable(unittests ../cdoc/CDocCipher.cpp ../cdoc/Crypto.cpp) -target_compile_definitions(unittests PRIVATE - DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data") - target_link_libraries(unittests OpenSSL::SSL cdoc @@ -13,7 +10,7 @@ target_link_libraries(unittests add_test(NAME runtest COMMAND ${CMAKE_CURRENT_BINARY_DIR}/unittests --build_info=YES --logger=HRF,all,stdout - WORKING_DIRECTORY $ + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/test ) add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} DEPENDS unittests) diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index d9ff9eab..3f61fc1d 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -32,7 +32,7 @@ #include "pipe.h" #ifndef DATA_DIR -#define DATA_DIR "." +#define DATA_DIR "data" #endif namespace btools = boost::test_tools; @@ -45,6 +45,8 @@ using namespace std; * @brief Unencrypted file name. */ constexpr string_view SourceFile("test_data.txt"); +constexpr string_view SourceFile2("test_data2.txt"); +constexpr string_view SourceFile3("test_data3.txt"); /** * @brief Encrypted file name. @@ -52,8 +54,10 @@ constexpr string_view SourceFile("test_data.txt"); constexpr string_view TargetFile("test_data.txt.cdoc"); constexpr string_view ECPrivKeyFile("ec-secp384r1-priv.der"); constexpr string_view ECPubKeyFile("ec-secp384r1-pub.der"); +constexpr string_view ECCertFile("ec-secp384r1-cert.der"); constexpr string_view RSAPrivKeyFile("rsa_2048_priv.der"); constexpr string_view RSAPubKeyFile("rsa_2048_pub.der"); +constexpr string_view RSACertFile("rsa_2048_cert.der"); const string Label("Proov"); @@ -110,34 +114,46 @@ class FixtureBase target /= fileName; } - /** - * @brief Checks if the file exists in the test data path. - * - * The method prepends the fileName with the test data path and checks its existence. If the file does not - * exist then appropriate message is appended to returned predicate_result object and the value of the object - * is set to false. - * @param fileName the name of the file thats existence has to be checked. - * @return predicate_result object with the check result. - */ - boost::test_tools::predicate_result DoesFileExist(const string& fileName) const + std::string formTargetFile(const std::string_view name) const { - fs::path file(testDataPath); - file /= fileName; - if (fs::exists(file)) - { - return true; - } - else - { - btools::predicate_result res(false); - res.message() << "File " << file << " does not exist"; - return res; + fs::path path(fs::path(tmpDataPath) / name); + if (fs::exists(path)) { + error_code e; + fs::remove(path, e); + if(e) BOOST_TEST_MESSAGE("Failed to remove file"); } + return path.string(); + } + + std::string checkDataFile(const std::string_view name) const + { + fs::path path(fs::path(testDataPath) / name); + BOOST_TEST_REQUIRE(fs::exists(path), "file " << name << " does not exist"); + return path.string(); + } + + std::string checkTargetFile(const std::string_view name) const + { + fs::path path(fs::path(tmpDataPath) / name); + BOOST_TEST_REQUIRE(fs::exists(path), "file " << name << " does not exist"); + return path.string(); + } + + std::vector fetchDataFile(const std::string_view name) const + { + fs::path path(fs::path(testDataPath) / name); + BOOST_TEST_REQUIRE(fs::exists(path), "file " << name << " does not exist"); + return libcdoc::readAllBytes(path.string()); } fs::path testDataPath = DATA_DIR; + fs::path tmpDataPath = fs::path(DATA_DIR) / "tmp"; fs::path sourceFilePath; - fs::path targetFilePath; + fs::path sourceFilePath2; + fs::path sourceFilePath3; + + std::vector sources = {"test_data.txt", "test_data2.txt", "test_data3.txt"}; + size_t max_filesize = 100000000; }; @@ -153,18 +169,8 @@ class EncryptFixture : public FixtureBase // Setup source, unencrypted file path FormFilePath(SourceFile, sourceFilePath); - - // Setup target, encrypted file path - FormFilePath(TargetFile, targetFilePath); - - // Remove target file if it exists - if (fs::exists(targetFilePath)) - { - error_code e; - fs::remove(targetFilePath, e); - if(e) - BOOST_TEST_MESSAGE("Failed to remove file"); - } + FormFilePath(SourceFile2, sourceFilePath2); + FormFilePath(SourceFile3, sourceFilePath3); } ~EncryptFixture() { BOOST_TEST_MESSAGE("Encrypt fixture deardown"); } @@ -218,9 +224,6 @@ class DecryptFixture : public FixtureBase // Setup source, encrypted file path FormFilePath(TargetFile, sourceFilePath); - - // Setup target, unencrypted file path - FormFilePath(SourceFile, targetFilePath); } ~DecryptFixture() @@ -229,6 +232,70 @@ class DecryptFixture : public FixtureBase } }; +static void +encrypt(unsigned int version, const std::vector& files, const std::string& container, std::vector& rcpts) { + libcdoc::ToolConf conf; + for (auto file : files) { + conf.input_files.push_back(file); + } + conf.out = container; + conf.cdocVersion = version; + + libcdoc::CDocCipher cipher; + BOOST_CHECK_EQUAL(cipher.Encrypt(conf, rcpts), 0); + + BOOST_TEST(fs::exists(fs::path(container)), "File " << container << " does not exist"); +} + +static void +encryptV1(const std::vector& files, const std::string& container, const std::vector& cert) { + std::vector rcpts { + {libcdoc::RcptInfo::CERT, {}, cert} + }; + encrypt(1, files, container, rcpts); +} + +static void +encryptV2(const std::vector& files, const std::string& container, const std::vector& cert) { + std::vector rcpts { + {libcdoc::RcptInfo::CERT, {}, cert} + }; + encrypt(2, files, container, rcpts); +} + +static void +decrypt(const std::vector& files, const std::string& container, const std::string& dir, libcdoc::RcptInfo& rcpt) +{ + libcdoc::ToolConf conf; + conf.input_files.push_back(container); + conf.out = dir; + + libcdoc::CDocCipher cipher; + if (rcpt.label.empty()) { + BOOST_CHECK_EQUAL(cipher.Decrypt(conf, 1, rcpt), 0); + } else { + BOOST_CHECK_EQUAL(cipher.Decrypt(conf, rcpt.label, rcpt), 0); + } + + fs::path path(dir); + for (auto file : files) { + BOOST_TEST(fs::exists(path / fs::path(file).filename()), "File " << file << " does not exist"); + } + + path = fs::path(container); + if (fs::exists(path)) { + error_code e; + fs::remove(path, e); + if(e) + BOOST_TEST_MESSAGE("Failed to remove file"); } +} + +static void +decrypt(const std::vector& files, const std::string& container, const std::string& dir, const std::vector& key) +{ + libcdoc::RcptInfo rcpt {libcdoc::RcptInfo::ANY, {}, {}, key}; + decrypt(files, container, dir, rcpt); +} static int unicode_to_utf8 (unsigned int uval, uint8_t *d, uint64_t size) { @@ -344,258 +411,141 @@ BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithPasswordAndLabel, FixtureBase, * u BOOST_AUTO_TEST_SUITE_END() -BOOST_AUTO_TEST_SUITE(PasswordUsageWithLabel) +// CDoc2 password and label +BOOST_AUTO_TEST_SUITE(PasswordUsageWithLabel) BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithPasswordAndLabel, EncryptFixture, * utf::description("Encrypting a file with password and given label")) { - // Check if the source, unecrypted file exists - BOOST_TEST_REQUIRE(fs::exists(sourceFilePath), "File " << sourceFilePath << " exists"); - - libcdoc::ToolConf conf; - conf.input_files.push_back(sourceFilePath.string()); - conf.out = targetFilePath.string(); - - libcdoc::RcptInfo rcpt; - rcpt.type = libcdoc::RcptInfo::PASSWORD; - rcpt.secret.assign(Password.cbegin(), Password.cend()); - rcpt.label = Label; - - libcdoc::RecipientInfoVector rcpts {rcpt}; - - libcdoc::CDocCipher cipher; - BOOST_CHECK_EQUAL(cipher.Encrypt(conf, rcpts), 0); - - // Validate the encrypted file - BOOST_TEST(ValidateEncryptedFile(targetFilePath)); + std::vector rcpts { + {libcdoc::RcptInfo::PASSWORD, Label, {}, std::vector(Password.cbegin(), Password.cend())} + }; + encrypt(2, {checkDataFile(sources[0])}, formTargetFile("PasswordUsageWithoutLabel.cdoc"), rcpts); } - BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithPasswordAndLabel, DecryptFixture, * utf::depends_on("PasswordUsageWithLabel/EncryptWithPasswordAndLabel") * utf::description("Decrypting a file with password and given label")) { - // Check if the source, encrypted file exists - BOOST_TEST_REQUIRE(fs::exists(sourceFilePath), "File " << sourceFilePath << " must exists"); - - auto tmp = testDataPath / "tmp"; - fs::remove_all(tmp); - fs::create_directory(tmp); - libcdoc::ToolConf conf; - conf.input_files.push_back(sourceFilePath.string()); - conf.out = tmp.string(); - - libcdoc::RcptInfo rcpt {libcdoc::RcptInfo::ANY, {}, vector(Password.cbegin(), Password.cend())}; - - libcdoc::CDocCipher cipher; - BOOST_CHECK_EQUAL(cipher.Decrypt(conf, Label, rcpt), 0); - - // Check if the encrypted file exists - BOOST_TEST(fs::exists(targetFilePath), "File " << targetFilePath << " exists"); + libcdoc::RcptInfo rcpt {libcdoc::RcptInfo::ANY, Label, {}, std::vector(Password.cbegin(), Password.cend())}; + decrypt({checkDataFile(sources[0])}, checkTargetFile("PasswordUsageWithoutLabel.cdoc"), tmpDataPath, rcpt); } - BOOST_AUTO_TEST_SUITE_END() -BOOST_AUTO_TEST_SUITE(PasswordUsageWithoutLabel) +// CDoc2 password and label +BOOST_AUTO_TEST_SUITE(PasswordUsageWithoutLabel) BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithPasswordWithoutLabel, EncryptFixture, * utf::description("Encrypting a file with password and without label")) { - // Check if the source, unecrypted file exists - BOOST_TEST_REQUIRE(fs::exists(sourceFilePath), "File " << sourceFilePath << " must exists"); - - libcdoc::ToolConf conf; - conf.gen_label = true; - conf.input_files.push_back(sourceFilePath.string()); - conf.out = targetFilePath.string(); - - libcdoc::RcptInfo rcpt; - rcpt.type = libcdoc::RcptInfo::PASSWORD; - rcpt.secret.assign(Password.cbegin(), Password.cend()); - - libcdoc::RecipientInfoVector rcpts {rcpt}; - - libcdoc::CDocCipher cipher; - BOOST_CHECK_EQUAL(cipher.Encrypt(conf, rcpts), 0); - - // Validate the encrypted file - BOOST_TEST(ValidateEncryptedFile(targetFilePath)); + std::vector rcpts { + {libcdoc::RcptInfo::PASSWORD, {}, {}, std::vector(Password.cbegin(), Password.cend())} + }; + encrypt(2, {checkDataFile(sources[0])}, formTargetFile("PasswordUsageWithoutLabel.cdoc"), rcpts); } - BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithPasswordLabelIndex, DecryptFixture, * utf::depends_on("PasswordUsageWithoutLabel/EncryptWithPasswordWithoutLabel") * utf::description("Decrypting a file with password and label index")) { - // Check if the source, encrypted file exists - BOOST_TEST_REQUIRE(fs::exists(sourceFilePath), "File " << sourceFilePath << " must exists"); - - libcdoc::ToolConf conf; - conf.input_files.push_back(sourceFilePath.string()); - conf.out = testDataPath.string(); - - libcdoc::RcptInfo rcpt {libcdoc::RcptInfo::ANY, {}, vector(Password.cbegin(), Password.cend())}; - - libcdoc::CDocCipher cipher; - BOOST_CHECK_EQUAL(cipher.Decrypt(conf, 1, rcpt), 0); - - // Check if the encrypted file exists - BOOST_TEST(fs::exists(targetFilePath), "File " << targetFilePath << " exists"); + decrypt({checkDataFile(sources[0])}, checkTargetFile("PasswordUsageWithoutLabel.cdoc"), tmpDataPath, std::vector(Password.cbegin(), Password.cend())); } - BOOST_AUTO_TEST_SUITE_END() -BOOST_AUTO_TEST_SUITE(AESKeyUsage) +// CDoc2 AES key +BOOST_AUTO_TEST_SUITE(AESKeyUsage) BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithAESKey, EncryptFixture, * utf::description("Encrypting a file with symmetric AES key")) { - // Check if the source, unecrypted file exists - BOOST_TEST_REQUIRE(fs::exists(sourceFilePath), "File " << sourceFilePath << " must exists"); - - libcdoc::ToolConf conf; - conf.input_files.push_back(sourceFilePath.string()); - conf.out = targetFilePath.string(); - - libcdoc::RcptInfo rcpt; - rcpt.type = libcdoc::RcptInfo::SKEY; - rcpt.secret = std::move(libcdoc::fromHex(AESKey)); - rcpt.label = Label; - - libcdoc::RecipientInfoVector rcpts {rcpt}; - - libcdoc::CDocCipher cipher; - BOOST_CHECK_EQUAL(cipher.Encrypt(conf, rcpts), 0); - - // Validate the encrypted file - BOOST_TEST(ValidateEncryptedFile(targetFilePath)); + std::vector rcpts { + {libcdoc::RcptInfo::SKEY, {}, {}, libcdoc::fromHex(AESKey)} + }; + encrypt(2, {checkDataFile(sources[0])}, formTargetFile("AESKeyUsage.cdoc"), rcpts); } - BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithAESKey, DecryptFixture, * utf::depends_on("AESKeyUsage/EncryptWithAESKey") * utf::description("Decrypting a file with with symmetric AES key")) { - // Check if the source, encrypted file exists - BOOST_TEST_REQUIRE(fs::exists(sourceFilePath), "File " << sourceFilePath << " must exists"); - - libcdoc::ToolConf conf; - conf.input_files.push_back(sourceFilePath.string()); - conf.out = testDataPath.string(); - - libcdoc::RcptInfo rcpt {libcdoc::RcptInfo::ANY, {}, libcdoc::fromHex(AESKey)}; - - libcdoc::CDocCipher cipher; - BOOST_CHECK_EQUAL(cipher.Decrypt(conf, Label, rcpt), 0); - - // Check if the encrypted file exists - BOOST_TEST(fs::exists(targetFilePath), "File " << targetFilePath << " exists"); + decrypt({checkDataFile(sources[0])}, checkTargetFile("AESKeyUsage.cdoc"), tmpDataPath, libcdoc::fromHex(AESKey)); } - BOOST_AUTO_TEST_SUITE_END() -BOOST_AUTO_TEST_SUITE(ECKeyUsage) +// CDoc2 EC public/private key +BOOST_AUTO_TEST_SUITE(ECKeyUsage) BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithECKey, EncryptFixture, * utf::description("Encrypting a file with EC key")) { - // Check if the source and public key file exists - BOOST_TEST_REQUIRE(fs::exists(sourceFilePath), "File " << sourceFilePath << " must exists"); - fs::path keyPath; - FormFilePath(ECPubKeyFile, keyPath); - BOOST_TEST_REQUIRE(fs::exists(keyPath), "File " << keyPath << " must exists"); - - libcdoc::ToolConf conf; - conf.input_files.push_back(sourceFilePath.string()); - conf.out = targetFilePath.string(); - - libcdoc::RcptInfo rcpt; - rcpt.type = libcdoc::RcptInfo::PKEY; - rcpt.secret = libcdoc::readAllBytes(keyPath.string()); - rcpt.label = Label; - - libcdoc::RecipientInfoVector rcpts {rcpt}; - - libcdoc::CDocCipher cipher; - BOOST_CHECK_EQUAL(cipher.Encrypt(conf, rcpts), 0); - - // Validate the encrypted file - BOOST_TEST(ValidateEncryptedFile(targetFilePath)); + std::vector rcpts { + {libcdoc::RcptInfo::PKEY, {}, {}, fetchDataFile(ECPubKeyFile)} + }; + encrypt(2, {checkDataFile(sources[0])}, formTargetFile("ECKeyUsage.cdoc"), rcpts); } - BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithECKey, DecryptFixture, * utf::depends_on("ECKeyUsage/EncryptWithECKey") * utf::description("Decrypting a file with with EC private key")) { - // Check if the source, encrypted file exists - BOOST_TEST_REQUIRE(fs::exists(sourceFilePath), "File " << sourceFilePath << " must exists"); - fs::path keyPath; - FormFilePath(ECPrivKeyFile, keyPath); - BOOST_TEST_REQUIRE(fs::exists(keyPath), "File " << keyPath << " must exists"); - - libcdoc::ToolConf conf; - conf.input_files.push_back(sourceFilePath.string()); - conf.out = testDataPath.string(); - - libcdoc::RcptInfo rcpt {libcdoc::RcptInfo::ANY, {}, libcdoc::readAllBytes(keyPath.string())}; - - libcdoc::CDocCipher cipher; - BOOST_CHECK_EQUAL(cipher.Decrypt(conf, Label, rcpt), 0); - - // Check if the encrypted file exists - BOOST_TEST(fs::exists(targetFilePath), "File " << targetFilePath << " exists"); + decrypt({checkDataFile(sources[0])}, checkTargetFile("ECKeyUsage.cdoc"), tmpDataPath, fetchDataFile(ECPrivKeyFile)); } - BOOST_AUTO_TEST_SUITE_END() -BOOST_AUTO_TEST_SUITE(RSAKeyUsage) +// CDoc2 RSA public/private key +BOOST_AUTO_TEST_SUITE(RSAKeyUsage) BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithRSAKey, EncryptFixture, * utf::description("Encrypting a file with RSA key")) { - // Check if the source and public key file exists - BOOST_TEST_REQUIRE(fs::exists(sourceFilePath), "File " << sourceFilePath << " must exists"); - fs::path keyPath; - FormFilePath(RSAPubKeyFile, keyPath); - BOOST_TEST_REQUIRE(fs::exists(keyPath), "File " << keyPath << " must exists"); - - libcdoc::ToolConf conf; - conf.input_files.push_back(sourceFilePath.string()); - conf.out = targetFilePath.string(); - - libcdoc::RcptInfo rcpt; - rcpt.type = libcdoc::RcptInfo::PKEY; - rcpt.secret = libcdoc::readAllBytes(keyPath.string()); - rcpt.label = Label; - - libcdoc::RecipientInfoVector rcpts {rcpt}; - - libcdoc::CDocCipher cipher; - BOOST_CHECK_EQUAL(cipher.Encrypt(conf, rcpts), 0); - - // Validate the encrypted file - BOOST_TEST(ValidateEncryptedFile(targetFilePath)); + std::vector rcpts { + {libcdoc::RcptInfo::PKEY, {}, {}, fetchDataFile(RSAPubKeyFile)} + }; + encrypt(2, {checkDataFile(sources[0])}, formTargetFile("RSAKeyUsage.cdoc"), rcpts); } - BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithRSAKey, DecryptFixture, * utf::depends_on("RSAKeyUsage/EncryptWithRSAKey") * utf::description("Decrypting a file with with RSA private key")) { - // Check if the source, encrypted file exists - BOOST_TEST_REQUIRE(fs::exists(sourceFilePath), "File " << sourceFilePath << " must exists"); - fs::path keyPath; - FormFilePath(RSAPrivKeyFile, keyPath); - BOOST_TEST_REQUIRE(fs::exists(keyPath), "File " << keyPath << " must exists"); - - libcdoc::ToolConf conf; - conf.input_files.push_back(sourceFilePath.string()); - conf.out = testDataPath.string(); + decrypt({checkDataFile(sources[0])}, checkTargetFile("RSAKeyUsage.cdoc"), tmpDataPath, fetchDataFile(RSAPrivKeyFile)); +} +BOOST_AUTO_TEST_SUITE_END() - libcdoc::RcptInfo rcpt {libcdoc::RcptInfo::ANY, {}, libcdoc::readAllBytes(keyPath.string())}; +// CDoc1 tests - libcdoc::CDocCipher cipher; - BOOST_CHECK_EQUAL(cipher.Decrypt(conf, Label, rcpt), 0); +BOOST_AUTO_TEST_SUITE(CDoc1ECKeySingle) +BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithECKeyV1, EncryptFixture, * utf::description("Encrypting a file with EC key in CDoc1 format")) +{ + encryptV1({checkDataFile(sources[0])}, formTargetFile("ECKeyUsageV1.cdoc"), fetchDataFile(ECCertFile)); +} +BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithECKeyV1, DecryptFixture, + * utf::depends_on("CDoc1ECKeySingle/EncryptWithECKeyV1") + * utf::description("Decrypting a file in CDoc1 format with with EC private key")) +{ + decrypt({checkDataFile(sources[0])}, checkTargetFile("ECKeyUsageV1.cdoc"), tmpDataPath, fetchDataFile(ECPrivKeyFile)); +} +BOOST_AUTO_TEST_SUITE_END() - // Check if the encrypted file exists - BOOST_TEST(fs::exists(targetFilePath), "File " << targetFilePath << " exists"); +BOOST_AUTO_TEST_SUITE(CDoc1ECKeyMulti) +BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithECKeyV1Multi, EncryptFixture, * utf::description("Encrypting multiple files with EC key in CDoc1 format")) +{ + encryptV1({checkDataFile(sources[0]), checkDataFile(sources[1]), checkDataFile(sources[2])}, formTargetFile("ECKeyUsageV1Multi.cdoc"), fetchDataFile(ECCertFile)); +} +BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithECKeyV1Multi, DecryptFixture, + * utf::depends_on("CDoc1ECKeyMulti/EncryptWithECKeyV1Multi") + * utf::description("Decrypting multiple files in CDoc1 format with with EC private key")) +{ + decrypt({checkDataFile(sources[0]), checkDataFile(sources[1]), checkDataFile(sources[2])}, checkTargetFile("ECKeyUsageV1Multi.cdoc"), tmpDataPath, fetchDataFile(ECPrivKeyFile)); } +BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE(CDoc1RSAKeySingle) +BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithRSAKeyV1, EncryptFixture, * utf::description("Encrypting a file with RSA key in CDoc1 format")) +{ + encryptV1({checkDataFile(sources[0])}, formTargetFile("RSAKeyUsageV1.cdoc"), fetchDataFile(RSACertFile)); +} +BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithRSAKeyV1, DecryptFixture, + * utf::depends_on("CDoc1RSAKeySingle/EncryptWithRSAKeyV1") + * utf::description("Decrypting a file in CDoc1 format with with RSA private key")) +{ + decrypt({checkDataFile(sources[0])}, checkTargetFile("RSAKeyUsageV1.cdoc"), tmpDataPath, fetchDataFile(RSAPrivKeyFile)); +} BOOST_AUTO_TEST_SUITE_END() -BOOST_AUTO_TEST_SUITE(MachineLabelParsing) +// Label parsing +BOOST_AUTO_TEST_SUITE(MachineLabelParsing) BOOST_AUTO_TEST_CASE(PlainLabelParsing) { const string label("data:v=1&type=ID-card&serial_number=PNOEE-38001085718&cn=J%C3%95EORG%2CJAAK-KRISTJAN%2C38001085718"); @@ -643,11 +593,9 @@ BOOST_AUTO_TEST_CASE(Base64LabelParsingWithMediaType) } } } - BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(StreamingDecryption) - using BufTypes = std::tuple, std::array, std::array, std::array>; BOOST_AUTO_TEST_CASE_TEMPLATE(constructor, Buf, BufTypes) { From 26297566d95f4949ea7e0d1eb834b06fa82796d7 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Tue, 13 Jan 2026 17:06:16 +0200 Subject: [PATCH 03/65] More CDocCipher cleanups --- cdoc/CDocCipher.cpp | 134 ++++++++++++++++------------------------- cdoc/CDocCipher.h | 7 +-- cdoc/RcptInfo.h | 22 +++++-- cdoc/cdoc-tool.cpp | 42 ++++++------- test/libcdoc_boost.cpp | 10 +-- 5 files changed, 94 insertions(+), 121 deletions(-) diff --git a/cdoc/CDocCipher.cpp b/cdoc/CDocCipher.cpp index df775e3b..e93d5db1 100644 --- a/cdoc/CDocCipher.cpp +++ b/cdoc/CDocCipher.cpp @@ -43,46 +43,43 @@ using namespace libcdoc; static string GenerateRandomSequence(); struct ToolPKCS11 : public libcdoc::PKCS11Backend { - const RecipientInfoIdMap& rcpts; + const std::map& rcpts; - ToolPKCS11(const std::string& library, const RecipientInfoIdMap& vec) : libcdoc::PKCS11Backend(library), rcpts(vec) {} + ToolPKCS11(const std::string& library, const std::map& vec) : libcdoc::PKCS11Backend(library), rcpts(vec) {} libcdoc::result_t connectToKey(int idx, bool priv) override final { if (!rcpts.contains(idx)) return libcdoc::CRYPTO_ERROR; - const RcptInfo& rcpt = rcpts.at(idx); - int result = libcdoc::CRYPTO_ERROR; + libcdoc::RcptInfo rcpt = rcpts.at(idx); if (!priv) { - result = useSecretKey(rcpt.slot, rcpt.secret, rcpt.key_id, rcpt.key_label); + return useSecretKey(rcpt.p11.slot, rcpt.secret, rcpt.p11.key_id, rcpt.p11.key_label); } else { - result = usePrivateKey(rcpt.slot, rcpt.secret, rcpt.key_id, rcpt.key_label); + return usePrivateKey(rcpt.p11.slot, rcpt.secret, rcpt.p11.key_id, rcpt.p11.key_label); } - if (result != libcdoc::OK) return result; - return libcdoc::OK; } }; #ifdef _WIN32 struct ToolWin : public libcdoc::WinBackend { - const RecipientInfoIdMap& rcpts; + const std::map& rcpts; - ToolWin(const std::string& provider, const RecipientInfoIdMap& vec) : libcdoc::WinBackend(provider), rcpts(vec) {} + ToolWin(const std::string& provider, const std::map& vec) : libcdoc::WinBackend(provider), rcpts(vec) {} result_t connectToKey(int idx, bool priv) { - const RcptInfo& rcpt = rcpts.at(idx); - return useKey(rcpt.key_label, std::string(rcpt.secret.cbegin(), rcpt.secret.cend())); + if (!rcpts.contains(idx)) return libcdoc::CRYPTO_ERROR; + libcdoc::RcptInfo rcpt = rcpts.at(idx); + return useKey(rcpt.p11.key_label, std::string(rcpt.secret.cbegin(), rcpt.secret.cend())); } - }; #endif struct ToolCrypto : public libcdoc::CryptoBackend { - const RecipientInfoIdMap& rcpts; + const std::map& rcpts; std::unique_ptr p11; #ifdef _WIN32 std::unique_ptr ncrypt; #endif - ToolCrypto(const RecipientInfoIdMap& recipients) : rcpts(recipients) { + ToolCrypto(const std::map& recipients) : rcpts(recipients) { } bool connectLibrary(const std::string& library) { @@ -102,7 +99,7 @@ struct ToolCrypto : public libcdoc::CryptoBackend { libcdoc::result_t decryptRSA(std::vector& dst, const std::vector &data, bool oaep, unsigned int idx) override final { if (p11) return p11->decryptRSA(dst, data, oaep, idx); if (!rcpts.contains(idx)) return libcdoc::CRYPTO_ERROR; - const RcptInfo& rcpt = rcpts.at(idx); + libcdoc::RcptInfo rcpt = rcpts.at(idx); if (rcpt.secret.empty()) return libcdoc::CRYPTO_ERROR; const uint8_t *p = rcpt.secret.data(); @@ -134,7 +131,7 @@ struct ToolCrypto : public libcdoc::CryptoBackend { libcdoc::result_t deriveECDH1(std::vector& dst, const std::vector &public_key, unsigned int idx) override final { if (!rcpts.contains(idx)) return libcdoc::CRYPTO_ERROR; - const RcptInfo& rcpt = rcpts.at(idx); + libcdoc::RcptInfo rcpt = rcpts.at(idx); if (rcpt.secret.empty()) return libcdoc::CRYPTO_ERROR; const uint8_t *p = rcpt.secret.data(); @@ -151,7 +148,7 @@ struct ToolCrypto : public libcdoc::CryptoBackend { (EVP_PKEY_paramgen(ctx.get(), ¶ms) < 0)) return libcdoc::CRYPTO_ERROR; - p = public_key.data(); + p = public_key.data(); auto pubkey = make_unique_ptr(d2i_PublicKey(EVP_PKEY_EC, ¶ms, &p, long(public_key.size()))); if (!pubkey) return libcdoc::CRYPTO_ERROR; @@ -187,7 +184,7 @@ struct ToolCrypto : public libcdoc::CryptoBackend { libcdoc::result_t getSecret(std::vector& secret, unsigned int idx) override final { if (!rcpts.contains(idx)) return libcdoc::CRYPTO_ERROR; - const RcptInfo& rcpt = rcpts.at(idx); + libcdoc::RcptInfo rcpt = rcpts.at(idx); secret = rcpt.secret; return secret.empty() ? INVALID_PARAMS : libcdoc::OK; } @@ -212,7 +209,7 @@ struct ToolNetwork : public libcdoc::NetworkBackend { if (!crypto->rcpts.contains(rcpt_idx)) return libcdoc::CRYPTO_ERROR; const RcptInfo& rcpt = crypto->rcpts.at(rcpt_idx); bool rsa = false; - return crypto->p11->getCertificate(dst, rsa, rcpt.slot, rcpt.secret, rcpt.key_id, rcpt.key_label); + return crypto->p11->getCertificate(dst, rsa, rcpt.p11.slot, rcpt.secret, rcpt.p11.key_id, rcpt.p11.key_label); } libcdoc::result_t getPeerTLSCertificates(std::vector> &dst) override final { @@ -262,7 +259,7 @@ int CDocCipher::writer_push(CDocWriter& writer, const vector& rcpts, #define PUSH true static bool -fill_recipients_from_rcpt_info(ToolConf& conf, ToolCrypto& crypto, std::vector& rcpts, RecipientInfoIdMap& crypto_rcpts, const std::vector& recipients) +fill_recipients_from_rcpt_info(ToolConf& conf, ToolCrypto& crypto, std::vector& rcpts, std::map& crypto_rcpts, const std::vector& recipients) { int idx = 0; for (const auto& rcpt : recipients) { @@ -305,9 +302,9 @@ fill_recipients_from_rcpt_info(ToolConf& conf, ToolCrypto& crypto, std::vector val; bool rsa; ToolPKCS11* p11 = dynamic_cast(crypto.p11.get()); - int result = p11->getPublicKey(val, rsa, rcpt.slot, rcpt.secret, rcpt.key_id, rcpt.key_label); + int result = p11->getPublicKey(val, rsa, rcpt.p11.slot, rcpt.secret, rcpt.p11.key_id, rcpt.p11.key_label); if (result != libcdoc::OK) { - LOG_ERROR("No such public key: {}", rcpt.key_label); + LOG_ERROR("No such public key: {}", rcpt.p11.key_label); continue; } LOG_DBG("Public key ({}): {}", rsa ? "rsa" : "ecc", toHex(val)); @@ -331,7 +328,7 @@ fill_recipients_from_rcpt_info(ToolConf& conf, ToolCrypto& crypto, std::vector& recipients) { - RecipientInfoIdMap crypto_rcpts; + std::map crypto_rcpts; ToolCrypto crypto(crypto_rcpts); ToolNetwork network(&crypto); network.certs = std::move(conf.accept_certs); @@ -372,48 +369,9 @@ int CDocCipher::Encrypt(ToolConf& conf, std::vector& recipien return result; } -int CDocCipher::Decrypt(ToolConf& conf, int idx_base_1, const RcptInfo& recipient) -{ - RecipientInfoIdMap rcpts; - ToolCrypto crypto(rcpts); - ToolNetwork network(&crypto); - network.certs = std::move(conf.accept_certs); - - unique_ptr rdr(CDocReader::createReader(conf.input_files[0], &conf, &crypto, &network)); - if (rdr) { - LOG_DBG("Reader created"); - } else { - LOG_ERROR("Cannot create reader (invalid file?)"); - return 1; - } - - // Acquire the locks and get the labels according to the index - const vector &locks = rdr->getLocks(); - int lock_idx = idx_base_1 - 1; - if (lock_idx < 0) { - LOG_ERROR("Indexing of labels starts from 1"); - return 1; - } - if (lock_idx >= locks.size()) { - LOG_ERROR("Label index is out of range"); - return 1; - } - rcpts[lock_idx] = recipient; - - const Lock& lock = locks[lock_idx]; - LOG_INFO("Found matching label: {}", lock.label); - network.rcpt_idx = lock_idx; - //rcpts[idx_base_1] = recipient; - - if (!conf.library.empty()) - crypto.connectLibrary(conf.library); - - return Decrypt(rdr, lock_idx, conf.out); -} - -int CDocCipher::Decrypt(ToolConf& conf, const std::string& label, const RcptInfo& recipient) +int CDocCipher::Decrypt(ToolConf& conf, const RcptInfo& recipient) { - RecipientInfoIdMap rcpts; + std::map rcpts; ToolCrypto crypto(rcpts); ToolNetwork network(&crypto); network.certs = std::move(conf.accept_certs); @@ -428,39 +386,45 @@ int CDocCipher::Decrypt(ToolConf& conf, const std::string& label, const RcptInfo } LOG_DBG("Reader created"); - // Acquire the locks and get the labels according to the index + // Find lock by label/index/certificate int lock_idx = -1; const vector& locks = rdr->getLocks(); - if (!label.empty()) { + if (!recipient.label.empty()) { LOG_DBG("Looking for lock by label"); for (unsigned int i = 0; i < locks.size(); i++) { - if (locks[i].label == label) { + if (locks[i].label == recipient.label) { lock_idx = i; break; } } + } else if (recipient.lock_idx >= 0) { + if (recipient.lock_idx >= locks.size()) { + LOG_ERROR("Label index is out of range"); + return 1; + } + lock_idx = recipient.lock_idx; } else if (crypto.p11) { bool isRsa; vector cert_bytes; ToolPKCS11* p11 = dynamic_cast(crypto.p11.get()); - int64_t result = p11->getCertificate(cert_bytes, isRsa, (int) recipient.slot, recipient.secret, recipient.key_id, recipient.key_label); + int64_t result = p11->getCertificate(cert_bytes, isRsa, (int) recipient.p11.slot, recipient.secret, recipient.p11.key_id, recipient.p11.key_label); if (result != libcdoc::OK) { - LOG_ERROR("Certificate reading from SC card failed. Key label: {}", recipient.key_label); + LOG_ERROR("Certificate reading from SC card failed. Key label: {}", recipient.p11.key_label); return 1; } LOG_DBG("Got certificate from P11 module"); result = rdr->getLockForCert(cert_bytes); if (result < 0) { - LOG_ERROR("No lock for certificate {}", recipient.key_label); + LOG_ERROR("No lock for certificate {}", recipient.p11.key_label); return 1; } lock_idx = (int) result; } if (lock_idx < 0) { - LOG_ERROR("Lock not found: {}", label); + LOG_ERROR("Lock not found: {}", recipient.label); return 1; } - LOG_INFO("Found matching label: {}", label); + LOG_INFO("Found matching lock: {}", recipient.label); rcpts[lock_idx] = recipient; network.rcpt_idx = lock_idx; @@ -550,10 +514,10 @@ int CDocCipher::Decrypt(const unique_ptr& rdr, unsigned int lock_idx } int -CDocCipher::ReEncrypt(ToolConf& conf, int lock_idx_base_1, const std::string& lock_label, const RcptInfo& lock_info, std::vector& recipients) +CDocCipher::ReEncrypt(ToolConf& conf, const RcptInfo& lock_info, std::vector& recipients) { // Decryption part - RecipientInfoIdMap dec_info; + std::map dec_info; ToolCrypto crypto(dec_info); ToolNetwork network(&crypto); network.certs = std::move(conf.accept_certs); @@ -571,26 +535,32 @@ CDocCipher::ReEncrypt(ToolConf& conf, int lock_idx_base_1, const std::string& lo const vector& locks = rdr->getLocks(); - int lock_idx = lock_idx_base_1 - 1; - if (lock_idx < 0) { + int lock_idx = -1; + if (!lock_info.label.empty()) { for (unsigned int i = 0; i < locks.size(); i++) { - if (locks[i].label == lock_label) { + if (locks[i].label == lock_info.label) { lock_idx = i; break; } } - if (lock_idx < 0) { - LOG_ERROR("Lock not found: {}", lock_label); + } else if (lock_info.lock_idx >= 0) { + if (lock_info.lock_idx >= locks.size()) { + LOG_ERROR("Label index is out of range"); return 1; } - LOG_INFO("Found matching label: {}", lock_label); + lock_idx = lock_info.lock_idx; } + if (lock_idx < 0) { + LOG_ERROR("Lock not found: {}", lock_info.label); + return 1; + } + dec_info[lock_idx] = lock_info; network.rcpt_idx = lock_idx; // Encryption part - RecipientInfoIdMap crypto_rcpts; + std::map crypto_rcpts; ToolCrypto enc_crypto(crypto_rcpts); ToolNetwork enc_network(&enc_crypto); enc_network.certs = std::move(conf.accept_certs); diff --git a/cdoc/CDocCipher.h b/cdoc/CDocCipher.h index bd3d8333..5563a8fc 100644 --- a/cdoc/CDocCipher.h +++ b/cdoc/CDocCipher.h @@ -30,7 +30,7 @@ namespace libcdoc { -typedef typename std::map RecipientInfoIdMap; +//typedef typename std::map RecipientInfoIdMap; class CDocCipher { @@ -41,10 +41,9 @@ class CDocCipher int Encrypt(ToolConf& conf, std::vector& recipients); - int Decrypt(ToolConf& conf, int idx_base_1, const RcptInfo& recipient); - int Decrypt(ToolConf& conf, const std::string& label, const RcptInfo& recipient); + int Decrypt(ToolConf& conf, const RcptInfo& recipient); - int ReEncrypt(ToolConf& conf, int lock_idx_base_1, const std::string& lock_label, const RcptInfo& lock_info, std::vector& recipients); + int ReEncrypt(ToolConf& conf, const RcptInfo& lock_info, std::vector& recipients); void Locks(const char* file) const; diff --git a/cdoc/RcptInfo.h b/cdoc/RcptInfo.h index c9ecd53f..e4142553 100644 --- a/cdoc/RcptInfo.h +++ b/cdoc/RcptInfo.h @@ -24,10 +24,19 @@ namespace libcdoc { struct RcptInfo { + // PKCS11/NCrypt data + // NB! PIN is stored in secret + struct PKCS11Info { + long slot = 0; + std::vector key_id; + std::string key_label; + }; + enum Type { - // Detect type from container - ANY, + // For decryption (use the lock type) + LOCK, + // For encryption // Certificate from file CERT, // Password from command line @@ -53,12 +62,15 @@ struct RcptInfo { std::vector cert; // Pin or password std::vector secret; - long slot = 0; - std::vector key_id; - std::string key_label; + // PKCS11-specific info + PKCS11Info p11; + + // Keyfile name for automatic labels std::string key_file_name; // ID code for shares server std::string id; + // Lock index + int lock_idx = -1; }; } diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index ad76cd88..45848c15 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -202,10 +202,10 @@ parse_rcpt(ToolConf& conf, std::vector& rcpts, int& arg_idx, size_t last_char_idx; if (parts[2].starts_with("0x")) { - rcpt.slot = std::stoul(parts[2].substr(2), &last_char_idx, 16); + rcpt.p11.slot = std::stoul(parts[2].substr(2), &last_char_idx, 16); last_char_idx += 2; } else { - rcpt.slot = std::stoul(parts[2], &last_char_idx); + rcpt.p11.slot = std::stoul(parts[2], &last_char_idx); } if (last_char_idx < parts[2].size()) { LOG_ERROR("Slot is not a number"); @@ -215,31 +215,31 @@ parse_rcpt(ToolConf& conf, std::vector& rcpts, int& arg_idx, if (parts.size() > 3) { rcpt.secret.assign(parts[3].cbegin(), parts[3].cend()); if (parts.size() > 4) { - if (!parts[4].empty()) rcpt.key_id = fromHex(parts[4]); + if (!parts[4].empty()) rcpt.p11.key_id = fromHex(parts[4]); if (parts.size() > 5) - rcpt.key_label = parts[5]; + rcpt.p11.key_label = parts[5]; } } #ifndef NDEBUG // For debugging LOG_DBG("Method: {}", method); - LOG_DBG("Slot: {}", rcpt.slot); + LOG_DBG("Slot: {}", rcpt.p11.slot); if (!rcpt.secret.empty()) { string str(rcpt.secret.cbegin(), rcpt.secret.cend()); LOG_TRACE("Pin: {}", str); } - if (!rcpt.key_id.empty()) - LOG_DBG("Key ID: {}", toHex(rcpt.key_id)); - if (!rcpt.key_label.empty()) - LOG_DBG("Key label: {}", rcpt.key_label); + if (!rcpt.p11.key_id.empty()) + LOG_DBG("Key ID: {}", toHex(rcpt.p11.key_id)); + if (!rcpt.p11.key_label.empty()) + LOG_DBG("Key label: {}", rcpt.p11.key_label); #endif } else if (method == "ncrypt") { // label:ncrypt:key_label[:pin] rcpt.type = RcptInfo::NCRYPT; if (parts.size() > 2) { - rcpt.key_label = parts[2]; + rcpt.p11.key_label = parts[2]; if (parts.size() > 3) { rcpt.secret.assign(parts[3].cbegin(), parts[3].cend()); } @@ -248,13 +248,13 @@ parse_rcpt(ToolConf& conf, std::vector& rcpts, int& arg_idx, #ifndef NDEBUG // For debugging cout << "Method: " << method << endl; - cout << "Slot: " << rcpt.slot << endl; + cout << "Slot: " << rcpt.p11.slot << endl; if (!rcpt.secret.empty()) cout << "Pin: " << string(rcpt.secret.cbegin(), rcpt.secret.cend()) << endl; - if (!rcpt.key_id.empty()) - cout << "Key ID: " << toHex(rcpt.key_id) << endl; - if (!rcpt.key_label.empty()) - cout << "Key label: " << rcpt.key_label << endl; + if (!rcpt.p11.key_id.empty()) + cout << "Key ID: " << toHex(rcpt.p11.key_id) << endl; + if (!rcpt.p11.key_label.empty()) + cout << "Key label: " << rcpt.p11.key_label << endl; #endif } else if (method == "share") { // label:share:RECIPIENT_ID @@ -539,12 +539,8 @@ static int ParseAndDecrypt(int argc, char *argv[]) } CDocCipher cipher; - RcptInfo rcpt {RcptInfo::ANY, {}, {}, ldata.secret, ldata.slot, ldata.key_id, ldata.key_label}; - if (ldata.lock_idx != -1) { - return cipher.Decrypt(conf, ldata.lock_idx, rcpt); - } else { - return cipher.Decrypt(conf, ldata.lock_label, rcpt); - } + RcptInfo rcpt {.type=RcptInfo::LOCK, .label=ldata.lock_label, .lock_idx=ldata.lock_idx - 1, .secret=ldata.secret, .p11={ldata.slot, ldata.key_id, ldata.key_label}}; + return cipher.Decrypt(conf, rcpt); } static int ParseAndReEncrypt(int argc, char *argv[]) @@ -627,9 +623,9 @@ static int ParseAndReEncrypt(int argc, char *argv[]) } CDocCipher cipher; - RcptInfo rcpt {RcptInfo::ANY, {}, {}, ldata.secret, ldata.slot, ldata.key_id, ldata.key_label}; + RcptInfo rcpt {.type=RcptInfo::LOCK, .label=ldata.lock_label, .lock_idx=ldata.lock_idx, .secret=ldata.secret, .p11={ldata.slot, ldata.key_id, ldata.key_label}}; if (ldata.lock_idx != -1) { - return cipher.ReEncrypt(conf, ldata.lock_idx, ldata.lock_label, rcpt, rcpts); + return cipher.ReEncrypt(conf, rcpt, rcpts); } return true; } diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 3f61fc1d..75015535 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -271,11 +271,7 @@ decrypt(const std::vector& files, const std::string& container, con conf.out = dir; libcdoc::CDocCipher cipher; - if (rcpt.label.empty()) { - BOOST_CHECK_EQUAL(cipher.Decrypt(conf, 1, rcpt), 0); - } else { - BOOST_CHECK_EQUAL(cipher.Decrypt(conf, rcpt.label, rcpt), 0); - } + BOOST_CHECK_EQUAL(cipher.Decrypt(conf, rcpt), 0); fs::path path(dir); for (auto file : files) { @@ -293,7 +289,7 @@ decrypt(const std::vector& files, const std::string& container, con static void decrypt(const std::vector& files, const std::string& container, const std::string& dir, const std::vector& key) { - libcdoc::RcptInfo rcpt {libcdoc::RcptInfo::ANY, {}, {}, key}; + libcdoc::RcptInfo rcpt {.type=libcdoc::RcptInfo::LOCK, .lock_idx=0, .secret=key}; decrypt(files, container, dir, rcpt); } static int @@ -425,7 +421,7 @@ BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithPasswordAndLabel, DecryptFixture, * utf::depends_on("PasswordUsageWithLabel/EncryptWithPasswordAndLabel") * utf::description("Decrypting a file with password and given label")) { - libcdoc::RcptInfo rcpt {libcdoc::RcptInfo::ANY, Label, {}, std::vector(Password.cbegin(), Password.cend())}; + libcdoc::RcptInfo rcpt {.type=libcdoc::RcptInfo::LOCK, .label=Label, .secret=std::vector(Password.cbegin(), Password.cend())}; decrypt({checkDataFile(sources[0])}, checkTargetFile("PasswordUsageWithoutLabel.cdoc"), tmpDataPath, rcpt); } BOOST_AUTO_TEST_SUITE_END() From 4febb5c6f1b919a366442fd6fec6f164683c42ac Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Tue, 13 Jan 2026 17:19:52 +0200 Subject: [PATCH 04/65] Fix windows build --- test/libcdoc_boost.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 75015535..035b4e35 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -440,7 +440,7 @@ BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithPasswordLabelIndex, DecryptFixture * utf::depends_on("PasswordUsageWithoutLabel/EncryptWithPasswordWithoutLabel") * utf::description("Decrypting a file with password and label index")) { - decrypt({checkDataFile(sources[0])}, checkTargetFile("PasswordUsageWithoutLabel.cdoc"), tmpDataPath, std::vector(Password.cbegin(), Password.cend())); + decrypt({checkDataFile(sources[0])}, checkTargetFile("PasswordUsageWithoutLabel.cdoc"), tmpDataPath.string(), std::vector(Password.cbegin(), Password.cend())); } BOOST_AUTO_TEST_SUITE_END() @@ -458,7 +458,7 @@ BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithAESKey, DecryptFixture, * utf::depends_on("AESKeyUsage/EncryptWithAESKey") * utf::description("Decrypting a file with with symmetric AES key")) { - decrypt({checkDataFile(sources[0])}, checkTargetFile("AESKeyUsage.cdoc"), tmpDataPath, libcdoc::fromHex(AESKey)); + decrypt({checkDataFile(sources[0])}, checkTargetFile("AESKeyUsage.cdoc"), tmpDataPath.string(), libcdoc::fromHex(AESKey)); } BOOST_AUTO_TEST_SUITE_END() @@ -476,7 +476,7 @@ BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithECKey, DecryptFixture, * utf::depends_on("ECKeyUsage/EncryptWithECKey") * utf::description("Decrypting a file with with EC private key")) { - decrypt({checkDataFile(sources[0])}, checkTargetFile("ECKeyUsage.cdoc"), tmpDataPath, fetchDataFile(ECPrivKeyFile)); + decrypt({checkDataFile(sources[0])}, checkTargetFile("ECKeyUsage.cdoc"), tmpDataPath.string(), fetchDataFile(ECPrivKeyFile)); } BOOST_AUTO_TEST_SUITE_END() @@ -494,7 +494,7 @@ BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithRSAKey, DecryptFixture, * utf::depends_on("RSAKeyUsage/EncryptWithRSAKey") * utf::description("Decrypting a file with with RSA private key")) { - decrypt({checkDataFile(sources[0])}, checkTargetFile("RSAKeyUsage.cdoc"), tmpDataPath, fetchDataFile(RSAPrivKeyFile)); + decrypt({checkDataFile(sources[0])}, checkTargetFile("RSAKeyUsage.cdoc"), tmpDataPath.string(), fetchDataFile(RSAPrivKeyFile)); } BOOST_AUTO_TEST_SUITE_END() @@ -509,7 +509,7 @@ BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithECKeyV1, DecryptFixture, * utf::depends_on("CDoc1ECKeySingle/EncryptWithECKeyV1") * utf::description("Decrypting a file in CDoc1 format with with EC private key")) { - decrypt({checkDataFile(sources[0])}, checkTargetFile("ECKeyUsageV1.cdoc"), tmpDataPath, fetchDataFile(ECPrivKeyFile)); + decrypt({checkDataFile(sources[0])}, checkTargetFile("ECKeyUsageV1.cdoc"), tmpDataPath.string(), fetchDataFile(ECPrivKeyFile)); } BOOST_AUTO_TEST_SUITE_END() @@ -522,7 +522,7 @@ BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithECKeyV1Multi, DecryptFixture, * utf::depends_on("CDoc1ECKeyMulti/EncryptWithECKeyV1Multi") * utf::description("Decrypting multiple files in CDoc1 format with with EC private key")) { - decrypt({checkDataFile(sources[0]), checkDataFile(sources[1]), checkDataFile(sources[2])}, checkTargetFile("ECKeyUsageV1Multi.cdoc"), tmpDataPath, fetchDataFile(ECPrivKeyFile)); + decrypt({checkDataFile(sources[0]), checkDataFile(sources[1]), checkDataFile(sources[2])}, checkTargetFile("ECKeyUsageV1Multi.cdoc"), tmpDataPath.string(), fetchDataFile(ECPrivKeyFile)); } BOOST_AUTO_TEST_SUITE_END() @@ -535,7 +535,7 @@ BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithRSAKeyV1, DecryptFixture, * utf::depends_on("CDoc1RSAKeySingle/EncryptWithRSAKeyV1") * utf::description("Decrypting a file in CDoc1 format with with RSA private key")) { - decrypt({checkDataFile(sources[0])}, checkTargetFile("RSAKeyUsageV1.cdoc"), tmpDataPath, fetchDataFile(RSAPrivKeyFile)); + decrypt({checkDataFile(sources[0])}, checkTargetFile("RSAKeyUsageV1.cdoc"), tmpDataPath.string(), fetchDataFile(RSAPrivKeyFile)); } BOOST_AUTO_TEST_SUITE_END() From dd2cf6838837297335ca2f081c0f6cdbb8d42d82 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Tue, 13 Jan 2026 17:28:32 +0200 Subject: [PATCH 05/65] Fix initializer order for gcc --- cdoc/cdoc-tool.cpp | 4 ++-- test/libcdoc_boost.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index 45848c15..7c81c1a8 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -539,7 +539,7 @@ static int ParseAndDecrypt(int argc, char *argv[]) } CDocCipher cipher; - RcptInfo rcpt {.type=RcptInfo::LOCK, .label=ldata.lock_label, .lock_idx=ldata.lock_idx - 1, .secret=ldata.secret, .p11={ldata.slot, ldata.key_id, ldata.key_label}}; + RcptInfo rcpt {.type=RcptInfo::LOCK, .label=ldata.lock_label, .secret=ldata.secret, .p11={ldata.slot, ldata.key_id, ldata.key_label}, .lock_idx=ldata.lock_idx - 1}; return cipher.Decrypt(conf, rcpt); } @@ -623,7 +623,7 @@ static int ParseAndReEncrypt(int argc, char *argv[]) } CDocCipher cipher; - RcptInfo rcpt {.type=RcptInfo::LOCK, .label=ldata.lock_label, .lock_idx=ldata.lock_idx, .secret=ldata.secret, .p11={ldata.slot, ldata.key_id, ldata.key_label}}; + RcptInfo rcpt {.type=RcptInfo::LOCK, .label=ldata.lock_label, .secret=ldata.secret, .p11={ldata.slot, ldata.key_id, ldata.key_label}, .lock_idx=ldata.lock_idx}; if (ldata.lock_idx != -1) { return cipher.ReEncrypt(conf, rcpt, rcpts); } diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 035b4e35..982ea26f 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -289,7 +289,7 @@ decrypt(const std::vector& files, const std::string& container, con static void decrypt(const std::vector& files, const std::string& container, const std::string& dir, const std::vector& key) { - libcdoc::RcptInfo rcpt {.type=libcdoc::RcptInfo::LOCK, .lock_idx=0, .secret=key}; + libcdoc::RcptInfo rcpt {.type=libcdoc::RcptInfo::LOCK, .secret=key, .lock_idx=0}; decrypt(files, container, dir, rcpt); } static int From 5e4e80f65571cf2e47f1b51acf72dc110a3f0730 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Tue, 13 Jan 2026 17:38:37 +0200 Subject: [PATCH 06/65] Use again absolute paths for tests --- test/CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1e9f06c1..78f89cac 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,6 +3,10 @@ add_executable(unittests ../cdoc/CDocCipher.cpp ../cdoc/Crypto.cpp) +target_compile_definitions(unittests PRIVATE + DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" +) + target_link_libraries(unittests OpenSSL::SSL cdoc @@ -10,7 +14,7 @@ target_link_libraries(unittests add_test(NAME runtest COMMAND ${CMAKE_CURRENT_BINARY_DIR}/unittests --build_info=YES --logger=HRF,all,stdout - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/test + WORKING_DIRECTORY $ ) add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} DEPENDS unittests) From bb66be55ced8e6a9bf84f75ec3d844de18c27ddb Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Tue, 13 Jan 2026 18:02:19 +0200 Subject: [PATCH 07/65] Create test tmp path if not exist --- test/CMakeLists.txt | 6 +----- test/libcdoc_boost.cpp | 10 +++++++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 78f89cac..1e9f06c1 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,10 +3,6 @@ add_executable(unittests ../cdoc/CDocCipher.cpp ../cdoc/Crypto.cpp) -target_compile_definitions(unittests PRIVATE - DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" -) - target_link_libraries(unittests OpenSSL::SSL cdoc @@ -14,7 +10,7 @@ target_link_libraries(unittests add_test(NAME runtest COMMAND ${CMAKE_CURRENT_BINARY_DIR}/unittests --build_info=YES --logger=HRF,all,stdout - WORKING_DIRECTORY $ + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/test ) add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} DEPENDS unittests) diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 982ea26f..93e95350 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -101,6 +101,14 @@ class FixtureBase max_filesize = std::stoull(utf::framework::master_test_suite().argv[i]); } } + if (!fs::exists(testDataPath)) { + std::cerr << "Path " << testDataPath << " does not exist!" << std::endl; + ::exit(1); + } + tmpDataPath = fs::path(DATA_DIR) / "tmp"; + if (!fs::exists(tmpDataPath)) { + fs::create_directories(tmpDataPath); + } } /** @@ -147,7 +155,7 @@ class FixtureBase } fs::path testDataPath = DATA_DIR; - fs::path tmpDataPath = fs::path(DATA_DIR) / "tmp"; + fs::path tmpDataPath; fs::path sourceFilePath; fs::path sourceFilePath2; fs::path sourceFilePath3; From a4efe25f3c875e8f2ef20f2e63481d0fd697f54f Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 14 Jan 2026 12:50:02 +0200 Subject: [PATCH 08/65] Change SID/MID callback signature to handle QR codes --- cdoc/NetworkBackend.cpp | 14 ++++++++------ cdoc/NetworkBackend.h | 14 ++++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 40163de2..068236bf 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -539,9 +539,9 @@ rsa_sign(int type, const unsigned char *m, unsigned int m_len, unsigned char *si } libcdoc::result_t -libcdoc::NetworkBackend::showVerificationCode(unsigned int code) +libcdoc::NetworkBackend::showFeedback(SIDMIDFeedback& feedback) { - LOG_INFO("Verification code: {:04d}", code); + LOG_INFO("Verification code: {:04d} url: {}", feedback.code, feedback.url); return OK; } @@ -747,8 +747,9 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector // Generate code uint8_t b[32]; SHA256(digest.data(), digest.size(), b); - unsigned int code = ((b[30] << 8) | b[31]) % 10000; - result = showVerificationCode(code); + SIDMIDFeedback fb; + fb.code = ((b[30] << 8) | b[31]) % 10000; + result = showFeedback(fb); if (result != OK) return result; picojson::object aio1 = { @@ -834,8 +835,9 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector std::string algo_name = algo_names[(int) algo]; // Generate code - unsigned int code = (((digest[0] & 0xfc) << 5) | (digest[digest.size() - 1] & 0x7f)); - result = showVerificationCode(code); + SIDMIDFeedback fb; + fb.code = (((digest[0] & 0xfc) << 5) | (digest[digest.size() - 1] & 0x7f)); + result = showFeedback(fb); if (result != OK) return result; // etsi/PNOEE-01234567890 diff --git a/cdoc/NetworkBackend.h b/cdoc/NetworkBackend.h index 6c082240..69cef945 100644 --- a/cdoc/NetworkBackend.h +++ b/cdoc/NetworkBackend.h @@ -119,6 +119,11 @@ struct CDOC_EXPORT NetworkBackend { std::string password; }; + struct SIDMIDFeedback { + int code; + std::string url; + }; + NetworkBackend() = default; virtual ~NetworkBackend() noexcept = default; NetworkBackend(const NetworkBackend&) = delete; @@ -235,13 +240,14 @@ struct CDOC_EXPORT NetworkBackend { } /** - * @brief show MID/SID verification code + * @brief show MID/SID verification code or QR code + * + * Show SID/MID verification code or QR code. The default implementation logs the content with level INFO. * - * Show SID/MID verification code. The default implementation logs it with level INFO. - * @param code verification code + * @param feedback SID/MID feedback data * @return error code or OK */ - virtual result_t showVerificationCode(unsigned int code); + virtual result_t showFeedback(SIDMIDFeedback& feedback); /** * @brief Sign digest with SmartID authentication key From 0cab1cb98077067a32a55577a1be683fe5e4a1d3 Mon Sep 17 00:00:00 2001 From: lauris71 Date: Wed, 14 Jan 2026 13:17:19 +0200 Subject: [PATCH 09/65] Apply suggestions from code review Co-authored-by: Raul Metsma --- cdoc/CDocCipher.cpp | 8 ++++---- cdoc/CDocCipher.h | 1 - cdoc/cdoc-tool.cpp | 4 ++-- test/libcdoc_boost.cpp | 21 ++++++++++++++------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/cdoc/CDocCipher.cpp b/cdoc/CDocCipher.cpp index e93d5db1..b1f3230d 100644 --- a/cdoc/CDocCipher.cpp +++ b/cdoc/CDocCipher.cpp @@ -66,7 +66,7 @@ struct ToolWin : public libcdoc::WinBackend { result_t connectToKey(int idx, bool priv) { if (!rcpts.contains(idx)) return libcdoc::CRYPTO_ERROR; - libcdoc::RcptInfo rcpt = rcpts.at(idx); + const libcdoc::RcptInfo &rcpt = rcpts.at(idx); return useKey(rcpt.p11.key_label, std::string(rcpt.secret.cbegin(), rcpt.secret.cend())); } }; @@ -99,7 +99,7 @@ struct ToolCrypto : public libcdoc::CryptoBackend { libcdoc::result_t decryptRSA(std::vector& dst, const std::vector &data, bool oaep, unsigned int idx) override final { if (p11) return p11->decryptRSA(dst, data, oaep, idx); if (!rcpts.contains(idx)) return libcdoc::CRYPTO_ERROR; - libcdoc::RcptInfo rcpt = rcpts.at(idx); + const libcdoc::RcptInfo &rcpt = rcpts.at(idx); if (rcpt.secret.empty()) return libcdoc::CRYPTO_ERROR; const uint8_t *p = rcpt.secret.data(); @@ -131,7 +131,7 @@ struct ToolCrypto : public libcdoc::CryptoBackend { libcdoc::result_t deriveECDH1(std::vector& dst, const std::vector &public_key, unsigned int idx) override final { if (!rcpts.contains(idx)) return libcdoc::CRYPTO_ERROR; - libcdoc::RcptInfo rcpt = rcpts.at(idx); + const libcdoc::RcptInfo &rcpt = rcpts.at(idx); if (rcpt.secret.empty()) return libcdoc::CRYPTO_ERROR; const uint8_t *p = rcpt.secret.data(); @@ -184,7 +184,7 @@ struct ToolCrypto : public libcdoc::CryptoBackend { libcdoc::result_t getSecret(std::vector& secret, unsigned int idx) override final { if (!rcpts.contains(idx)) return libcdoc::CRYPTO_ERROR; - libcdoc::RcptInfo rcpt = rcpts.at(idx); + const libcdoc::RcptInfo &rcpt = rcpts.at(idx); secret = rcpt.secret; return secret.empty() ? INVALID_PARAMS : libcdoc::OK; } diff --git a/cdoc/CDocCipher.h b/cdoc/CDocCipher.h index 5563a8fc..42b76cfb 100644 --- a/cdoc/CDocCipher.h +++ b/cdoc/CDocCipher.h @@ -30,7 +30,6 @@ namespace libcdoc { -//typedef typename std::map RecipientInfoIdMap; class CDocCipher { diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index 7c81c1a8..080533de 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -592,7 +592,7 @@ static int ParseAndReEncrypt(int argc, char *argv[]) if (!conf.gen_label) { // If labels must not be generated then is there any Recipient without provided label? - auto rcpt_wo_label{ find_if(rcpts.cbegin(), rcpts.cend(), [](std::vector::const_reference rcpt) -> bool {return rcpt.label.empty();}) }; + auto rcpt_wo_label{ find_if(rcpts.cbegin(), rcpts.cend(), [](const libcdoc::RcptInfo &rcpt) -> bool {return rcpt.label.empty();}) }; if (rcpt_wo_label != rcpts.cend()) { if (rcpts.size() > 1) { LOG_ERROR("Not all Recipients have label"); @@ -615,7 +615,7 @@ static int ParseAndReEncrypt(int argc, char *argv[]) // CDOC1 is supported only for encryption with certificate. if (conf.cdocVersion == 1) { - auto rcpt_type_non_cert{ find_if(rcpts.cbegin(), rcpts.cend(), [](std::vector::const_reference rcpt) -> bool {return rcpt.type != RcptInfo::CERT;}) }; + auto rcpt_type_non_cert{ find_if(rcpts.cbegin(), rcpts.cend(), [](const libcdoc::RcptInfo &rcpt) -> bool {return rcpt.type != RcptInfo::CERT;}) }; if (rcpt_type_non_cert != rcpts.cend()) { LOG_ERROR("CDOC version 1 container can be used for encryption with certificate only."); return 1; diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 93e95350..78e3ce4a 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -418,7 +418,8 @@ BOOST_AUTO_TEST_SUITE_END() // CDoc2 password and label BOOST_AUTO_TEST_SUITE(PasswordUsageWithLabel) -BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithPasswordAndLabel, EncryptFixture, * utf::description("Encrypting a file with password and given label")) +BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithPasswordAndLabel, EncryptFixture, + * utf::description("Encrypting a file with password and given label")) { std::vector rcpts { {libcdoc::RcptInfo::PASSWORD, Label, {}, std::vector(Password.cbegin(), Password.cend())} @@ -437,7 +438,8 @@ BOOST_AUTO_TEST_SUITE_END() // CDoc2 password and label BOOST_AUTO_TEST_SUITE(PasswordUsageWithoutLabel) -BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithPasswordWithoutLabel, EncryptFixture, * utf::description("Encrypting a file with password and without label")) +BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithPasswordWithoutLabel, EncryptFixture, + * utf::description("Encrypting a file with password and without label")) { std::vector rcpts { {libcdoc::RcptInfo::PASSWORD, {}, {}, std::vector(Password.cbegin(), Password.cend())} @@ -455,7 +457,8 @@ BOOST_AUTO_TEST_SUITE_END() // CDoc2 AES key BOOST_AUTO_TEST_SUITE(AESKeyUsage) -BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithAESKey, EncryptFixture, * utf::description("Encrypting a file with symmetric AES key")) +BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithAESKey, EncryptFixture, + * utf::description("Encrypting a file with symmetric AES key")) { std::vector rcpts { {libcdoc::RcptInfo::SKEY, {}, {}, libcdoc::fromHex(AESKey)} @@ -473,7 +476,8 @@ BOOST_AUTO_TEST_SUITE_END() // CDoc2 EC public/private key BOOST_AUTO_TEST_SUITE(ECKeyUsage) -BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithECKey, EncryptFixture, * utf::description("Encrypting a file with EC key")) +BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithECKey, EncryptFixture, + * utf::description("Encrypting a file with EC key")) { std::vector rcpts { {libcdoc::RcptInfo::PKEY, {}, {}, fetchDataFile(ECPubKeyFile)} @@ -491,7 +495,8 @@ BOOST_AUTO_TEST_SUITE_END() // CDoc2 RSA public/private key BOOST_AUTO_TEST_SUITE(RSAKeyUsage) -BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithRSAKey, EncryptFixture, * utf::description("Encrypting a file with RSA key")) +BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithRSAKey, EncryptFixture, + * utf::description("Encrypting a file with RSA key")) { std::vector rcpts { {libcdoc::RcptInfo::PKEY, {}, {}, fetchDataFile(RSAPubKeyFile)} @@ -509,7 +514,8 @@ BOOST_AUTO_TEST_SUITE_END() // CDoc1 tests BOOST_AUTO_TEST_SUITE(CDoc1ECKeySingle) -BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithECKeyV1, EncryptFixture, * utf::description("Encrypting a file with EC key in CDoc1 format")) +BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithECKeyV1, EncryptFixture, + * utf::description("Encrypting a file with EC key in CDoc1 format")) { encryptV1({checkDataFile(sources[0])}, formTargetFile("ECKeyUsageV1.cdoc"), fetchDataFile(ECCertFile)); } @@ -522,7 +528,8 @@ BOOST_FIXTURE_TEST_CASE_WITH_DECOR(DecryptWithECKeyV1, DecryptFixture, BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(CDoc1ECKeyMulti) -BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithECKeyV1Multi, EncryptFixture, * utf::description("Encrypting multiple files with EC key in CDoc1 format")) +BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithECKeyV1Multi, EncryptFixture, + * utf::description("Encrypting multiple files with EC key in CDoc1 format")) { encryptV1({checkDataFile(sources[0]), checkDataFile(sources[1]), checkDataFile(sources[2])}, formTargetFile("ECKeyUsageV1Multi.cdoc"), fetchDataFile(ECCertFile)); } From 0da1fd80dd02745b866f1ee4bfd6a6159db9eb5b Mon Sep 17 00:00:00 2001 From: lauris71 Date: Wed, 14 Jan 2026 14:06:16 +0200 Subject: [PATCH 10/65] Update cdoc/cdoc-tool.cpp Co-authored-by: Raul Metsma --- cdoc/cdoc-tool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index 080533de..ff049a85 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -325,7 +325,7 @@ static int ParseAndEncrypt(int argc, char *argv[]) } if (!conf.gen_label) { // If labels must not be generated then is there any Recipient without provided label? - auto rcpt_wo_label{ find_if(rcpts.cbegin(), rcpts.cend(), [](std::vector::const_reference rcpt) -> bool {return rcpt.label.empty();}) }; + auto rcpt_wo_label{ find_if(rcpts.cbegin(), rcpts.cend(), [](const libcdoc::RcptInfo &rcpt) -> bool {return rcpt.label.empty();}) }; if (rcpt_wo_label != rcpts.cend()) { if (rcpts.size() > 1) { LOG_ERROR("Not all Recipients have label"); From 75d1101d6bc7638226dd1f28bc7f975717eebf13 Mon Sep 17 00:00:00 2001 From: lauris71 Date: Wed, 14 Jan 2026 14:06:27 +0200 Subject: [PATCH 11/65] Update cdoc/cdoc-tool.cpp Co-authored-by: Raul Metsma --- cdoc/cdoc-tool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index ff049a85..c00495fb 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -352,7 +352,7 @@ static int ParseAndEncrypt(int argc, char *argv[]) // CDOC1 is supported only for encryption with certificate. if (conf.cdocVersion == 1) { - auto rcpt_type_non_cert{ find_if(rcpts.cbegin(), rcpts.cend(), [](std::vector::const_reference rcpt) -> bool {return rcpt.type != RcptInfo::CERT;}) }; + auto rcpt_type_non_cert{ find_if(rcpts.cbegin(), rcpts.cend(), [](const libcdoc::RcptInfo &rcpt) -> bool {return rcpt.type != RcptInfo::CERT;}) }; if (rcpt_type_non_cert != rcpts.cend()) { LOG_ERROR("CDOC version 1 container can be used for encryption with certificate only."); return 1; From 53a689853287467f3824db045a4e031d72ee7fb0 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 20 May 2026 17:29:07 +0300 Subject: [PATCH 12/65] Some fixes from ai security audit --- cdoc/CDoc2Reader.cpp | 8 +++++++- cdoc/CDoc2Writer.cpp | 8 ++++---- cdoc/Crypto.cpp | 38 +++----------------------------------- cdoc/Crypto.h | 7 ++++--- cdoc/CryptoBackend.cpp | 4 ++-- cdoc/NetworkBackend.cpp | 5 ++++- cdoc/PKCS11Backend.cpp | 15 ++++++++++++--- cdoc/Utils.cpp | 2 +- cdoc/Utils.h | 2 +- cdoc/XmlReader.cpp | 5 +++-- cdoc/json/base.h | 6 +++--- cdoc/utils/memory.h | 23 +++++++++++++++++++++++ 12 files changed, 67 insertions(+), 56 deletions(-) diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index 7b3992ac..ed05bdc8 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -32,6 +32,7 @@ #include "header_generated.h" +// TODO: Port to new OpenSSL #define OPENSSL_SUPPRESS_DEPRECATED #include @@ -341,6 +342,7 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) if (auto err = libcdoc::Crypto::xor_data(fmk, lock.encrypted_fmk, kek); err != libcdoc::OK) { setLastError(t_("Failed to decrypt/derive fmk")); LOG_ERROR("{}", last_error); + libcdoc::cleanse(kek); return err; } std::vector hhk = libcdoc::Crypto::expand(fmk, libcdoc::CDoc2::HMAC); @@ -350,11 +352,15 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) LOG_TRACE_KEY("hhk: {}", hhk); LOG_TRACE_KEY("hmac: {}", priv->headerHMAC); - if(libcdoc::Crypto::sign_hmac(hhk, priv->header_data) != priv->headerHMAC) { + if(!libcdoc::constant_time_compare(libcdoc::Crypto::sign_hmac(hhk, priv->header_data), priv->headerHMAC)) { setLastError(t_("Wrong decryption key (user key)")); LOG_ERROR("{}", last_error); + libcdoc::cleanse(kek); + libcdoc::cleanse(hhk); return libcdoc::WRONG_KEY; } + libcdoc::cleanse(kek); + libcdoc::cleanse(hhk); setLastError({}); return libcdoc::OK; } diff --git a/cdoc/CDoc2Writer.cpp b/cdoc/CDoc2Writer.cpp index b2ebb455..17740f40 100644 --- a/cdoc/CDoc2Writer.cpp +++ b/cdoc/CDoc2Writer.cpp @@ -51,23 +51,23 @@ CDoc2Writer::writeHeader(const std::vector &recipients) if(auto rv = crypto->random(rnd, libcdoc::CDoc2::KEY_LEN); rv < 0) return rv; std::vector fmk = libcdoc::Crypto::extract(rnd, {libcdoc::CDoc2::SALT.cbegin(), libcdoc::CDoc2::SALT.cend()}); - std::fill(rnd.begin(), rnd.end(), 0); + libcdoc::cleanse(rnd); LOG_TRACE_KEY("fmk: {}", fmk); std::vector header; if(auto rv = buildHeader(header, recipients, fmk); rv < 0) { - std::fill(fmk.begin(), fmk.end(), 0); + libcdoc::cleanse(fmk); return rv; } auto hhk = libcdoc::Crypto::expand(fmk, libcdoc::CDoc2::HMAC); auto cek = libcdoc::Crypto::expand(fmk, libcdoc::CDoc2::CEK); - std::fill(fmk.begin(), fmk.end(), 0); + libcdoc::cleanse(fmk); LOG_TRACE_KEY("cek: {}", cek); LOG_TRACE_KEY("hhk: {}", hhk); std::vector headerHMAC = libcdoc::Crypto::sign_hmac(hhk, header); - std::fill(hhk.begin(), hhk.end(), 0); + libcdoc::cleanse(hhk); LOG_TRACE_KEY("hmac: {}", headerHMAC); uint32_t hs = uint32_t(header.size()); diff --git a/cdoc/Crypto.cpp b/cdoc/Crypto.cpp index 47238ec2..dbff3b51 100644 --- a/cdoc/Crypto.cpp +++ b/cdoc/Crypto.cpp @@ -138,14 +138,15 @@ Crypto::encrypt(EVP_PKEY *pub, int padding, const std::vector &data) auto ctx = make_unique_ptr(EVP_PKEY_CTX_new(pub, nullptr)); size_t size = 0; if (SSL_FAILED(EVP_PKEY_encrypt_init(ctx.get()), "EVP_PKEY_encrypt_init") || - SSL_FAILED(EVP_PKEY_CTX_set_rsa_padding(ctx.get(), padding), "EVP_PKEY_CTX_set_rsa_padding") || - SSL_FAILED(EVP_PKEY_encrypt(ctx.get(), nullptr, &size, data.data(), data.size()), "EVP_PKEY_encrypt")) + SSL_FAILED(EVP_PKEY_CTX_set_rsa_padding(ctx.get(), padding), "EVP_PKEY_CTX_set_rsa_padding")) return {}; if(padding == RSA_PKCS1_OAEP_PADDING) { if (SSL_FAILED(EVP_PKEY_CTX_set_rsa_oaep_md(ctx.get(), EVP_sha256()), "EVP_PKEY_CTX_set_rsa_oaep_md") || SSL_FAILED(EVP_PKEY_CTX_set_rsa_mgf1_md(ctx.get(), EVP_sha256()), "EVP_PKEY_CTX_set_rsa_mgf1_md")) return {}; } + if (SSL_FAILED(EVP_PKEY_encrypt(ctx.get(), nullptr, &size, data.data(), data.size()), "EVP_PKEY_encrypt")) + return {}; std::vector result(int(size), 0); if(SSL_FAILED(EVP_PKEY_encrypt(ctx.get(), result.data(), &size, data.data(), data.size()), "EVP_PKEY_encrypt")) @@ -153,39 +154,6 @@ Crypto::encrypt(EVP_PKEY *pub, int padding, const std::vector &data) return result; } -std::vector Crypto::decodeBase64(const uint8_t *data) -{ - std::vector result; - if (!data) - { - LOG_ERROR("decodeBase64: null pointer was provided as input data"); - return result; - } - result.resize(strlen((const char*)data)); - auto ctx = make_unique_ptr(EVP_ENCODE_CTX_new()); - if (!ctx) - { - LOG_SSL_ERROR("EVP_ENCODE_CTX_new"); - return {}; - } - - EVP_DecodeInit(ctx.get()); - int size1 = 0, size2 = 0; - if(EVP_DecodeUpdate(ctx.get(), result.data(), &size1, data, int(result.size())) == -1) - { - LOG_SSL_ERROR("EVP_DecodeUpdate"); - result.clear(); - return result; - } - - if(SSL_FAILED(EVP_DecodeFinal(ctx.get(), result.data(), &size2), "EVP_DecodeFinal")) - result.clear(); - else - result.resize(size_t(size1 + size2)); - - return result; -} - std::vector Crypto::deriveSharedSecret(EVP_PKEY *pkey, EVP_PKEY *peerPKey) { std::vector sharedSecret; diff --git a/cdoc/Crypto.h b/cdoc/Crypto.h index 0c2bcaa4..ffb52f69 100644 --- a/cdoc/Crypto.h +++ b/cdoc/Crypto.h @@ -58,8 +58,8 @@ class Crypto Key() {} ~Key() { - std::fill(key.begin(), key.end(), 0); - std::fill(iv.begin(), iv.end(), 0); + libcdoc::cleanse(key); + libcdoc::cleanse(iv); } Key(std::vector _key, std::vector _iv) : key(std::move(_key)), iv(std::move(_iv)) {} Key(size_t keySize, size_t ivSize) : key(keySize), iv(ivSize) {} @@ -71,7 +71,6 @@ class Crypto static std::vector concatKDF(const std::string &hashAlg, uint32_t keyDataLen, const std::vector &z, const std::vector &AlgorithmID, const std::vector &PartyUInfo, const std::vector &PartyVInfo); static std::vector encrypt(EVP_PKEY *pub, int padding, const std::vector &data); - static std::vector decodeBase64(const uint8_t *data); static std::vector deriveSharedSecret(EVP_PKEY *pkey, EVP_PKEY *peerPKey); static Key generateKey(const std::string &method); static uint32_t keySize(const std::string &algo); @@ -113,6 +112,7 @@ class Crypto struct EncryptionConsumer final : public DataConsumer { EncryptionConsumer(DataConsumer &dst, const std::string &method, const Crypto::Key &key); EncryptionConsumer(DataConsumer &dst, const EVP_CIPHER *cipher, const Crypto::Key &key); + ~EncryptionConsumer() { libcdoc::cleanse(buf); } CDOC_DISABLE_MOVE_COPY(EncryptionConsumer) result_t write(const uint8_t *src, size_t size) noexcept final; result_t writeAAD(const std::vector &data) noexcept; @@ -129,6 +129,7 @@ struct EncryptionConsumer final : public DataConsumer { struct DecryptionSource final : public DataSource { DecryptionSource(DataSource &src, const std::string &method, const std::vector &key, size_t ivLen = 0); DecryptionSource(DataSource &src, const EVP_CIPHER *cipher, const std::vector &key, size_t ivLen = 0); + ~DecryptionSource() { libcdoc::cleanse(tag); } CDOC_DISABLE_MOVE_COPY(DecryptionSource) result_t read(unsigned char* dst, size_t size) noexcept final; diff --git a/cdoc/CryptoBackend.cpp b/cdoc/CryptoBackend.cpp index 56bc5ccd..db6086c5 100644 --- a/cdoc/CryptoBackend.cpp +++ b/cdoc/CryptoBackend.cpp @@ -86,7 +86,7 @@ CryptoBackend::getKeyMaterial(std::vector& key_material, const std::vec LOG_DBG("Secret: {}", toHex(secret)); key_material = libcdoc::Crypto::pbkdf2_sha256(secret, pw_salt, kdf_iter); - std::fill(secret.begin(), secret.end(), 0); + libcdoc::cleanse(secret); if (key_material.empty()) return OPENSSL_ERROR; } else { int result = getSecret(key_material, idx); @@ -112,7 +112,7 @@ CryptoBackend::extractHKDF(std::vector& kek_pm, const std::vector(); std::vector shareval = fromBase64(share64); - shareval.resize(32); + if (shareval.size() != 32) { + error = FORMAT("Invalid share size: expected 32, got {}", shareval.size()); + return NETWORK_ERROR; + } LOG_DBG("Share: {}", toHex(shareval)); share = {std::move(shareval), std::move(recipient)}; return OK; diff --git a/cdoc/PKCS11Backend.cpp b/cdoc/PKCS11Backend.cpp index d69e7428..bc3b8c58 100644 --- a/cdoc/PKCS11Backend.cpp +++ b/cdoc/PKCS11Backend.cpp @@ -363,8 +363,8 @@ libcdoc::PKCS11Backend::getPublicKey(std::vector& val, int slot, const return CRYPTO_ERROR; } std::vector w = d->attribute(d->session, handle, CKA_EC_POINT); - if (w.empty()) { - LOG_DBG("PKCS11: getValue CKA_EC_POINT error"); + if (w.size() < 2) { + LOG_DBG("PKCS11: getValue CKA_EC_POINT too short"); return CRYPTO_ERROR; } const uint8_t *p = v.data(); @@ -374,7 +374,16 @@ libcdoc::PKCS11Backend::getPublicKey(std::vector& val, int slot, const return CRYPTO_ERROR; } EC_POINT *pub_key_point = EC_POINT_new(group); - int result = EC_POINT_oct2point(group, pub_key_point, w.data() + 2, w.size() - 2, NULL); + if (!pub_key_point) { + EC_GROUP_free(group); + return CRYPTO_ERROR; + } + if (EC_POINT_oct2point(group, pub_key_point, w.data() + 2, w.size() - 2, NULL) != 1) { + LOG_DBG("PKCS11: EC_POINT_oct2point error"); + EC_POINT_free(pub_key_point); + EC_GROUP_free(group); + return CRYPTO_ERROR; + } // Associate the Point with an EC_KEY: Finally, set up an EC_KEY structure and assign the point as the public key. EC_KEY *key = EC_KEY_new(); EC_KEY_set_group(key, group); diff --git a/cdoc/Utils.cpp b/cdoc/Utils.cpp index 07853336..26ad0d86 100644 --- a/cdoc/Utils.cpp +++ b/cdoc/Utils.cpp @@ -39,7 +39,7 @@ toBase64(const uint8_t *data, size_t len) } std::vector -fromBase64(const std::string& data) +fromBase64(std::string_view data) { std::string str = jwt::base::details::decode(data, jwt::alphabet::base64::rdata(), "="); return std::vector(str.cbegin(), str.cend()); diff --git a/cdoc/Utils.h b/cdoc/Utils.h index dad25f3a..f5ef7e2a 100644 --- a/cdoc/Utils.h +++ b/cdoc/Utils.h @@ -54,7 +54,7 @@ static std::string toBase64(const std::vector &data) { return toBase64(data.data(), data.size()); } -std::vector fromBase64(const std::string& data); +std::vector fromBase64(std::string_view data); template static std::string toHex(const F &data) diff --git a/cdoc/XmlReader.cpp b/cdoc/XmlReader.cpp index d9428bcc..db3a1339 100644 --- a/cdoc/XmlReader.cpp +++ b/cdoc/XmlReader.cpp @@ -18,7 +18,8 @@ #include "XmlReader.h" -#include "Crypto.h" +#include "Io.h" +#include "Utils.h" #include @@ -80,7 +81,7 @@ bool XMLReader::read() std::vector XMLReader::readBase64() { xmlTextReaderRead(d); - return libcdoc::Crypto::decodeBase64(xmlTextReaderConstValue(d)); + return libcdoc::fromBase64(reinterpret_cast(xmlTextReaderConstValue(d))); } std::string XMLReader::readText() diff --git a/cdoc/json/base.h b/cdoc/json/base.h index 7258b2e7..3682abac 100644 --- a/cdoc/json/base.h +++ b/cdoc/json/base.h @@ -163,7 +163,7 @@ namespace jwt { } }; - inline padding count_padding(const std::string& base, const std::vector& fills) { + inline padding count_padding(std::string_view base, const std::vector& fills) { for (const auto& fill : fills) { if (base.size() < fill.size()) continue; // Does the end of the input exactly match the fill pattern? @@ -225,7 +225,7 @@ namespace jwt { return res; } - inline std::string decode(const std::string& base, const std::array& rdata, + inline std::string decode(std::string_view base, const std::array& rdata, const std::vector& fill) { const auto pad = count_padding(base, fill); if (pad.count > 2) throw std::runtime_error("Invalid input: too much fill"); @@ -271,7 +271,7 @@ namespace jwt { return res; } - inline std::string decode(const std::string& base, const std::array& rdata, + inline std::string decode(std::string_view base, const std::array& rdata, const std::string& fill) { return decode(base, rdata, std::vector{fill}); } diff --git a/cdoc/utils/memory.h b/cdoc/utils/memory.h index 708b313a..edb4a3c8 100644 --- a/cdoc/utils/memory.h +++ b/cdoc/utils/memory.h @@ -19,11 +19,34 @@ #pragma once #include +#include #include #include +#include + namespace libcdoc { +template +void cleanse(std::vector& v) noexcept +{ + if (!v.empty()) { + OPENSSL_cleanse(v.data(), v.size() * sizeof(T)); + } +} + +template +void cleanse(std::array& a) noexcept +{ + OPENSSL_cleanse(a.data(), a.size() * sizeof(T)); +} + +inline bool constant_time_compare(const std::vector& a, const std::vector& b) noexcept +{ + if (a.size() != b.size()) return false; + return CRYPTO_memcmp(a.data(), b.data(), a.size()) == 0; +} + template struct free_deleter { From 47715fe30c234c4d0534db29872a98a7448e6a10 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 22 May 2026 11:31:42 +0300 Subject: [PATCH 13/65] Fixed cdoc-tool index usage --- cdoc/cdoc-tool.cpp | 72 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index 52fe681f..c40d588a 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -42,20 +42,28 @@ static std::map str2level = { enum { RESULT_OK = 0, - RESULT_ERROR, - RESULT_USAGE + RESULT_ERROR = -1, + RESULT_USAGE = -2 }; -static void print_usage(ostream& ofs) +static void +print_version(ostream& ofs) { ofs << "cdoc-tool version: " << VERSION_STR << endl; ofs << "libcdoc version: " << libcdoc::getVersion() << endl; + ofs.flush(); +} + +static void +print_usage(ostream& ofs) +{ + ofs << "Usage:" << endl; ofs << "cdoc-tool encrypt --rcpt RECIPIENT [--rcpt...] [-v1] [--genlabel] --out OUTPUTFILE FILE [FILE...]" << endl; ofs << " Encrypt files for one or more recipients" << endl; ofs << " RECIPIENT has to be one of the following:" << endl; ofs << " [label]:cert:CERTIFICATE_FILE - public key from certificate file (DER format)" << endl; - ofs << " [label]:pkey:SECRET_KEY_HEX - hex encoded public key (DER format; rsa, secp384r1 or secp256r1 key)." << endl; - ofs << " [label]:pfkey:PUB_KEY_FILE - public key from file (DER format; rsa, secp384r1 or secp256r1 key)." << endl; + ofs << " [label]:pkey:SECRET_KEY_HEX - hex encoded public key (DER format; rsa, secp384r1, secp256r1 or secp521r1 key)." << endl; + ofs << " [label]:pfkey:PUB_KEY_FILE - public key from file (DER format; rsa, secp384r1, secp256r1 or secp521r1 key)." << endl; ofs << " [label]:skey:SECRET_KEY_HEX - AES key, hex encoded" << endl; ofs << " [label]:pw:PASSWORD - AES key derived from password with PWBKDF" << endl; ofs << " [label]:p11sk:SLOT:[PIN]:[PKCS11 ID]:[PKCS11 LABEL] - use AES key from PKCS11 module" << endl; @@ -68,7 +76,7 @@ static void print_usage(ostream& ofs) ofs << " Decrypt CDoc container using lock specified by label or number" << endl; ofs << " Supported arguments" << endl; ofs << " --label LABEL - lock label" << endl; - ofs << " --label_idx INDEX - lock number (1-based)" << endl; + ofs << " --lock-idx INDEX - lock number (1-based)" << endl; ofs << " --pkey PRIVATE_KEY_HEX - hex encoded private key (DER format)" << endl; ofs << " --pfkey PRIVATE_KEY_HEX - private key from file (DER format)" << endl; ofs << " --slot SLOT - PKCS11 slot number" << endl; @@ -366,7 +374,13 @@ static int ParseAndEncrypt(int argc, char *argv[]) } CDocCipher cipher; - return cipher.Encrypt(conf, rcpts); + if (int ret = cipher.Encrypt(conf, rcpts); ret != 0) { + cerr << "Encryption failed"; + return ret; + } else { + cout << "Container " << conf.out << " encrypted successfully" << endl; + } + return 0; } struct LockData { @@ -378,7 +392,11 @@ struct LockData { vector secret; int validate(ToolConf& conf) { - if (lock_label.empty() && (lock_idx == -1) && (slot < 0)) { + if (lock_idx == 0) { + LOG_ERROR("Lock indices start from 1"); + return RESULT_USAGE; + } + if (lock_label.empty() && (lock_idx < 0) && (slot < 0)) { LOG_ERROR("No label nor index was provided"); return RESULT_USAGE; } @@ -394,13 +412,13 @@ static int parse_key_data(LockData& ldata, const int& arg_idx, int argc, char *argv[]) { string_view arg(argv[arg_idx]); - if ((arg == "--label" || arg == "--label_idx") && (arg_idx + 1) < argc) { + if ((arg == "--label" || arg == "--label_idx" || arg == "--lock-idx") && (arg_idx + 1) < argc) { // Make sure the label or label index is provided only once. - if (!ldata.lock_label.empty() || ldata.lock_idx != -1) { + if (!ldata.lock_label.empty() || ldata.lock_idx > 0) { LOG_ERROR("The label or label's index was already provided"); return RESULT_USAGE; } - if (arg == "--label_idx") { + if (arg == "--label_idx" || arg == "--lock-idx") { size_t last_char_idx; string str(argv[arg_idx + 1]); ldata.lock_idx = std::stol(str, &last_char_idx); @@ -408,6 +426,10 @@ parse_key_data(LockData& ldata, const int& arg_idx, int argc, char *argv[]) LOG_ERROR("Label index is not a number"); return RESULT_USAGE; } + if (ldata.lock_idx < 1) { + LOG_ERROR("Lock indices start from 1"); + return RESULT_USAGE; + } } else { ldata.lock_label = argv[arg_idx + 1]; } @@ -546,7 +568,13 @@ static int ParseAndDecrypt(int argc, char *argv[]) CDocCipher cipher; RcptInfo rcpt {.type=RcptInfo::LOCK, .label=ldata.lock_label, .secret=ldata.secret, .p11={ldata.slot, ldata.key_id, ldata.key_label}, .lock_idx=ldata.lock_idx - 1}; - return cipher.Decrypt(conf, rcpt); + if (int ret = cipher.Decrypt(conf, rcpt); ret != 0) { + cerr << "Decryption failed" << endl; + return ret; + } else { + cout << "Container " << conf.input_files[0] << " decrypted successfully" << endl; + } + return 0; } static int ParseAndReEncrypt(int argc, char *argv[]) @@ -628,12 +656,20 @@ static int ParseAndReEncrypt(int argc, char *argv[]) } } + if ((ldata.lock_idx < 0) && (ldata.lock_label.empty())) { + LOG_ERROR("Lock index or label must be provided"); + return RESULT_USAGE; + } + CDocCipher cipher; - RcptInfo rcpt {.type=RcptInfo::LOCK, .label=ldata.lock_label, .secret=ldata.secret, .p11={ldata.slot, ldata.key_id, ldata.key_label}, .lock_idx=ldata.lock_idx}; - if (ldata.lock_idx != -1) { - return cipher.ReEncrypt(conf, rcpt, rcpts); + RcptInfo rcpt {.type=RcptInfo::LOCK, .label=ldata.lock_label, .secret=ldata.secret, .p11={ldata.slot, ldata.key_id, ldata.key_label}, .lock_idx=ldata.lock_idx - 1}; + if (int ret = cipher.ReEncrypt(conf, rcpt, rcpts); ret != 0) { + cerr << "Re-encryption failed" << std::endl; + return ret; + } else { + cout << "Successfully re-encrypted container " << conf.input_files[0] << " to " << conf.out << std::endl; } - return true; + return 0; } // @@ -671,6 +707,8 @@ static int ParseAndGetLocks(int argc, char *argv[]) int main(int argc, char *argv[]) { + print_version(cout); + if (argc < 2) { print_usage(cerr); return 1; @@ -694,7 +732,7 @@ int main(int argc, char *argv[]) cerr << "Invalid command: " << command << endl; } - if (retVal == 2) { + if (retVal == RESULT_USAGE) { // We print usage information only in case the parse-function returned 2. Value 1 indicates other error. print_usage(cout); } From fef2389ca52bdaf43dfdb994a0aa48e2ea431c4f Mon Sep 17 00:00:00 2001 From: lauris71 Date: Mon, 25 May 2026 16:38:37 +0300 Subject: [PATCH 14/65] Update cdoc/cdoc-tool.cpp Co-authored-by: Raul Metsma --- cdoc/cdoc-tool.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index c40d588a..c060d6e6 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -51,7 +51,6 @@ print_version(ostream& ofs) { ofs << "cdoc-tool version: " << VERSION_STR << endl; ofs << "libcdoc version: " << libcdoc::getVersion() << endl; - ofs.flush(); } static void From 884854288da0e9b6f9aa7633eadcc0d3f4397a81 Mon Sep 17 00:00:00 2001 From: lauris71 Date: Mon, 25 May 2026 16:38:44 +0300 Subject: [PATCH 15/65] Update cdoc/cdoc-tool.cpp Co-authored-by: Raul Metsma --- cdoc/cdoc-tool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index c060d6e6..21b9e33d 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -668,7 +668,7 @@ static int ParseAndReEncrypt(int argc, char *argv[]) } else { cout << "Successfully re-encrypted container " << conf.input_files[0] << " to " << conf.out << std::endl; } - return 0; + return RESULT_OK; } // From bf5ca628a780e8ca2a9e16b182de6239593f1cf3 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Tue, 26 May 2026 16:11:14 +0300 Subject: [PATCH 16/65] Some more AI issue fixes --- cdoc/CDoc2Reader.cpp | 6 +++--- cdoc/CDoc2Writer.cpp | 2 +- cdoc/Crypto.cpp | 8 +++---- cdoc/CryptoBackend.cpp | 6 +++--- cdoc/Io.cpp | 2 +- cdoc/KeyShares.cpp | 4 ++-- cdoc/NetworkBackend.cpp | 46 +++++++++++++++++++++++++++++++++++------ cdoc/NetworkBackend.h | 9 ++++++++ cdoc/Tar.cpp | 25 ++++++++++++++-------- doc/usage.md | 2 +- 10 files changed, 80 insertions(+), 30 deletions(-) diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index ed05bdc8..495708b0 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -202,8 +202,8 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) key_material = lock.getBytes(Lock::Params::KEY_MATERIAL); } - LOG_DBG("Public key: {}", toHex(lock.getBytes(Lock::Params::RCPT_KEY))); - LOG_DBG("Key material: {}", toHex(key_material)); + LOG_TRACE_KEY("Public key: {}", lock.getBytes(Lock::Params::RCPT_KEY)); + LOG_TRACE_KEY("Key material: {}", key_material); if (lock.isRSA()) { int result = crypto->decryptRSA(kek, key_material, true, lock_idx); @@ -615,7 +615,7 @@ CDoc2Reader::Private::buildLock(Lock& lock, const cdoc20::header::RecipientRecor std::string urls = join(strs, ";"); LOG_DBG("Keyshare urls: {}", urls); std::vector salt = toUint8Vector(capsule->salt()); - LOG_DBG("Keyshare salt: {}", toHex(salt)); + LOG_TRACE_KEY("Keyshare salt: {}", salt); std::string recipient_id = capsule->recipient_id()->str(); LOG_DBG("Keyshare recipient id: {}", recipient_id); lock.type = Lock::SHARE_SERVER; diff --git a/cdoc/CDoc2Writer.cpp b/cdoc/CDoc2Writer.cpp index 17740f40..c516c11e 100644 --- a/cdoc/CDoc2Writer.cpp +++ b/cdoc/CDoc2Writer.cpp @@ -378,7 +378,7 @@ CDoc2Writer::buildHeader(std::vector& header, const std::vector> transaction_ids(N_SHARES); for (int i = 0; i < N_SHARES; i++) { std::string send_url = urls[i]; - LOG_DBG("Sending share: {} {} {}", i, send_url, libcdoc::toHex(kek_shares[i])); + LOG_TRACE("Sending share: {} {} {}", i, send_url, libcdoc::toHex(kek_shares[i])); int result = network->sendShare(transaction_ids[i], send_url, RecipientInfo_i, kek_shares[i]); if (result < 0) FAIL(network->getLastErrorStr(result), result); diff --git a/cdoc/Crypto.cpp b/cdoc/Crypto.cpp index dbff3b51..9eab0377 100644 --- a/cdoc/Crypto.cpp +++ b/cdoc/Crypto.cpp @@ -120,10 +120,10 @@ std::vector Crypto::concatKDF(const std::string &hashAlg, uint32_t keyD std::vector Crypto::concatKDF(const std::string &hashAlg, uint32_t keyDataLen, const std::vector &z, const std::vector &AlgorithmID, const std::vector &PartyUInfo, const std::vector &PartyVInfo) { - LOG_DBG("Ksr {}", toHex(z)); - LOG_DBG("AlgorithmID {}", toHex(AlgorithmID)); - LOG_DBG("PartyUInfo {}", toHex(PartyUInfo)); - LOG_DBG("PartyVInfo {}", toHex(PartyVInfo)); + LOG_TRACE_KEY("Ksr {}", z); + LOG_TRACE_KEY("AlgorithmID {}", AlgorithmID); + LOG_TRACE_KEY("PartyUInfo {}", PartyUInfo); + LOG_TRACE_KEY("PartyVInfo {}", PartyVInfo); std::vector otherInfo; otherInfo.insert(otherInfo.cend(), AlgorithmID.cbegin(), AlgorithmID.cend()); diff --git a/cdoc/CryptoBackend.cpp b/cdoc/CryptoBackend.cpp index db6086c5..e8c2cb68 100644 --- a/cdoc/CryptoBackend.cpp +++ b/cdoc/CryptoBackend.cpp @@ -83,7 +83,7 @@ CryptoBackend::getKeyMaterial(std::vector& key_material, const std::vec int result = getSecret(secret, idx); if (result) return result; - LOG_DBG("Secret: {}", toHex(secret)); + LOG_TRACE_KEY("Secret: {}", secret); key_material = libcdoc::Crypto::pbkdf2_sha256(secret, pw_salt, kdf_iter); libcdoc::cleanse(secret); @@ -91,13 +91,13 @@ CryptoBackend::getKeyMaterial(std::vector& key_material, const std::vec } else { int result = getSecret(key_material, idx); if (result) return result; - LOG_DBG("Secret: {}", toHex(key_material)); + LOG_TRACE_KEY("Secret: {}", key_material); if (key_material.size() != 32) { return INVALID_PARAMS; } } - LOG_DBG("Key material: {}", toHex(key_material)); + LOG_TRACE_KEY("Key material: {}", key_material); return OK; } diff --git a/cdoc/Io.cpp b/cdoc/Io.cpp index 1d845b1d..bc1ac845 100644 --- a/cdoc/Io.cpp +++ b/cdoc/Io.cpp @@ -166,7 +166,7 @@ FileListSource::next(std::string& name, int64_t& size) name = _files[_current]; std::error_code ec; size = fs::file_size(path, ec); - if (!ec) return IO_ERROR; + if (ec) return IO_ERROR; return OK; } diff --git a/cdoc/KeyShares.cpp b/cdoc/KeyShares.cpp index 06d48875..7e77aa93 100644 --- a/cdoc/KeyShares.cpp +++ b/cdoc/KeyShares.cpp @@ -204,7 +204,7 @@ Signer::generateTickets(std::vector& dst, std::vector& s result_t SIDSigner::signDigest(std::vector& dst, const std::vector& digest) { - LOG_DBG("SID signing: {}", toHex(digest)); + LOG_TRACE_KEY("SID signing: {}", digest); result_t result = network->signSID(dst, cert, url, rp_uuid, rp_name, rcpt_id, digest, libcdoc::CryptoBackend::SHA_256); if (result != OK) { @@ -222,7 +222,7 @@ result_t libcdoc::MIDSigner::signDigest(std::vector& dst, const std::vector& digest) { - LOG_DBG("MID signing: {}", toHex(digest)); + LOG_TRACE_KEY("MID signing: {}", digest); result_t result = network->signMID(dst, cert, url, rp_uuid, rp_name, phone, rcpt_id, digest, libcdoc::CryptoBackend::SHA_256); if (result != OK) { diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 7e0c3ec1..f9036cc4 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -184,10 +184,15 @@ setPeerCertificates(httplib::SSLClient& cli, libcdoc::NetworkBackend *network, c } cli.enable_server_certificate_verification(true); cli.enable_server_hostname_verification(true); - } else { - // TODO: Allow only if global parameter is set + } + else { +#ifdef NDEBUG + error = "No peer TLS certificates configured"; + return libcdoc::CONFIGURATION_ERROR; +#else cli.enable_server_certificate_verification(false); cli.enable_server_hostname_verification(false); +#endif } return libcdoc::OK; } @@ -214,6 +219,20 @@ setProxy(httplib::SSLClient& cli, libcdoc::NetworkBackend *network) } } +// +// Set SSL timeouts +// +static libcdoc::result_t +applySSLTimeout(httplib::SSLClient& cli, libcdoc::NetworkBackend *network) +{ + libcdoc::result_t timeout = network->getSSLTimeout(); + if (timeout < 0) return libcdoc::CONFIGURATION_ERROR; + cli.set_connection_timeout(timeout); + cli.set_read_timeout(timeout); + cli.set_write_timeout(timeout); + return libcdoc::OK; +} + // // Post request and fetch response // @@ -278,6 +297,7 @@ libcdoc::NetworkBackend::sendKey (CapsuleInfo& dst, const std::string& url, cons if (result != libcdoc::OK) return result; httplib::SSLClient cli(host, port); + if (result = applySSLTimeout(cli, this); result != OK) return result; result = setPeerCertificates(cli, this, buildURL(host, port)); if (result != OK) return result; if (result = setProxy(cli, this); result != OK) return result; @@ -298,9 +318,13 @@ libcdoc::NetworkBackend::sendKey (CapsuleInfo& dst, const std::string& url, cons error = FORMAT("No Location header in response"); return NETWORK_ERROR; } + constexpr std::string_view kCapsulePrefix = "/key-capsules/"; + if (location.compare(0, kCapsulePrefix.size(), kCapsulePrefix) != 0) { + error = FORMAT("Unexpected Location header value"); + return NETWORK_ERROR; + } error = {}; - /* Remove /key-capsules/ */ - location.erase(0, 14); + location.erase(0, kCapsulePrefix.size()); dst.transaction_id = std::move(location); std::string expiry_str = rsp.get_header_value("x-expiry-time"); @@ -336,6 +360,7 @@ libcdoc::NetworkBackend::sendShare(std::vector& dst, const std::string& if (result != libcdoc::OK) return result; httplib::SSLClient cli(host, port); + if (result = applySSLTimeout(cli, this); result != OK) return result; result = setPeerCertificates(cli, this, buildURL(host, port)); if (result != OK) return result; if (result = setProxy(cli, this); result != OK) return result; @@ -351,10 +376,14 @@ libcdoc::NetworkBackend::sendShare(std::vector& dst, const std::string& error = FORMAT("No Location header in response"); return NETWORK_ERROR; } + constexpr std::string_view kSharePrefix = "/key-shares/"; + if (location.compare(0, kSharePrefix.size(), kSharePrefix) != 0) { + error = FORMAT("Unexpected Location header value"); + return NETWORK_ERROR; + } error = {}; - /* Remove /key-shares/ */ - dst.assign(location.cbegin() + 12, location.cend()); + dst.assign(location.cbegin() + kSharePrefix.size(), location.cend()); LOG_DBG("Share: {}", std::string((const char *) dst.data(), dst.size())); return OK; @@ -376,6 +405,7 @@ libcdoc::NetworkBackend::fetchKey (std::vector& dst, const std::string& if (!cert.empty() && (!d->x509 || !d->pkey)) return CRYPTO_ERROR; httplib::SSLClient cli(host, port, d->x509.handle(), d->pkey); + if (result = applySSLTimeout(cli, this); result != OK) return result; result = setPeerCertificates(cli, this, buildURL(host, port)); if (result != OK) return result; if (result = setProxy(cli, this); result != OK) return result; @@ -411,6 +441,7 @@ libcdoc::NetworkBackend::fetchNonce(std::vector& dst, const std::string LOG_DBG("Starting client: {} {}", host, port); httplib::SSLClient cli(host, port); + if (result = applySSLTimeout(cli, this); result != OK) return result; result = setPeerCertificates(cli, this, buildURL(host, port)); if (result != OK) return result; if (result = setProxy(cli, this); result != OK) return result; @@ -446,6 +477,7 @@ libcdoc::NetworkBackend::fetchShare(ShareInfo& share, const std::string& url, co LOG_DBG("Starting client: {} {}", host, port); httplib::SSLClient cli(host, port); + if (result = applySSLTimeout(cli, this); result != OK) return result; result = setPeerCertificates(cli, this, buildURL(host, port)); if (result != OK) return result; @@ -702,6 +734,7 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector LOG_DBG("Starting client: {} {}", host, port); httplib::SSLClient cli(host, port); + if (result = applySSLTimeout(cli, this); result != OK) return result; result = setPeerCertificates(cli, this, buildURL(host, port)); if (result != OK) return result; if (result = setProxy(cli, this); result != OK) return result; @@ -824,6 +857,7 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector LOG_DBG("Starting client: {} {}", host, port); httplib::SSLClient cli(host, port); + if (result = applySSLTimeout(cli, this); result != OK) return result; result = setPeerCertificates(cli, this, buildURL(host, port)); if (result != OK) return result; if (result = setProxy(cli, this); result != OK) return result; diff --git a/cdoc/NetworkBackend.h b/cdoc/NetworkBackend.h index 3ec43618..cdf8f442 100644 --- a/cdoc/NetworkBackend.h +++ b/cdoc/NetworkBackend.h @@ -241,6 +241,15 @@ struct CDOC_EXPORT NetworkBackend { return NOT_IMPLEMENTED; } + /** + * @brief Get SSL connection/read/write timeout + * + * @return timeout in seconds (default 30) + */ + virtual result_t getSSLTimeout() const noexcept { + return 30; + } + #ifdef HAS_KEYSHARES /** * @brief show MID/SID verification code diff --git a/cdoc/Tar.cpp b/cdoc/Tar.cpp index b039e910..6a29db7c 100644 --- a/cdoc/Tar.cpp +++ b/cdoc/Tar.cpp @@ -29,14 +29,14 @@ using namespace libcdoc; constexpr unsigned int BLOCKSIZE = 512; template -[[nodiscard]] static constexpr auto svtoi(std::string_view data) noexcept +[[nodiscard]] static constexpr bool svtoi(std::string_view data, T& result) noexcept { - T result {}; if (data.empty()) - return result; - auto p = &*data.begin(); - std::from_chars(p, p + std::ranges::distance(data), result); - return result; + return false; + const auto *p = data.data(); + const auto *end = p + data.size(); + auto [ptr, ec] = std::from_chars(p, end, result); + return ec == std::errc{} && ptr == end; } template @@ -329,15 +329,22 @@ libcdoc::TarSource::readPaxHeader(const Header& hdr, std::string& name, int64_t& auto keyWord = range_to_sv(std::next(sp), eq); auto headerValue = range_to_sv(std::next(eq), line.end()); - if (std::ranges::distance(line) + 1 != svtoi(lenStr)) { + int parsedLen; + if (!svtoi(lenStr, parsedLen) || std::ranges::distance(line) + 1 != parsedLen) { _error = DATA_FORMAT_ERROR; return _error; } LOG_DBG("PAX {} : {}", keyWord, headerValue); if (keyWord == "path") name = headerValue; - if (keyWord == "size") - size = svtoi(headerValue); + if (keyWord == "size") { + int64_t parsedSize; + if (!svtoi(headerValue, parsedSize)) { + _error = DATA_FORMAT_ERROR; + return _error; + } + size = parsedSize; + } } return OK; } diff --git a/doc/usage.md b/doc/usage.md index b778ff51..27eb6d74 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -138,7 +138,7 @@ Returns the client's TLS certificate for authentication with the key-server. int getPeerTLSCertificates(std::vector> &dst) ``` -Returns the list of acceptable peer certificates for the key-server. +Returns the list of acceptable peer certificates for the key-server. Returning empty list disables TLS peer certificate check in debug builds but results in CONFIGURATION_ERROR in release builds. #### `signTLS` From 0a6b12b9d6f505a5c872b3bfead1b1ee54c5731d Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 27 May 2026 09:45:18 +0300 Subject: [PATCH 17/65] Hardcode SSL timeout --- cdoc/NetworkBackend.cpp | 22 +++++++++++----------- cdoc/NetworkBackend.h | 9 --------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index f9036cc4..9c0c14f6 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -40,6 +40,8 @@ #include #endif +#define CDOC_SSL_TIMEOUT 30 + using namespace std::literals::chrono_literals; using EC_KEY_sign = int (*)(int type, const unsigned char *dgst, int dlen, unsigned char *sig, unsigned int *siglen, const BIGNUM *kinv, const BIGNUM *r, EC_KEY *eckey); @@ -225,11 +227,9 @@ setProxy(httplib::SSLClient& cli, libcdoc::NetworkBackend *network) static libcdoc::result_t applySSLTimeout(httplib::SSLClient& cli, libcdoc::NetworkBackend *network) { - libcdoc::result_t timeout = network->getSSLTimeout(); - if (timeout < 0) return libcdoc::CONFIGURATION_ERROR; - cli.set_connection_timeout(timeout); - cli.set_read_timeout(timeout); - cli.set_write_timeout(timeout); + cli.set_connection_timeout(CDOC_SSL_TIMEOUT); + cli.set_read_timeout(CDOC_SSL_TIMEOUT); + cli.set_write_timeout(CDOC_SSL_TIMEOUT); return libcdoc::OK; } @@ -318,13 +318,13 @@ libcdoc::NetworkBackend::sendKey (CapsuleInfo& dst, const std::string& url, cons error = FORMAT("No Location header in response"); return NETWORK_ERROR; } - constexpr std::string_view kCapsulePrefix = "/key-capsules/"; - if (location.compare(0, kCapsulePrefix.size(), kCapsulePrefix) != 0) { + constexpr std::string_view prefix = "/key-capsules/"; + if (location.compare(0, prefix.size(), prefix) != 0) { error = FORMAT("Unexpected Location header value"); return NETWORK_ERROR; } error = {}; - location.erase(0, kCapsulePrefix.size()); + location.erase(0, prefix.size()); dst.transaction_id = std::move(location); std::string expiry_str = rsp.get_header_value("x-expiry-time"); @@ -376,14 +376,14 @@ libcdoc::NetworkBackend::sendShare(std::vector& dst, const std::string& error = FORMAT("No Location header in response"); return NETWORK_ERROR; } - constexpr std::string_view kSharePrefix = "/key-shares/"; - if (location.compare(0, kSharePrefix.size(), kSharePrefix) != 0) { + constexpr std::string_view prefix = "/key-shares/"; + if (location.compare(0, prefix.size(), prefix) != 0) { error = FORMAT("Unexpected Location header value"); return NETWORK_ERROR; } error = {}; - dst.assign(location.cbegin() + kSharePrefix.size(), location.cend()); + dst.assign(location.cbegin() + prefix.size(), location.cend()); LOG_DBG("Share: {}", std::string((const char *) dst.data(), dst.size())); return OK; diff --git a/cdoc/NetworkBackend.h b/cdoc/NetworkBackend.h index cdf8f442..3ec43618 100644 --- a/cdoc/NetworkBackend.h +++ b/cdoc/NetworkBackend.h @@ -241,15 +241,6 @@ struct CDOC_EXPORT NetworkBackend { return NOT_IMPLEMENTED; } - /** - * @brief Get SSL connection/read/write timeout - * - * @return timeout in seconds (default 30) - */ - virtual result_t getSSLTimeout() const noexcept { - return 30; - } - #ifdef HAS_KEYSHARES /** * @brief show MID/SID verification code From a4dafb9fac218ac045c8911446ddf1d79e0fde36 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 27 May 2026 11:34:52 +0300 Subject: [PATCH 18/65] Fixed xstream uint overflow and tool logging --- cdoc/ZStream.h | 100 ++++++++++++++++++++++++++------------------- cdoc/cdoc-tool.cpp | 16 ++++---- 2 files changed, 66 insertions(+), 50 deletions(-) diff --git a/cdoc/ZStream.h b/cdoc/ZStream.h index 28b8ddb3..eecfc5b5 100644 --- a/cdoc/ZStream.h +++ b/cdoc/ZStream.h @@ -24,6 +24,7 @@ #include #include +#include namespace libcdoc { @@ -45,24 +46,29 @@ struct ZConsumer : public DataConsumer { libcdoc::result_t write(const uint8_t *src, size_t size) noexcept final { if (_fail) return OUTPUT_ERROR; - _s.next_in = (z_const Bytef *) src; - _s.avail_in = uInt(size); + size_t total_written = 0; std::array out{}; - while(true) { - _s.next_out = (Bytef *)out.data(); - _s.avail_out = out.size(); - int res = deflate(&_s, flush); - if(res == Z_STREAM_ERROR) - return OUTPUT_ERROR; - auto o_size = out.size() - _s.avail_out; - if(o_size > 0) { - int64_t result = _dst->write(out.data(), o_size); - if (result != o_size) return result; + do { + size_t chunk = std::min(size - total_written, std::numeric_limits::max()); + _s.next_in = (z_const Bytef *) (src ? src + total_written : nullptr); + _s.avail_in = uInt(chunk); + while(true) { + _s.next_out = (Bytef *)out.data(); + _s.avail_out = out.size(); + int res = deflate(&_s, flush); + if(res == Z_STREAM_ERROR) + return OUTPUT_ERROR; + auto o_size = out.size() - _s.avail_out; + if(o_size > 0) { + int64_t result = _dst->write(out.data(), o_size); + if (result != o_size) return result; + } + if(res == Z_STREAM_END) break; + if(flush == Z_FINISH) continue; + if(_s.avail_in == 0) break; } - if(res == Z_STREAM_END) break; - if(flush == Z_FINISH) continue; - if(_s.avail_in == 0) break; - } + total_written += chunk; + } while (total_written < size); return size; } @@ -72,8 +78,8 @@ struct ZConsumer : public DataConsumer { libcdoc::result_t close() noexcept final { flush = Z_FINISH; - write (nullptr, 0); - deflateEnd(&_s); + libcdoc::result_t rv = write(nullptr, 0); + if (rv < 0) return rv; return _owned ? _dst->close() : OK; } }; @@ -99,34 +105,42 @@ struct ZSource : public DataSource { libcdoc::result_t read(uint8_t *dst, size_t size) noexcept final try { if (_error) return _error; - _s.next_out = (Bytef *) dst; - _s.avail_out = uInt (size); + size_t total_produced = 0; std::array in{}; - int res = Z_OK; - while((_s.avail_out > 0) && (res == Z_OK)) { - int64_t n_read = _src->read(in.data(), in.size()); - if (n_read > 0) { - buf.insert(buf.end(), in.begin(), in.begin() + n_read); - } else if (n_read != 0) { - _error = n_read; - return _error; - } - _s.next_in = (z_const Bytef *) buf.data(); - _s.avail_in = uInt(buf.size()); - res = inflate(&_s, flush); - switch(res) { - case Z_OK: - buf.erase(buf.begin(), buf.end() - _s.avail_in); - break; - case Z_STREAM_END: - buf.clear(); - break; - default: - _error = ZLIB_ERROR; - return _error; + while (total_produced < size) { + size_t chunk = std::min(size - total_produced, std::numeric_limits::max()); + _s.next_out = (Bytef *) (dst + total_produced); + _s.avail_out = uInt(chunk); + int res = Z_OK; + while((_s.avail_out > 0) && (res == Z_OK)) { + int64_t n_read = _src->read(in.data(), in.size()); + if (n_read > 0) { + buf.insert(buf.end(), in.begin(), in.begin() + n_read); + } else if (n_read != 0) { + _error = n_read; + return _error; + } + size_t buf_chunk = std::min(buf.size(), std::numeric_limits::max()); + _s.next_in = (z_const Bytef *) buf.data(); + _s.avail_in = uInt(buf_chunk); + res = inflate(&_s, flush); + switch(res) { + case Z_OK: + buf.erase(buf.begin(), buf.begin() + (buf_chunk - _s.avail_in)); + break; + case Z_STREAM_END: + buf.clear(); + break; + default: + _error = ZLIB_ERROR; + return _error; + } } + size_t produced = chunk - _s.avail_out; + total_produced += produced; + if (produced == 0) break; // no progress (EOF or stream end) } - return size - _s.avail_out; + return total_produced; } catch(...) { return INPUT_STREAM_ERROR; } diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index 52fe681f..c59838c6 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -253,14 +253,16 @@ parse_rcpt(ToolConf& conf, std::vector& rcpts, int& arg_idx, #ifndef NDEBUG // For debugging - cout << "Method: " << method << endl; - cout << "Slot: " << rcpt.p11.slot << endl; - if (!rcpt.secret.empty()) - cout << "Pin: " << string(rcpt.secret.cbegin(), rcpt.secret.cend()) << endl; + LOG_DBG("Method: {}", method); + LOG_DBG("Slot: {}", rcpt.p11.slot); + if (!rcpt.secret.empty()) { + string str(rcpt.secret.cbegin(), rcpt.secret.cend()); + LOG_TRACE("Pin: {}", str); + } if (!rcpt.p11.key_id.empty()) - cout << "Key ID: " << toHex(rcpt.p11.key_id) << endl; + LOG_DBG("Key ID: {}", toHex(rcpt.p11.key_id)); if (!rcpt.p11.key_label.empty()) - cout << "Key label: " << rcpt.p11.key_label << endl; + LOG_DBG("Key label: {}", rcpt.p11.key_label); #endif } else if (method == "share") { // label:share:RECIPIENT_ID @@ -539,7 +541,7 @@ static int ParseAndDecrypt(int argc, char *argv[]) } // Ask secret if not provided - if (ldata.secret[0] == '?') { + if (!ldata.secret.empty() && ldata.secret[0] == '?') { std::string secret = inputSecret("Enter password: "); ldata.secret.assign(secret.cbegin(), secret.cend()); } From 8c9b789d0b027bc393b978f3ea67843b39020ee4 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 27 May 2026 13:21:47 +0300 Subject: [PATCH 19/65] Some more fixes --- cdoc/Crypto.cpp | 33 +++++++++++++++++++++++++++++++++ cdoc/Crypto.h | 1 + cdoc/PKCS11Backend.cpp | 11 +++++++++++ cdoc/XmlReader.cpp | 9 ++++++++- 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/cdoc/Crypto.cpp b/cdoc/Crypto.cpp index 9eab0377..0ac43848 100644 --- a/cdoc/Crypto.cpp +++ b/cdoc/Crypto.cpp @@ -154,6 +154,39 @@ Crypto::encrypt(EVP_PKEY *pub, int padding, const std::vector &data) return result; } +std::vector Crypto::decodeBase64(const uint8_t *data) +{ + std::vector result; + if (!data) + { + LOG_ERROR("decodeBase64: null pointer was provided as input data"); + return result; + } + result.resize(strlen((const char*)data)); + auto ctx = make_unique_ptr(EVP_ENCODE_CTX_new()); + if (!ctx) + { + LOG_SSL_ERROR("EVP_ENCODE_CTX_new"); + return {}; + } + + EVP_DecodeInit(ctx.get()); + int size1 = 0, size2 = 0; + if(EVP_DecodeUpdate(ctx.get(), result.data(), &size1, data, int(result.size())) == -1) + { + LOG_SSL_ERROR("EVP_DecodeUpdate"); + result.clear(); + return result; + } + + if(SSL_FAILED(EVP_DecodeFinal(ctx.get(), result.data(), &size2), "EVP_DecodeFinal")) + result.clear(); + else + result.resize(size_t(size1 + size2)); + + return result; +} + std::vector Crypto::deriveSharedSecret(EVP_PKEY *pkey, EVP_PKEY *peerPKey) { std::vector sharedSecret; diff --git a/cdoc/Crypto.h b/cdoc/Crypto.h index ffb52f69..5d6db58e 100644 --- a/cdoc/Crypto.h +++ b/cdoc/Crypto.h @@ -71,6 +71,7 @@ class Crypto static std::vector concatKDF(const std::string &hashAlg, uint32_t keyDataLen, const std::vector &z, const std::vector &AlgorithmID, const std::vector &PartyUInfo, const std::vector &PartyVInfo); static std::vector encrypt(EVP_PKEY *pub, int padding, const std::vector &data); + static std::vector decodeBase64(const uint8_t *data); static std::vector deriveSharedSecret(EVP_PKEY *pkey, EVP_PKEY *peerPKey); static Key generateKey(const std::string &method); static uint32_t keySize(const std::string &algo); diff --git a/cdoc/PKCS11Backend.cpp b/cdoc/PKCS11Backend.cpp index bc3b8c58..e3c67520 100644 --- a/cdoc/PKCS11Backend.cpp +++ b/cdoc/PKCS11Backend.cpp @@ -386,9 +386,20 @@ libcdoc::PKCS11Backend::getPublicKey(std::vector& val, int slot, const } // Associate the Point with an EC_KEY: Finally, set up an EC_KEY structure and assign the point as the public key. EC_KEY *key = EC_KEY_new(); + if (!key) { + EC_POINT_free(pub_key_point); + EC_GROUP_free(group); + return CRYPTO_ERROR; + } EC_KEY_set_group(key, group); EC_KEY_set_public_key(key, pub_key_point); EVP_PKEY *evp_pkey = EVP_PKEY_new(); + if (!evp_pkey) { + EC_KEY_free(key); + EC_POINT_free(pub_key_point); + EC_GROUP_free(group); + return CRYPTO_ERROR; + } EVP_PKEY_assign_EC_KEY(evp_pkey, key); val = Crypto::toPublicKeyDer(evp_pkey); EVP_PKEY_free(evp_pkey); diff --git a/cdoc/XmlReader.cpp b/cdoc/XmlReader.cpp index db3a1339..4f138f5f 100644 --- a/cdoc/XmlReader.cpp +++ b/cdoc/XmlReader.cpp @@ -18,6 +18,7 @@ #include "XmlReader.h" +#include "Crypto.h" #include "Io.h" #include "Utils.h" @@ -57,6 +58,7 @@ XMLReader::~XMLReader() noexcept std::string XMLReader::attribute(const char *attr) const { + if (!d) return {}; xmlChar *tmp = xmlTextReaderGetAttribute(d, pcxmlChar(attr)); std::string result = tostring(tmp); xmlFree(tmp); @@ -65,27 +67,32 @@ std::string XMLReader::attribute(const char *attr) const bool XMLReader::isEndElement() const { + if (!d) return false; return xmlTextReaderNodeType(d) == XML_READER_TYPE_END_ELEMENT; } bool XMLReader::isElement(const char *elem) const { + if (!d) return false; return xmlStrEqual(xmlTextReaderConstLocalName(d), pcxmlChar(elem)) == 1; } bool XMLReader::read() { + if (!d) return false; return xmlTextReaderRead(d) == 1; } std::vector XMLReader::readBase64() { + if (!d) return {}; xmlTextReaderRead(d); - return libcdoc::fromBase64(reinterpret_cast(xmlTextReaderConstValue(d))); + return libcdoc::Crypto::decodeBase64(xmlTextReaderConstValue(d)); } std::string XMLReader::readText() { + if (!d) return {}; xmlTextReaderRead(d); return tostring(xmlTextReaderConstValue(d)); } From e9a8a6efc40c761ab35bbbdbe9db381eaad29667 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 27 May 2026 15:14:39 +0300 Subject: [PATCH 20/65] Secure tool key handling, use explicid compile time definitions for key dump and TLS diabling --- cdoc/CDocCipher.cpp | 40 +++++++--- cdoc/CMakeLists.txt | 8 ++ cdoc/Crypto.cpp | 27 ++++--- cdoc/NetworkBackend.cpp | 9 ++- cdoc/RcptInfo.h | 25 +------ cdoc/Utils.h | 6 +- cdoc/utils/memory.h | 159 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 224 insertions(+), 50 deletions(-) diff --git a/cdoc/CDocCipher.cpp b/cdoc/CDocCipher.cpp index 9f445895..fdd39d39 100644 --- a/cdoc/CDocCipher.cpp +++ b/cdoc/CDocCipher.cpp @@ -41,14 +41,26 @@ using namespace std; using namespace libcdoc; +static libcdoc::result_t validateRcptIdx(const std::vector& rcpts, unsigned int& idx) +{ + if (rcpts.empty()) return libcdoc::WRONG_ARGUMENTS; + if (idx < rcpts.size()) return libcdoc::OK; + if ((int)idx == rcpts[0].resolved_lock_idx) { + idx = 0; + return libcdoc::OK; + } + return libcdoc::WRONG_ARGUMENTS; +} + struct ToolPKCS11 : public libcdoc::PKCS11Backend { const std::vector& rcpts; ToolPKCS11(const std::string& library, const std::vector& vec) : libcdoc::PKCS11Backend(library), rcpts(vec) {} libcdoc::result_t connectToKey(int idx, bool priv) override final { - if (idx >= rcpts.size()) idx = 0; - const libcdoc::RcptInfo& rcpt = rcpts[idx]; + unsigned int l_idx = idx; + if (auto rv = validateRcptIdx(rcpts, l_idx); rv != libcdoc::OK) return rv; + const libcdoc::RcptInfo& rcpt = rcpts[l_idx]; if (!priv) { return useSecretKey(rcpt.p11.slot, rcpt.secret, rcpt.p11.key_id, rcpt.p11.key_label); } else { @@ -64,8 +76,9 @@ struct ToolWin : public libcdoc::WinBackend { ToolWin(const std::string& provider, const std::vector& vec) : libcdoc::WinBackend(provider), rcpts(vec) {} result_t connectToKey(int idx, bool priv) { - if (idx >= rcpts.size()) idx = 0; - const libcdoc::RcptInfo& rcpt = rcpts[idx]; + unsigned int l_idx = idx; + if (auto rv = validateRcptIdx(rcpts, l_idx); rv != OK) return rv; + const libcdoc::RcptInfo& rcpt = rcpts[l_idx]; return useKey(rcpt.p11.key_label, std::string(rcpt.secret.cbegin(), rcpt.secret.cend())); } }; @@ -97,7 +110,7 @@ struct ToolCrypto : public libcdoc::CryptoBackend { libcdoc::result_t decryptRSA(std::vector& dst, const std::vector &data, bool oaep, unsigned int idx) override final { if (p11) return p11->decryptRSA(dst, data, oaep, idx); - if (idx >= rcpts.size()) idx = 0; + if (auto rv = validateRcptIdx(rcpts, idx); rv != libcdoc::OK) return rv; const libcdoc::RcptInfo& rcpt = rcpts[idx]; if (rcpt.secret.empty()) return libcdoc::CRYPTO_ERROR; const uint8_t *p = rcpt.secret.data(); @@ -129,7 +142,7 @@ struct ToolCrypto : public libcdoc::CryptoBackend { } libcdoc::result_t deriveECDH1(std::vector& dst, const std::vector &public_key, unsigned int idx) override final { - if (idx >= rcpts.size()) idx = 0; + if (auto rv = validateRcptIdx(rcpts, idx); rv != libcdoc::OK) return rv; const libcdoc::RcptInfo& rcpt = rcpts[idx]; if (rcpt.secret.empty()) return libcdoc::CRYPTO_ERROR; const uint8_t *p = rcpt.secret.data(); @@ -181,7 +194,7 @@ struct ToolCrypto : public libcdoc::CryptoBackend { } libcdoc::result_t getSecret(std::vector& secret, unsigned int idx) override final { - if (idx >= rcpts.size()) idx = 0; + if (auto rv = validateRcptIdx(rcpts, idx); rv != libcdoc::OK) return rv; const libcdoc::RcptInfo& rcpt = rcpts[idx]; secret = rcpt.secret; return secret.empty() ? INVALID_PARAMS : libcdoc::OK; @@ -204,8 +217,9 @@ struct ToolNetwork : public libcdoc::NetworkBackend { } libcdoc::result_t getClientTLSCertificate(std::vector& dst) override final { - if (rcpt_idx >= crypto->rcpts.size()) rcpt_idx = 0; - const libcdoc::RcptInfo& rcpt = crypto->rcpts[rcpt_idx]; + unsigned int l_idx = rcpt_idx; + if (auto rv = validateRcptIdx(crypto->rcpts, l_idx); rv != libcdoc::OK) return rv; + const libcdoc::RcptInfo& rcpt = crypto->rcpts[l_idx]; return crypto->p11->getCertificate(dst, rcpt.p11.slot, rcpt.secret, rcpt.p11.key_id, rcpt.p11.key_label); } @@ -215,7 +229,8 @@ struct ToolNetwork : public libcdoc::NetworkBackend { } libcdoc::result_t signTLS(std::vector& dst, libcdoc::CryptoBackend::HashAlgorithm algorithm, const std::vector &digest) override final { - if (rcpt_idx >= crypto->rcpts.size()) rcpt_idx = 0; + unsigned int l_idx = rcpt_idx; + if (auto rv = validateRcptIdx(crypto->rcpts, l_idx); rv != libcdoc::OK) return rv; return crypto->p11->sign(dst, algorithm, digest, rcpt_idx); } @@ -377,7 +392,6 @@ int CDocCipher::Decrypt(ToolConf& conf, const RcptInfo& recipient) } LOG_DBG("Reader created"); - // Find lock by label/index/certificate int lock_idx = -1; const vector& locks = rdr->getLocks(); if (!recipient.label.empty()) { @@ -389,7 +403,7 @@ int CDocCipher::Decrypt(ToolConf& conf, const RcptInfo& recipient) } } } else if (recipient.lock_idx >= 0) { - if (recipient.lock_idx >= locks.size()) { + if (recipient.lock_idx >= (int)locks.size()) { LOG_ERROR("Label index is out of range"); return 1; } @@ -415,6 +429,7 @@ int CDocCipher::Decrypt(ToolConf& conf, const RcptInfo& recipient) return 1; } LOG_INFO("Found matching lock: {}", recipient.label); + r[0].resolved_lock_idx = lock_idx; network.rcpt_idx = lock_idx; return Decrypt(rdr, lock_idx, conf.out); @@ -544,6 +559,7 @@ CDocCipher::ReEncrypt(ToolConf& conf, const RcptInfo& dec_info, std::vector Crypto::deriveSharedSecret(EVP_PKEY *pkey, EVP_PKEY *peerPK return sharedSecret; sharedSecret.resize(sharedSecretLen); - if(EVP_PKEY_derive(ctx.get(), sharedSecret.data(), &sharedSecretLen) <= 0) + if(EVP_PKEY_derive(ctx.get(), sharedSecret.data(), &sharedSecretLen) <= 0) { sharedSecret.clear(); + return sharedSecret; + } + sharedSecret.resize(sharedSecretLen); return sharedSecret; } Crypto::Key Crypto::generateKey(const std::string &method) { const EVP_CIPHER *c = cipher(method); -#ifdef WIN32 - RAND_screen(); -#else - RAND_load_file("/dev/urandom", 1024); -#endif + if (!c) { + LOG_ERROR("generateKey: unsupported cipher method {}", method); + return {}; + } Key key(EVP_CIPHER_key_length(c), EVP_CIPHER_iv_length(c)); - uint8_t salt[PKCS5_SALT_LEN], indata[128]; - RAND_bytes(salt, sizeof(salt)); - RAND_bytes(indata, sizeof(indata)); - if (SSL_FAILED(EVP_BytesToKey(c, EVP_sha256(), salt, indata, sizeof(indata), 1, key.key.data(), key.iv.data()), "EVP_BytesToKey")) + if (RAND_status() != 1) { + LOG_ERROR("generateKey: OpenSSL PRNG not seeded"); return {}; - else - return key; + } + if (SSL_FAILED(RAND_bytes(key.key.data(), int(key.key.size())), "RAND_bytes") || + SSL_FAILED(RAND_bytes(key.iv.data(), int(key.iv.size())), "RAND_bytes")) + return {}; + return key; } uint32_t Crypto::keySize(const std::string &algo) diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 9c0c14f6..22faa07d 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -188,12 +188,13 @@ setPeerCertificates(httplib::SSLClient& cli, libcdoc::NetworkBackend *network, c cli.enable_server_hostname_verification(true); } else { -#ifdef NDEBUG - error = "No peer TLS certificates configured"; - return libcdoc::CONFIGURATION_ERROR; -#else +#ifdef LIBCDOC_ALLOW_INSECURE_TLS + LOG_WARN("TLS certificate verification disabled (LIBCDOC_ALLOW_INSECURE_TLS)"); cli.enable_server_certificate_verification(false); cli.enable_server_hostname_verification(false); +#else + error = "No peer TLS certificates configured"; + return libcdoc::CONFIGURATION_ERROR; #endif } return libcdoc::OK; diff --git a/cdoc/RcptInfo.h b/cdoc/RcptInfo.h index e4142553..0bf85b26 100644 --- a/cdoc/RcptInfo.h +++ b/cdoc/RcptInfo.h @@ -19,13 +19,13 @@ #ifndef RCPTINFO_H #define RCPTINFO_H +#include "utils/memory.h" + #include namespace libcdoc { struct RcptInfo { - // PKCS11/NCrypt data - // NB! PIN is stored in secret struct PKCS11Info { long slot = 0; std::vector key_id; @@ -33,44 +33,27 @@ struct RcptInfo { }; enum Type { - // For decryption (use the lock type) LOCK, - - // For encryption - // Certificate from file CERT, - // Password from command line PASSWORD, - // Symetric key from command line SKEY, - // Public key from command line PKEY, - // Symetric key from PKCS11 device P11_SYMMETRIC, - // Public key from PKC11 device P11_PKI, - // Windows NCRYPT, - // N of n SHARE }; Type type; - // Locks label std::string label; - // Certificate for encryption std::vector cert; - // Pin or password - std::vector secret; - // PKCS11-specific info + SecureBytes secret; PKCS11Info p11; - // Keyfile name for automatic labels std::string key_file_name; - // ID code for shares server std::string id; - // Lock index int lock_idx = -1; + int resolved_lock_idx = -1; }; } diff --git a/cdoc/Utils.h b/cdoc/Utils.h index f5ef7e2a..62600dbc 100644 --- a/cdoc/Utils.h +++ b/cdoc/Utils.h @@ -230,10 +230,14 @@ static inline void LogFormat(LogLevel level, std::string_view file, int line, st #ifdef NDEBUG #define LOG_TRACE(...) -#define LOG_TRACE_KEY(MSG, KEY) #else #define LOG_TRACE(...) LogFormat(libcdoc::LEVEL_TRACE, __FILE__, __LINE__, __VA_ARGS__) +#endif + +#ifdef LIBCDOC_CRYPTO_TRACE #define LOG_TRACE_KEY(MSG, KEY) LogFormat(libcdoc::LEVEL_TRACE, __FILE__, __LINE__, MSG, toHex(KEY)) +#else +#define LOG_TRACE_KEY(MSG, KEY) #endif } // namespace libcdoc diff --git a/cdoc/utils/memory.h b/cdoc/utils/memory.h index edb4a3c8..0a2fbade 100644 --- a/cdoc/utils/memory.h +++ b/cdoc/utils/memory.h @@ -25,8 +25,167 @@ #include +#ifdef _WIN32 +#include +#else +#include +#endif + namespace libcdoc { +class SecureBytes { + std::vector data_; + bool locked_ = false; + + void lock() noexcept { + if (!data_.empty() && !locked_) { +#ifdef _WIN32 + locked_ = VirtualLock(data_.data(), data_.size()); +#else + locked_ = (mlock(data_.data(), data_.size()) == 0); +#endif + } + } + + void unlock() noexcept { + if (!data_.empty() && locked_) { +#ifdef _WIN32 + VirtualUnlock(data_.data(), data_.size()); +#else + munlock(data_.data(), data_.size()); +#endif + locked_ = false; + } + } + +public: + using iterator = std::vector::iterator; + using const_iterator = std::vector::const_iterator; + + SecureBytes() noexcept = default; + + ~SecureBytes() { + cleanse(); + unlock(); + } + + SecureBytes(const SecureBytes& other) : data_(other.data_) { + lock(); + } + + SecureBytes(SecureBytes&& other) noexcept : data_(std::move(other.data_)), locked_(other.locked_) { + other.locked_ = false; + } + + SecureBytes& operator=(const SecureBytes& other) { + if (this != &other) { + cleanse(); + unlock(); + data_ = other.data_; + lock(); + } + return *this; + } + + SecureBytes& operator=(SecureBytes&& other) noexcept { + if (this != &other) { + cleanse(); + unlock(); + data_ = std::move(other.data_); + locked_ = other.locked_; + other.locked_ = false; + } + return *this; + } + + SecureBytes& operator=(std::vector v) { + cleanse(); + unlock(); + data_ = std::move(v); + lock(); + return *this; + } + + SecureBytes(std::vector v) noexcept : data_(std::move(v)) { + lock(); + } + + template + SecureBytes(InputIt first, InputIt last) : data_(first, last) { + lock(); + } + + explicit SecureBytes(size_t size) : data_(size) { + lock(); + } + + void assign(std::vector::const_iterator first, std::vector::const_iterator last) { + cleanse(); + unlock(); + data_.assign(first, last); + lock(); + } + + void assign(std::string::const_iterator first, std::string::const_iterator last) { + cleanse(); + unlock(); + data_.assign(first, last); + lock(); + } + + [[nodiscard]] bool empty() const noexcept { return data_.empty(); } + [[nodiscard]] size_t size() const noexcept { return data_.size(); } + [[nodiscard]] const uint8_t* data() const noexcept { return data_.data(); } + [[nodiscard]] uint8_t* data() noexcept { return data_.data(); } + [[nodiscard]] const uint8_t& operator[](size_t i) const noexcept { return data_[i]; } + [[nodiscard]] uint8_t& operator[](size_t i) noexcept { return data_[i]; } + + [[nodiscard]] const_iterator cbegin() const noexcept { return data_.cbegin(); } + [[nodiscard]] const_iterator cend() const noexcept { return data_.cend(); } + [[nodiscard]] iterator begin() noexcept { return data_.begin(); } + [[nodiscard]] iterator end() noexcept { return data_.end(); } + [[nodiscard]] const_iterator begin() const noexcept { return data_.begin(); } + [[nodiscard]] const_iterator end() const noexcept { return data_.end(); } + + void resize(size_t n) { + unlock(); + data_.resize(n); + lock(); + } + + void clear() { + cleanse(); + unlock(); + data_.clear(); + } + + static inline void secure_cleanse(void* ptr, size_t len) noexcept { +#if defined(_WIN32) + SecureZeroMemory(ptr, len); +#else + volatile unsigned char* p = static_cast(ptr); + while (len--) *p++ = 0; +#endif + } + + void cleanse() noexcept { + if (!data_.empty()) { + secure_cleanse(data_.data(), data_.size()); + } + } + + [[nodiscard]] operator const std::vector&() const noexcept { return data_; } + + [[nodiscard]] bool operator==(const SecureBytes& other) const noexcept { + if (data_.size() != other.data_.size()) return false; + return CRYPTO_memcmp(data_.data(), other.data_.data(), data_.size()) == 0; + } + + [[nodiscard]] bool operator!=(const SecureBytes& other) const noexcept { + return !(*this == other); + } +}; + template void cleanse(std::vector& v) noexcept { From fa288ee95ad834e356d9d04725a2a1edddcbd45a Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 27 May 2026 16:01:15 +0300 Subject: [PATCH 21/65] Fixed potential tar size overflow and secured proxy password --- cdoc/NetworkBackend.cpp | 5 +---- cdoc/NetworkBackend.h | 4 +++- cdoc/Tar.cpp | 11 +++++++++-- cdoc/utils/memory.h | 8 ++++++++ 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 22faa07d..8e1c153b 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -215,7 +215,7 @@ setProxy(httplib::SSLClient& cli, libcdoc::NetworkBackend *network) cli.set_proxy(cred.host, cred.port); } if (!cred.username.empty()) { - cli.set_proxy_basic_auth(cred.username, cred.password); + cli.set_proxy_basic_auth(cred.username, cred.password.toString()); } return libcdoc::OK; default: return result; @@ -489,9 +489,6 @@ libcdoc::NetworkBackend::fetchShare(ShareInfo& share, const std::string& url, co httplib::Headers hdrs; hdrs.insert({"x-cdoc2-auth-ticket", ticket}); hdrs.insert({"x-cdoc2-auth-x5c", std::string("-----BEGIN CERTIFICATE-----") + toBase64(cert) + "-----END CERTIFICATE-----"}); - for (auto i = hdrs.cbegin(); i != hdrs.cend(); i++) { - std::cerr << i->first << ": " << i->second << std::endl; - } picojson::value rsp_json; result = get(cli, hdrs, full, rsp_json); if (result != libcdoc::OK) return result; diff --git a/cdoc/NetworkBackend.h b/cdoc/NetworkBackend.h index 3ec43618..ee5f839d 100644 --- a/cdoc/NetworkBackend.h +++ b/cdoc/NetworkBackend.h @@ -21,6 +21,8 @@ #include +#include "utils/memory.h" + namespace libcdoc { struct CDOC_EXPORT NetworkBackend { @@ -120,7 +122,7 @@ struct CDOC_EXPORT NetworkBackend { /** * @brief Proxy password */ - std::string password; + SecureBytes password; }; NetworkBackend() = default; diff --git a/cdoc/Tar.cpp b/cdoc/Tar.cpp index 6a29db7c..64c7e403 100644 --- a/cdoc/Tar.cpp +++ b/cdoc/Tar.cpp @@ -28,6 +28,8 @@ using namespace libcdoc; constexpr unsigned int BLOCKSIZE = 512; +constexpr int64_t CDOC2_MAX_FILE_SIZE = 8LL * 1024 * 1024 * 1024; + template [[nodiscard]] static constexpr bool svtoi(std::string_view data, T& result) noexcept { @@ -47,6 +49,8 @@ static constexpr int64_t fromOctal(const std::array &data) noexcept { if(c < '0' || c > '7') continue; + if (i > (INT64_MAX >> 3)) + return INT64_MAX; i <<= 3; i += c - '0'; } @@ -114,7 +118,10 @@ struct libcdoc::Header { } constexpr int64_t getSize() const noexcept { - return fromOctal(size); + int64_t s = fromOctal(size); + if (s < 0 || s > CDOC2_MAX_FILE_SIZE) + return -1; + return s; } constexpr bool operator==(const Header&) const noexcept = default; @@ -339,7 +346,7 @@ libcdoc::TarSource::readPaxHeader(const Header& hdr, std::string& name, int64_t& name = headerValue; if (keyWord == "size") { int64_t parsedSize; - if (!svtoi(headerValue, parsedSize)) { + if (!svtoi(headerValue, parsedSize) || parsedSize < 0 || parsedSize > CDOC2_MAX_FILE_SIZE) { _error = DATA_FORMAT_ERROR; return _error; } diff --git a/cdoc/utils/memory.h b/cdoc/utils/memory.h index 0a2fbade..bbfc8681 100644 --- a/cdoc/utils/memory.h +++ b/cdoc/utils/memory.h @@ -110,6 +110,10 @@ class SecureBytes { lock(); } + SecureBytes(const std::string& s) : data_(s.cbegin(), s.cend()) { + lock(); + } + template SecureBytes(InputIt first, InputIt last) : data_(first, last) { lock(); @@ -176,6 +180,10 @@ class SecureBytes { [[nodiscard]] operator const std::vector&() const noexcept { return data_; } + [[nodiscard]] std::string toString() const { + return std::string(data_.cbegin(), data_.cend()); + } + [[nodiscard]] bool operator==(const SecureBytes& other) const noexcept { if (data_.size() != other.data_.size()) return false; return CRYPTO_memcmp(data_.data(), other.data_.data(), data_.size()) == 0; From b89e4f16171252db451c0d7f5f67851578569bbd Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 27 May 2026 16:25:17 +0300 Subject: [PATCH 22/65] Windows build fix --- cdoc/utils/memory.h | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/cdoc/utils/memory.h b/cdoc/utils/memory.h index bbfc8681..f0ea816a 100644 --- a/cdoc/utils/memory.h +++ b/cdoc/utils/memory.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -123,14 +124,8 @@ class SecureBytes { lock(); } - void assign(std::vector::const_iterator first, std::vector::const_iterator last) { - cleanse(); - unlock(); - data_.assign(first, last); - lock(); - } - - void assign(std::string::const_iterator first, std::string::const_iterator last) { + template + void assign(InputIt first, InputIt last) { cleanse(); unlock(); data_.assign(first, last); @@ -279,4 +274,4 @@ constexpr auto d2i(const std::vector &data, Args&&... args) noexcept return make_unique_ptr(F(std::forward(args)..., &p, long(data.size())), Free); } -} \ No newline at end of file +} From d0e0c90d21bcb3d5cda803644a48c7f37af6f030 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 27 May 2026 16:45:00 +0300 Subject: [PATCH 23/65] Revert proxy password for now --- cdoc/NetworkBackend.cpp | 2 +- cdoc/NetworkBackend.h | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 8e1c153b..478066bc 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -215,7 +215,7 @@ setProxy(httplib::SSLClient& cli, libcdoc::NetworkBackend *network) cli.set_proxy(cred.host, cred.port); } if (!cred.username.empty()) { - cli.set_proxy_basic_auth(cred.username, cred.password.toString()); + cli.set_proxy_basic_auth(cred.username, cred.password); } return libcdoc::OK; default: return result; diff --git a/cdoc/NetworkBackend.h b/cdoc/NetworkBackend.h index ee5f839d..3ec43618 100644 --- a/cdoc/NetworkBackend.h +++ b/cdoc/NetworkBackend.h @@ -21,8 +21,6 @@ #include -#include "utils/memory.h" - namespace libcdoc { struct CDOC_EXPORT NetworkBackend { @@ -122,7 +120,7 @@ struct CDOC_EXPORT NetworkBackend { /** * @brief Proxy password */ - SecureBytes password; + std::string password; }; NetworkBackend() = default; From 87d15474eb14d7ba28d108e475abac83cbf6b23d Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 29 May 2026 09:53:09 +0300 Subject: [PATCH 24/65] Make proxy password string_view --- cdoc/NetworkBackend.h | 4 +++- cdoc/cdoc-tool.cpp | 2 +- cdoc/httplib.h | 8 ++++---- cdoc/utils/memory.h | 5 +---- libcdoc.i | 2 +- test/libcdoc_boost.cpp | 1 + 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cdoc/NetworkBackend.h b/cdoc/NetworkBackend.h index 3ec43618..8a3b8621 100644 --- a/cdoc/NetworkBackend.h +++ b/cdoc/NetworkBackend.h @@ -119,8 +119,10 @@ struct CDOC_EXPORT NetworkBackend { std::string username; /** * @brief Proxy password + * + * It is the implementer's responsibility to ensure that the buffer remains valid during CDocWriter getFMK and beginEncryption calls */ - std::string password; + std::string_view password; }; NetworkBackend() = default; diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index c59838c6..d629034d 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -377,7 +377,7 @@ struct LockData { long slot = -1; vector key_id; string key_label; - vector secret; + SecureBytes secret; int validate(ToolConf& conf) { if (lock_label.empty() && (lock_idx == -1) && (slot < 0)) { diff --git a/cdoc/httplib.h b/cdoc/httplib.h index 465219d0..41fef190 100644 --- a/cdoc/httplib.h +++ b/cdoc/httplib.h @@ -1442,7 +1442,7 @@ class ClientImpl { void set_proxy(const std::string &host, int port); void set_proxy_basic_auth(const std::string &username, - const std::string &password); + const std::string_view &password); void set_proxy_bearer_token_auth(const std::string &token); #ifdef CPPHTTPLIB_OPENSSL_SUPPORT void set_proxy_digest_auth(const std::string &username, @@ -1875,7 +1875,7 @@ class Client { void set_proxy(const std::string &host, int port); void set_proxy_basic_auth(const std::string &username, - const std::string &password); + const std::string_view &password); void set_proxy_bearer_token_auth(const std::string &token); #ifdef CPPHTTPLIB_OPENSSL_SUPPORT void set_proxy_digest_auth(const std::string &username, @@ -8786,7 +8786,7 @@ inline void ClientImpl::set_proxy(const std::string &host, int port) { } inline void ClientImpl::set_proxy_basic_auth(const std::string &username, - const std::string &password) { + const std::string_view &password) { proxy_basic_auth_username_ = username; proxy_basic_auth_password_ = password; } @@ -10190,7 +10190,7 @@ inline void Client::set_proxy(const std::string &host, int port) { cli_->set_proxy(host, port); } inline void Client::set_proxy_basic_auth(const std::string &username, - const std::string &password) { + const std::string_view &password) { cli_->set_proxy_basic_auth(username, password); } inline void Client::set_proxy_bearer_token_auth(const std::string &token) { diff --git a/cdoc/utils/memory.h b/cdoc/utils/memory.h index f0ea816a..42e027dd 100644 --- a/cdoc/utils/memory.h +++ b/cdoc/utils/memory.h @@ -24,6 +24,7 @@ #include #include +#define OPENSSL_SUPPRESS_DEPRECATED #include #ifdef _WIN32 @@ -175,10 +176,6 @@ class SecureBytes { [[nodiscard]] operator const std::vector&() const noexcept { return data_; } - [[nodiscard]] std::string toString() const { - return std::string(data_.cbegin(), data_.cend()); - } - [[nodiscard]] bool operator==(const SecureBytes& other) const noexcept { if (data_.size() != other.data_.size()) return false; return CRYPTO_memcmp(data_.data(), other.data_.data(), data_.size()) == 0; diff --git a/libcdoc.i b/libcdoc.i index 40469569..6bc01d3f 100644 --- a/libcdoc.i +++ b/libcdoc.i @@ -38,7 +38,6 @@ // Handle standard C++ types %include "std_string.i" %include "std_vector.i" -//%include "std_map.i" %include "typemaps.i" @@ -311,6 +310,7 @@ #ifdef SWIGJAVA %include "arrays_java.i" +%include "std_string_view.i" %include "enums.swg" %javaconst(1); diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 8573e4f6..2b476b55 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -312,6 +312,7 @@ decrypt(const std::vector& files, const std::string& container, con libcdoc::RcptInfo rcpt {.type=libcdoc::RcptInfo::LOCK, .secret=key, .lock_idx=idx}; decrypt(files, container, dir, rcpt, remove); } + static int unicode_to_utf8 (unsigned int uval, uint8_t *d, uint64_t size) { From 3631991db4307c21998970584fc87da0702229dc Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Mon, 1 Jun 2026 12:56:40 +0300 Subject: [PATCH 25/65] Disable potential Bleichenbacher attack for CDoc1 RSA encryption --- cdoc/CDoc1Reader.cpp | 129 +++++++++++++--- cdoc/CDocCipher.cpp | 57 +++++-- cdoc/Crypto.cpp | 341 +++++++++++++++++++++++++++++++++++++++-- cdoc/Crypto.h | 90 +++++++++++ cdoc/CryptoBackend.cpp | 44 +++++- cdoc/CryptoBackend.h | 34 ++++ cdoc/PKCS11Backend.cpp | 110 +++++++++++-- cdoc/PKCS11Backend.h | 1 + cdoc/WinBackend.cpp | 209 ++++++++++++++++++++++--- cdoc/WinBackend.h | 1 + cdoc/cdoc-tool.cpp | 2 +- 11 files changed, 932 insertions(+), 86 deletions(-) diff --git a/cdoc/CDoc1Reader.cpp b/cdoc/CDoc1Reader.cpp index 0c9f18b8..94b97e32 100644 --- a/cdoc/CDoc1Reader.cpp +++ b/cdoc/CDoc1Reader.cpp @@ -25,6 +25,9 @@ #include "Lock.h" #include "Utils.h" #include "ZStream.h" +#include "utils/memory.h" + +#include #include @@ -109,14 +112,56 @@ CDoc1Reader::getFMK(std::vector& fmk, unsigned int lock_idx) if (lock_idx >= d->locks.size()) return libcdoc::WRONG_ARGUMENTS; const Lock &lock = d->locks.at(lock_idx); setLastError({}); + + // Determine the FMK length from the container's body cipher. The CDoc1 + // body uses AES-128/192/256 in CBC or GCM mode, so the FMK is 16, 24 + // or 32 bytes long. We pin this length up-front and pass it to the RSA + // decrypt path so that an attacker observing this function cannot + // distinguish between + // (a) RSA padding failed + // (b) RSA padding succeeded but the resulting length was wrong + // (c) a wholly different recipient was used to derive a wrong key. + // + // All three cases must look the same: the function returns OK with a + // candidate FMK of the right length, and the eventual AES decrypt at + // the container body level either authenticates that FMK (success) or + // rejects it. CDoc1 has no header HMAC, so the AES-GCM tag is the + // only bit of authentication we can rely on. AES-CBC containers + // therefore retain a residual oracle (PKCS#7 stripping); using GCM + // when re-encrypting with libcdoc is strongly preferred. + size_t expected_fmk_len = 0; + if (const EVP_CIPHER *c = libcdoc::Crypto::cipher(d->method); c) { + expected_fmk_len = size_t(EVP_CIPHER_key_length(c)); + } + if (expected_fmk_len != 16 && expected_fmk_len != 24 && expected_fmk_len != 32) { + // Method-level error - independent of key bits, so does NOT feed + // an oracle. + setLastError("Failed to derive FMK"); + LOG_ERROR("Unsupported CDoc1 encryption method: {}", d->method); + return libcdoc::CRYPTO_ERROR; + } + + // From this point on, every error path returns the SAME error code and + // SAME last-error string, so that the only bit of information leaking + // back to the caller is "this lock did/did not produce a usable FMK". + constexpr auto FAIL_MSG = "Failed to derive FMK"; + if (lock.isRSA()) { - int result = crypto->decryptRSA(fmk, lock.encrypted_fmk, false, lock_idx); - if (result < 0) { - setLastError(crypto->getLastErrorStr(result)); + // Implicit-rejection-aware decrypt. Returns OK with synthetic + // bytes on padding failure; only a fundamental error (e.g. ct size + // mismatch with modulus) yields a non-OK result. + int result = crypto->decryptRSACDoc1(fmk, lock.encrypted_fmk, expected_fmk_len, lock_idx); + if (result != libcdoc::OK) { + libcdoc::cleanse(fmk); + fmk.clear(); + setLastError(FAIL_MSG); LOG_ERROR("{}", last_error); - return libcdoc::CRYPTO_ERROR; - } - } else { + return libcdoc::CRYPTO_ERROR; + } + // Even on "OK" the contents may be synthetic - that is the point. + // The downstream AES decrypt at the body level is what tells + // success from failure. + } else { std::vector key; int result = crypto->deriveConcatKDF(key, lock.getBytes(Lock::Params::KEY_MATERIAL), @@ -125,18 +170,31 @@ CDoc1Reader::getFMK(std::vector& fmk, unsigned int lock_idx) lock.getBytes(Lock::Params::PARTY_UINFO), lock.getBytes(Lock::Params::PARTY_VINFO), lock_idx); - if (result < 0) { - setLastError(crypto->getLastErrorStr(result)); + if (result < 0) { + libcdoc::cleanse(key); + setLastError(FAIL_MSG); LOG_ERROR("{}", last_error); - return libcdoc::CRYPTO_ERROR; - } + return libcdoc::CRYPTO_ERROR; + } fmk = libcdoc::Crypto::AESWrap(key, lock.encrypted_fmk, false); - } - if (fmk.empty()) { - setLastError("Failed to decrypt/derive fmk"); + libcdoc::cleanse(key); + // AESWrap returns {} on failure. Pad the candidate to expected + // length so the failure shape matches the RSA path; the bytes + // are arbitrary because the body decrypt is going to reject + // them anyway. + if (fmk.size() != expected_fmk_len) { + libcdoc::cleanse(fmk); + fmk.assign(expected_fmk_len, 0); + } + } + + if (fmk.size() != expected_fmk_len) { + libcdoc::cleanse(fmk); + fmk.clear(); + setLastError(FAIL_MSG); LOG_ERROR("{}", last_error); - return libcdoc::CRYPTO_ERROR; - } + return libcdoc::CRYPTO_ERROR; + } return libcdoc::OK; } @@ -393,18 +451,47 @@ result_t CDoc1Reader::decryptData(const std::vector& fmk, setLastError("Failed to decode base64 data"); return libcdoc::IO_ERROR; } + + // Treat any post-FMK decrypt error - including AES-CBC PKCS#7 stripping + // failures and AES-GCM tag mismatches - as the same "container body + // decrypt failed" event. This is the single bit of information an + // attacker can extract per submission of a tampered CDoc1, and we + // rate-limit it. A per-process exponential backoff turns a remote + // Bleichenbacher campaign of 2^20+ queries into hours/days of + // wall-clock cost without penalising legitimate single-shot use. + constexpr auto THROTTLE_SCOPE = "cdoc1-rsa-decrypt"; + auto report_failure = [&]{ + libcdoc::Crypto::rsaOracleThrottleOnFailure(THROTTLE_SCOPE); + }; + VectorSource src(b64); libcdoc::DecryptionSource dec(src, d->method, fmk); if(dec.isError()) { - setLastError("Failed to decrypt data, verify if FMK is correct"); + setLastError("Failed to decrypt data"); + report_failure(); return CRYPTO_ERROR; } + libcdoc::result_t inner_rv = libcdoc::OK; if (d->mime == MIME_ZLIB) { libcdoc::ZSource zsrc(&dec); - if(auto rv = f(zsrc, d->properties["OriginalMimeType"]); rv < OK) - return rv; + inner_rv = f(zsrc, d->properties["OriginalMimeType"]); + } else { + inner_rv = f(dec, d->mime); + } + if (inner_rv < OK) { + // Body parse/decrypt failure. Could be a real I/O glitch, or a + // tampered container - we cannot tell, and on principle we treat + // both alike to deny the attacker a distinguisher. + setLastError("Failed to decrypt data"); + report_failure(); + return inner_rv; } - else if(auto rv = f(dec, d->mime); rv < OK) - return rv; - return dec.close(); + libcdoc::result_t close_rv = dec.close(); + if (close_rv != libcdoc::OK) { + setLastError("Failed to decrypt data"); + report_failure(); + return close_rv; + } + libcdoc::Crypto::rsaOracleThrottleOnSuccess(THROTTLE_SCOPE); + return libcdoc::OK; } diff --git a/cdoc/CDocCipher.cpp b/cdoc/CDocCipher.cpp index fdd39d39..581a8937 100644 --- a/cdoc/CDocCipher.cpp +++ b/cdoc/CDocCipher.cpp @@ -113,34 +113,69 @@ struct ToolCrypto : public libcdoc::CryptoBackend { if (auto rv = validateRcptIdx(rcpts, idx); rv != libcdoc::OK) return rv; const libcdoc::RcptInfo& rcpt = rcpts[idx]; if (rcpt.secret.empty()) return libcdoc::CRYPTO_ERROR; - const uint8_t *p = rcpt.secret.data(); + // Note: EVP_PKEY_* functions return 1 on success, 0 on a (possibly + // recoverable) failure such as RSA padding mismatch, and a negative + // value on fatal errors. Anything other than 1 must be treated as + // failure - returning 0 as success would leak partial/garbage + // plaintext and create a Bleichenbacher-style padding oracle for + // PKCS#1 v1.5 (CDoc1) decryption. + const uint8_t *p = rcpt.secret.data(); auto key = make_unique_ptr(d2i_PrivateKey(EVP_PKEY_RSA, nullptr, &p, rcpt.secret.size())); if (!key) return libcdoc::CRYPTO_ERROR; auto ctx = make_unique_ptr(EVP_PKEY_CTX_new(key.get(), nullptr)); if (!ctx) return libcdoc::CRYPTO_ERROR; - int result = EVP_PKEY_decrypt_init(ctx.get()); - if (result < 0) return libcdoc::CRYPTO_ERROR; + if (EVP_PKEY_decrypt_init(ctx.get()) != 1) + return libcdoc::CRYPTO_ERROR; + if (oaep) { - if ((EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING) < 0) || - (EVP_PKEY_CTX_set_rsa_oaep_md(ctx.get(), EVP_sha256()) < 0) || - (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx.get(), EVP_sha256()) < 0)) + if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING) != 1 || + EVP_PKEY_CTX_set_rsa_oaep_md(ctx.get(), EVP_sha256()) != 1 || + EVP_PKEY_CTX_set_rsa_mgf1_md(ctx.get(), EVP_sha256()) != 1) { return libcdoc::CRYPTO_ERROR; + } } - size_t outlen; - result = EVP_PKEY_decrypt(ctx.get(), NULL, &outlen, data.data(), data.size()); - if (result < 0) return libcdoc::CRYPTO_ERROR; + // First call queries the maximum output size. + size_t outlen = 0; + if (EVP_PKEY_decrypt(ctx.get(), nullptr, &outlen, data.data(), data.size()) != 1) + return libcdoc::CRYPTO_ERROR; + dst.resize(outlen); - result = EVP_PKEY_decrypt(ctx.get(), dst.data(), &outlen, data.data(), data.size()); - if (result < 0) return libcdoc::CRYPTO_ERROR; + if (EVP_PKEY_decrypt(ctx.get(), dst.data(), &outlen, data.data(), data.size()) != 1) { + // Wipe any partial plaintext that may have been written before + // padding verification failed; it could otherwise be observed by + // callers and used to mount a padding-oracle attack. + libcdoc::cleanse(dst); + dst.clear(); + return libcdoc::CRYPTO_ERROR; + } dst.resize(outlen); return libcdoc::OK; } + libcdoc::result_t decryptRSACDoc1(std::vector& dst, + const std::vector &data, + size_t expected_len, + unsigned int idx) override final { + if (p11) return p11->decryptRSACDoc1(dst, data, expected_len, idx); + if (auto rv = validateRcptIdx(rcpts, idx); rv != libcdoc::OK) return rv; + const libcdoc::RcptInfo& rcpt = rcpts[idx]; + if (rcpt.secret.empty()) return libcdoc::CRYPTO_ERROR; + + const uint8_t *p = rcpt.secret.data(); + auto key = make_unique_ptr(d2i_PrivateKey(EVP_PKEY_RSA, nullptr, &p, rcpt.secret.size())); + if (!key) return libcdoc::CRYPTO_ERROR; + + // Implicit-rejection-aware decrypt. Returns OK on padding success + // AND on padding failure (with synthetic output). Only fatal errors + // (e.g. ct size mismatch with modulus) are surfaced as CRYPTO_ERROR. + return libcdoc::Crypto::decryptRSAv15_implicitReject(dst, key.get(), data, expected_len); + } + libcdoc::result_t deriveECDH1(std::vector& dst, const std::vector &public_key, unsigned int idx) override final { if (auto rv = validateRcptIdx(rcpts, idx); rv != libcdoc::OK) return rv; const libcdoc::RcptInfo& rcpt = rcpts[idx]; diff --git a/cdoc/Crypto.cpp b/cdoc/Crypto.cpp index 30d81d14..daa72e70 100644 --- a/cdoc/Crypto.cpp +++ b/cdoc/Crypto.cpp @@ -19,6 +19,7 @@ #include "CDoc.h" #include "Crypto.h" #include "Utils.h" +#include "utils/ct.h" #define OPENSSL_SUPPRESS_DEPRECATED @@ -28,13 +29,26 @@ #include #include +#include #include +#include #include +#include #include #include +#if OPENSSL_VERSION_NUMBER >= 0x30200000L +#include +#include +#endif + #include +#include #include +#include +#include +#include +#include using namespace libcdoc; @@ -47,21 +61,29 @@ const std::string Crypto::AGREEMENT_MTH = "http://www.w3.org/2009/xmlenc11#ECDH- std::vector Crypto::AESWrap(const std::vector &key, const std::vector &data, bool encrypt) { - AES_KEY aes; - // fixme: Fix SSL_FAILED, current solution is idiotic - if (encrypt && !SSL_FAILED(AES_set_encrypt_key(key.data(), int(key.size()) * 8, &aes), "AES_set_encrypt_key") || - !encrypt && !SSL_FAILED(AES_set_decrypt_key(key.data(), int(key.size()) * 8, &aes), "AES_set_decrypt_key")) + // Note: AES_set_{encrypt,decrypt}_key return 0 on success and a negative + // value on failure - the opposite convention from OpenSSL's EVP_* APIs that + // SSL_FAILED is designed for. Check the return value directly. + AES_KEY aes; + const int key_bits = int(key.size()) * 8; + const int key_init_rv = encrypt + ? AES_set_encrypt_key(key.data(), key_bits, &aes) + : AES_set_decrypt_key(key.data(), key_bits, &aes); + if (key_init_rv != 0) { + LOG_SSL_ERROR(encrypt ? "AES_set_encrypt_key" : "AES_set_decrypt_key"); return {}; + } - std::vector result(data.size() + 8); - int size = encrypt ? - AES_wrap_key(&aes, nullptr, result.data(), data.data(), data.size()) : - AES_unwrap_key(&aes, nullptr, result.data(), data.data(), data.size()); - if(size > 0) - result.resize(size_t(size)); - else - result.clear(); - return result; + std::vector result(data.size() + 8); + const int size = encrypt + ? AES_wrap_key(&aes, nullptr, result.data(), data.data(), data.size()) + : AES_unwrap_key(&aes, nullptr, result.data(), data.data(), data.size()); + if (size <= 0) { + result.clear(); + return result; + } + result.resize(size_t(size)); + return result; } const EVP_CIPHER *Crypto::cipher(const std::string &algo) @@ -444,6 +466,299 @@ void Crypto::LogSslError(const char* funcName, const char* file, int line) } } +namespace { + +// Per-scope consecutive-failure counter. Process-wide. The mutex protects a +// small map keyed by scope string; lock contention is negligible because +// throttle invocations only happen on the failed-decrypt path which is +// already an attacker-budget-limited code path. +std::mutex g_throttle_mutex; +std::unordered_map g_throttle_failures; + +constexpr std::chrono::milliseconds kThrottleBase{50}; +constexpr std::chrono::milliseconds kThrottleCap{5000}; + +} // anonymous namespace + +void Crypto::rsaOracleThrottleOnFailure(const std::string& scope) +{ + unsigned int failures = 0; + { + std::lock_guard lk(g_throttle_mutex); + failures = ++g_throttle_failures[scope]; + } + + // delay = base * 2^(failures-1), capped at kThrottleCap. Computed on a + // wider integer to avoid overflow for very large failure counts. + auto delay = kThrottleBase; + for (unsigned int i = 1; i < failures && delay < kThrottleCap; ++i) { + delay *= 2; + } + if (delay > kThrottleCap) delay = kThrottleCap; + + LOG_WARN("RSA decrypt failure (scope={}, consecutive={}); throttling for {} ms", + scope, failures, delay.count()); + std::this_thread::sleep_for(delay); +} + +void Crypto::rsaOracleThrottleOnSuccess(const std::string& scope) +{ + std::lock_guard lk(g_throttle_mutex); + g_throttle_failures.erase(scope); +} + +namespace { + +// Derive a per-(privkey, ciphertext) deterministic byte string used as the +// "synthetic plaintext" when PKCS#1 v1.5 unpadding fails. We follow the +// recipe in RFC 8017 section 7.2.2 and OpenSSL 3.2's implicit-rejection +// implementation: HMAC-SHA-256(privkey_seed, ciphertext) seeded into HKDF +// expand. The output is deterministic-of-(key, ct) so repeating the same +// query yields the same synthetic output (this is what defeats the +// distinguisher); but unpredictable to an attacker who does not know the +// private key. +std::vector syntheticPlaintext(EVP_PKEY *priv, + const std::vector &ct, + size_t out_len) +{ + if (!priv || out_len == 0) return std::vector(out_len, 0); + + // Use the private key's PKCS#8 DER as the HMAC key. It is private to the + // decryption process and stable across calls. + int der_len = i2d_PrivateKey(priv, nullptr); + if (der_len <= 0) + return std::vector(out_len, 0); + std::vector mac_key(size_t(der_len), 0); + { + unsigned char *p = mac_key.data(); + if (i2d_PrivateKey(priv, &p) != der_len) { + libcdoc::cleanse(mac_key); + return std::vector(out_len, 0); + } + } + + std::vector prk = Crypto::sign_hmac(mac_key, ct); + libcdoc::cleanse(mac_key); + if (prk.empty()) + return std::vector(out_len, 0); + + auto out = Crypto::expand(prk, "cdoc1-rsa-implicit-reject", int(out_len)); + libcdoc::cleanse(prk); + if (out.size() != out_len) { + libcdoc::cleanse(out); + return std::vector(out_len, 0); + } + return out; +} + +// Constant-time PKCS#1 v1.5 unpadding. Walks the entire EM block in a +// data-independent fashion regardless of where (or whether) the 0x00 +// separator is found, the value of the leading bytes, or the eventual +// message length. Produces a single byte mask `good` (0xFF on valid +// padding, 0x00 otherwise) and a copy of either the recovered message +// or the synthetic plaintext into `dst`. dst is always exactly +// expected_len bytes long. +void unpadPKCS1v15CT(const std::vector &em, + const std::vector &synth, + size_t expected_len, + std::vector &dst) +{ + using namespace libcdoc::ct; + + dst.assign(expected_len, 0); + + // Need at least 0x00 || 0x02 || PS(>=8) || 0x00 || M + // -> EM length must be >= 11 + expected_len. + if (em.size() < 11u + expected_len) { + // Caller-side guarantees this in normal use because the modulus is + // always larger than expected_len. Still, fall back to synthetic + // output rather than reading out-of-bounds. + for (size_t i = 0; i < expected_len; ++i) + dst[i] = synth[i]; + return; + } + + // Initial header check. + uint8_t good = 0xFF; + good &= eq8(em[0], 0x00); + good &= eq8(em[1], 0x02); + + // Find the index of the first 0x00 byte at index >= 2. + // We must walk every byte of EM regardless of where the byte happens + // to be, otherwise a timing channel leaks the position of the first + // 0x00 (the classic "Manger / Bardou" oracle). + size_t first_zero_idx = 0; + uint8_t found_zero = 0x00; + for (size_t i = 2; i < em.size(); ++i) { + uint8_t is_zero = eq8(em[i], 0x00); + // latch the first index at which is_zero is set + uint8_t latch = uint8_t(is_zero & ~found_zero); + // "if latch then first_zero_idx = i". We can't branch; do it + // arithmetically. (i fits comfortably in size_t.) + const size_t mask_size = (latch == 0xFF) ? ~size_t(0) : size_t(0); + first_zero_idx = (i & mask_size) | (first_zero_idx & ~mask_size); + found_zero = uint8_t(found_zero | is_zero); + } + good &= found_zero; + + // PS must be at least 8 bytes -> first 0x00 index >= 10. + good &= ge_size(first_zero_idx, 10); + + // Message starts after the separator and runs to the end of EM. + // (We don't need ge here because if found_zero is 0xFF then + // first_zero_idx <= em.size()-1.) + size_t msg_off = first_zero_idx + 1; + size_t msg_len = (msg_off <= em.size()) ? (em.size() - msg_off) : 0; + + // Check that the message length matches what the caller expects. + good &= eq32(uint32_t(msg_len), uint32_t(expected_len)); + + // Constant-time copy: walk every possible message offset, and for + // each output position i select em[msg_off + i] if it is in range, + // otherwise 0. We then conditionally mux it against the synthetic + // plaintext using `good`. + // + // Important: the inner read em[src_idx] must not depend on `good` in + // a way that the compiler could turn into a conditional load. We + // therefore always perform the read and clamp src_idx to a valid + // range (em.size() - 1). When good==0 we discard the value. + for (size_t i = 0; i < expected_len; ++i) { + size_t src_idx = msg_off + i; + // Clamp: if src_idx >= em.size() use em[em.size()-1] (always in + // range since em.size() >= 11+expected_len > 0). The clamped value + // is replaced by synth[i] below when good == 0, so the actual + // bytes read here never reach the caller. + size_t in_range = size_t(ge_size(em.size() - 1, src_idx)); // 0 or 0xFF + size_t mask = in_range & ~size_t(0); + size_t safe_idx = (src_idx & mask) | ((em.size() - 1) & ~mask); + uint8_t real = em[safe_idx]; + uint8_t synthetic = synth[i]; + dst[i] = uint8_t((real & good) | (synthetic & uint8_t(~good))); + } +} + +} // anonymous namespace + +int Crypto::rsaImplicitRejectFromEM(std::vector& dst, + const std::vector& em, + const std::vector& /*ct*/, + const std::vector& synth_seed, + size_t expected_len) +{ + // The caller passes a key-derived synthetic seed already sized to + // `expected_len`. We don't recompute it here so that PKCS#11 / CNG + // callers who only have access to a public key (the private key never + // leaves the token) can still produce a stable synthetic output by + // seeding from any private-key-derived material they have - typically + // the certificate fingerprint plus the ciphertext. + if (synth_seed.size() != expected_len) + return CRYPTO_ERROR; + + unpadPKCS1v15CT(em, synth_seed, expected_len, dst); + return OK; +} + +int Crypto::decryptRSAv15_implicitReject(std::vector& dst, + EVP_PKEY *priv, + const std::vector& ct, + size_t expected_len) +{ + if (!priv || expected_len == 0) + return CRYPTO_ERROR; + + auto ctx = make_unique_ptr(EVP_PKEY_CTX_new(priv, nullptr)); + if (!ctx) { + LOG_SSL_ERROR("EVP_PKEY_CTX_new"); + return CRYPTO_ERROR; + } + +#if OPENSSL_VERSION_NUMBER >= 0x30200000L + // Native fast path: OpenSSL 3.2+ implements the implicit-rejection + // countermeasure internally, with platform-specific constant-time + // primitives, when this control is set AFTER EVP_PKEY_decrypt_init. + // + // Behaviour with implicit rejection enabled: a successful PKCS#1 v1.5 + // unpad returns the original plaintext (length = M length). A failed + // unpad returns a deterministic synthetic message of length + // (modulus_bytes - 11) - the maximum unpad length for the modulus. + // Either way EVP_PKEY_decrypt returns 1. + // + // We allocate a buffer of (modulus_bytes - 11) so both cases fit, then + // accept the result iff outlen == expected_len. A wrong-length + // unpadding (real bug or wrong-key-with-coincidentally-good-padding) + // is treated as a padding failure: fall through to the software path + // which produces a synthetic plaintext of expected_len bytes. + if (EVP_PKEY_decrypt_init(ctx.get()) == 1 && + EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_PADDING) == 1) { + unsigned int impl_reject = 1; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_uint(OSSL_ASYM_CIPHER_PARAM_IMPLICIT_REJECTION, + &impl_reject), + OSSL_PARAM_END + }; + if (EVP_PKEY_CTX_set_params(ctx.get(), params) == 1) { + const size_t mod_size = size_t(EVP_PKEY_get_size(priv)); + if (mod_size > 11 + expected_len && ct.size() == mod_size) { + // Allocate enough room for the worst-case (synthetic) + // output, then ask the API how many bytes it wrote. + std::vector tmp(mod_size, 0); + size_t outlen = tmp.size(); + int rv = EVP_PKEY_decrypt(ctx.get(), tmp.data(), &outlen, + ct.data(), ct.size()); + if (rv == 1 && outlen == expected_len) { + dst.assign(tmp.begin(), tmp.begin() + outlen); + libcdoc::cleanse(tmp); + return OK; + } + libcdoc::cleanse(tmp); + // Length didn't match - fall through to software path so we + // produce a synthetic plaintext of the correct length. + } + } + } + // Reset the context for the fall-through software path. + ctx = make_unique_ptr(EVP_PKEY_CTX_new(priv, nullptr)); + if (!ctx) { + LOG_SSL_ERROR("EVP_PKEY_CTX_new"); + return CRYPTO_ERROR; + } +#endif + + // Software path: raw RSA decrypt + constant-time unpadding. + if (EVP_PKEY_decrypt_init(ctx.get()) != 1 || + EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_NO_PADDING) != 1) { + LOG_SSL_ERROR("EVP_PKEY_decrypt_init/RSA_NO_PADDING"); + return CRYPTO_ERROR; + } + + const size_t mod_size = size_t(EVP_PKEY_get_size(priv)); + if (mod_size == 0 || ct.size() != mod_size) { + // Genuine input error - return CRYPTO_ERROR rather than synthetic + // bytes. The shape of this failure is independent of any padding + // bits, so it does not feed an oracle. + return CRYPTO_ERROR; + } + + std::vector em(mod_size, 0); + size_t em_len = mod_size; + int rv = EVP_PKEY_decrypt(ctx.get(), em.data(), &em_len, + ct.data(), ct.size()); + if (rv != 1 || em_len != mod_size) { + // Raw RSA can fail if ct >= modulus. Produce a synthetic plaintext + // anyway so the timing/return shape matches a "bad padding" path + // and does not leak the cause. + libcdoc::cleanse(em); + dst = syntheticPlaintext(priv, ct, expected_len); + return OK; + } + + std::vector synth = syntheticPlaintext(priv, ct, expected_len); + unpadPKCS1v15CT(em, synth, expected_len, dst); + libcdoc::cleanse(em); + libcdoc::cleanse(synth); + return OK; +} + EncryptionConsumer::EncryptionConsumer(DataConsumer &dst, const std::string &method, const Crypto::Key &key) : EncryptionConsumer(dst, Crypto::cipher(method), key) {} diff --git a/cdoc/Crypto.h b/cdoc/Crypto.h index 5d6db58e..03f93789 100644 --- a/cdoc/Crypto.h +++ b/cdoc/Crypto.h @@ -98,6 +98,96 @@ class Crypto static std::vector random(uint32_t len = 32); static int xor_data(std::vector& dst, const std::vector &lhs, const std::vector &rhs); + /** + * @brief Decrypt an RSA PKCS#1 v1.5 ciphertext with implicit rejection. + * + * Implements the decryption procedure of RFC 8017 section 7.2.2 with the + * "implicit rejection" countermeasure described by Bleichenbacher / + * RFC 8017 Appendix B / OpenSSL's @c EVP_PKEY_CTX_set_rsa_implicit_rejection. + * On successful unpadding produces the recovered plaintext; on padding + * failure produces a deterministic synthetic plaintext derived from the + * private key and the ciphertext, of the requested @p expected_len bytes, + * indistinguishable from a real decryption to an attacker who does not + * already know the private key. + * + * The function ALWAYS returns @c OK and ALWAYS produces exactly + * @p expected_len output bytes for any well-formed ciphertext (size equal + * to the modulus length). The caller MUST treat the output as a candidate + * key whose validity can only be confirmed by a downstream authenticated + * step (AES-GCM tag, HMAC, ...). It MUST NOT branch on the output's + * structure or surface a different error for "decryption failed" vs + * "key was wrong" - doing so reintroduces the very oracle this function + * exists to remove. + * + * On OpenSSL >= 3.2 this delegates to OpenSSL's native implementation; on + * older OpenSSL versions it implements the same algorithm in software. + * + * @param dst destination buffer (resized to @p expected_len) + * @param priv RSA private key + * @param ct ciphertext (must equal modulus length) + * @param expected_len length of plaintext the caller expects to receive + * @return OK on success, CRYPTO_ERROR only for unrecoverable, non-padding + * failures (e.g. ct size mismatch with modulus) + */ + static int decryptRSAv15_implicitReject(std::vector& dst, + EVP_PKEY *priv, + const std::vector& ct, + size_t expected_len); + + /** + * @brief Apply a delay proportional to the number of consecutive + * decrypt failures recorded for a given (process, key) pair. + * + * Bleichenbacher / cross-protocol attacks against RSA-PKCS#1 v1.5 require + * a large number of adaptive queries against the same victim + * ciphertext-key pair. This helper introduces an exponentially-growing + * sleep on consecutive decrypt failures, which dramatically increases + * the wall-clock cost of a remote oracle attack while remaining + * essentially invisible during normal use (one or two failures only). + * + * The throttle is per-process and is designed to be advisory: long- + * running services that decrypt many containers should additionally + * implement per-recipient rate limits in their host application. + * + * @param scope an arbitrary string that scopes the failure counter; use + * the recipient identifier or "default" if you don't have + * one. Different scopes have independent counters. + */ + static void rsaOracleThrottleOnFailure(const std::string& scope); + + /** + * @brief Reset the consecutive-failure counter for the given scope. + * + * Should be called after any successful authenticated decrypt to + * prevent the throttle from punishing legitimate retries. + */ + static void rsaOracleThrottleOnSuccess(const std::string& scope); + + /** + * @brief Constant-time PKCS#1 v1.5 unpadding from a pre-decrypted EM block. + * + * Same semantics as @ref decryptRSAv15_implicitReject, but skips the raw + * RSA decryption step. Intended for backends (PKCS#11, CNG) that obtain + * the EM block via raw RSA (CKM_RSA_X_509 / BCRYPT_PAD_NONE) and need to + * apply the constant-time unpadding in user space. + * + * @param dst destination buffer (resized to @p expected_len) + * @param em EM block as returned by raw RSA decryption + * @param ct original ciphertext (used as input to the synthetic + * plaintext derivation - MUST be the *same* bytes the + * caller decrypted, so that retries on the same + * ciphertext yield the same synthetic output) + * @param synth_seed private-key-derived seed used to make synthetic + * output unpredictable to attackers + * @param expected_len length of plaintext the caller expects to receive + * @return OK on success + */ + static int rsaImplicitRejectFromEM(std::vector& dst, + const std::vector& em, + const std::vector& ct, + const std::vector& synth_seed, + size_t expected_len); + static bool isError(int retval, const char* funcName, const char* file, int line) { if (retval < 1) { diff --git a/cdoc/CryptoBackend.cpp b/cdoc/CryptoBackend.cpp index e8c2cb68..68384289 100644 --- a/cdoc/CryptoBackend.cpp +++ b/cdoc/CryptoBackend.cpp @@ -44,12 +44,50 @@ CryptoBackend::getLastErrorStr(result_t code) const return "Internal error"; } +libcdoc::result_t +CryptoBackend::decryptRSACDoc1(std::vector& dst, + const std::vector& data, + size_t expected_len, + unsigned int idx) +{ + // Default fallback for custom backends that do not implement the + // implicit-rejection countermeasure themselves. We invoke the legacy + // decryptRSA() for PKCS#1 v1.5 unwrap and enforce a fixed plaintext + // length here, so that callers see the same "OK / CRYPTO_ERROR" surface + // as the new path. NOTE: this fallback does NOT add any constant-time + // guarantee on top of whatever the legacy backend provides - custom + // backends that decrypt CDoc1 RSA SHOULD override this method. + int rv = decryptRSA(dst, data, /*oaep=*/false, idx); + if (rv != OK) { + if (!dst.empty()) { + libcdoc::cleanse(dst); + dst.clear(); + } + return rv < 0 ? rv : CRYPTO_ERROR; + } + if (dst.size() != expected_len) { + libcdoc::cleanse(dst); + dst.clear(); + return CRYPTO_ERROR; + } + return OK; +} + libcdoc::result_t CryptoBackend::random(std::vector& dst, unsigned int size) { - dst.resize(size); - int result = RAND_bytes(dst.data(), size); - return (result < 0) ? OPENSSL_ERROR : OK; + // RAND_bytes returns 1 on success, 0 if the PRNG could not gather enough + // entropy, and -1 if the requested method is not supported. Any value + // other than 1 means the buffer must NOT be used as random material. + dst.resize(size); + const int rv = RAND_bytes(dst.data(), size); + if (rv != 1) { + LOG_SSL_ERROR("RAND_bytes"); + libcdoc::cleanse(dst); + dst.clear(); + return OPENSSL_ERROR; + } + return OK; } libcdoc::result_t diff --git a/cdoc/CryptoBackend.h b/cdoc/CryptoBackend.h index 04253a2c..cfbceb75 100644 --- a/cdoc/CryptoBackend.h +++ b/cdoc/CryptoBackend.h @@ -88,6 +88,40 @@ struct CDOC_EXPORT CryptoBackend { * @return error code or OK */ virtual result_t decryptRSA(std::vector& dst, const std::vector& data, bool oaep, unsigned int idx) { return NOT_IMPLEMENTED; }; + + /** + * @brief Decrypt a CDoc1 RSA-PKCS#1-v1.5-wrapped FMK with implicit rejection. + * + * CDoc1 wraps the AES File Master Key with raw RSA PKCS#1 v1.5 (no AES + * Key Wrap), which historically exposed a Bleichenbacher oracle. This + * method is the recommended decryption entry point for CDoc1 RSA recipients. + * + * Implementations MUST apply the implicit-rejection countermeasure + * (RFC 8017 section 7.2.2 / OpenSSL 3.2's + * @c EVP_PKEY_CTX_set_rsa_implicit_rejection): on padding failure they + * MUST return @c OK with a deterministic synthetic plaintext of + * @p expected_len bytes derived from the private key, and otherwise the + * recovered plaintext, indistinguishable from a real one to an + * attacker who does not know the private key. The downstream AES decrypt + * acts as the authentication step that distinguishes a real key from a + * synthetic one. + * + * The default implementation delegates to @ref decryptRSA and enforces + * @p expected_len. Backends provided by libcdoc override it to deliver + * the constant-time guarantee. Custom backends are encouraged to + * override it; the legacy @ref decryptRSA fallback is retained only for + * source compatibility. + * + * @param dst destination buffer (resized to @p expected_len) + * @param data RSA ciphertext (wrapped FMK) + * @param expected_len plaintext (FMK) length the caller expects + * @param idx lock index (0-based) in the container + * @return OK or CRYPTO_ERROR + */ + virtual result_t decryptRSACDoc1(std::vector& dst, + const std::vector& data, + size_t expected_len, + unsigned int idx); /** * @brief Derive key by ConcatKDF algorithm * diff --git a/cdoc/PKCS11Backend.cpp b/cdoc/PKCS11Backend.cpp index e3c67520..7f9fb689 100644 --- a/cdoc/PKCS11Backend.cpp +++ b/cdoc/PKCS11Backend.cpp @@ -411,28 +411,108 @@ libcdoc::PKCS11Backend::getPublicKey(std::vector& val, int slot, const libcdoc::result_t libcdoc::PKCS11Backend::decryptRSA(std::vector &dst, const std::vector &data, bool oaep, unsigned int idx) { - if(!d) return CRYPTO_ERROR; + if(!d) return CRYPTO_ERROR; int result = connectToKey(idx, true); if (result != OK) return result; - CK_RSA_PKCS_OAEP_PARAMS params { CKM_SHA256, CKG_MGF1_SHA256, 0, nullptr, 0 }; - auto mech = oaep ? CK_MECHANISM{ CKM_RSA_PKCS_OAEP, ¶ms, sizeof(params) } : CK_MECHANISM{ CKM_RSA_PKCS, nullptr, 0 }; - if(d->f->C_DecryptInit(d->session, &mech, d->key) != CKR_OK) { - d->logout(); - return CRYPTO_ERROR; - } - CK_ULONG size = 0; - if(d->f->C_Decrypt(d->session, CK_CHAR_PTR(data.data()), CK_ULONG(data.size()), 0, &size) != CKR_OK) { - d->logout(); - return CRYPTO_ERROR; - } - dst.resize(size); - if(d->f->C_Decrypt(d->session, CK_CHAR_PTR(data.data()), CK_ULONG(data.size()), dst.data(), &size) != CKR_OK) return CRYPTO_ERROR; - d->logout(); + CK_RSA_PKCS_OAEP_PARAMS params { CKM_SHA256, CKG_MGF1_SHA256, 0, nullptr, 0 }; + auto mech = oaep ? CK_MECHANISM{ CKM_RSA_PKCS_OAEP, ¶ms, sizeof(params) } : CK_MECHANISM{ CKM_RSA_PKCS, nullptr, 0 }; + if(d->f->C_DecryptInit(d->session, &mech, d->key) != CKR_OK) { + d->logout(); + return CRYPTO_ERROR; + } + CK_ULONG size = 0; + if(d->f->C_Decrypt(d->session, CK_CHAR_PTR(data.data()), CK_ULONG(data.size()), 0, &size) != CKR_OK) { + d->logout(); + return CRYPTO_ERROR; + } + dst.resize(size); + if(d->f->C_Decrypt(d->session, CK_CHAR_PTR(data.data()), CK_ULONG(data.size()), dst.data(), &size) != CKR_OK) { + // Always logout - failing to do so would leak the open session and + // (worse) reveal whether the second C_Decrypt failed for "padding" + // vs another reason via observable side-effects on the next call. + libcdoc::cleanse(dst); + dst.clear(); + d->logout(); + return CRYPTO_ERROR; + } + dst.resize(size); + d->logout(); return OK; } +libcdoc::result_t +libcdoc::PKCS11Backend::decryptRSACDoc1(std::vector &dst, + const std::vector &data, + size_t expected_len, + unsigned int idx) +{ + if(!d) return CRYPTO_ERROR; + if(expected_len == 0) return CRYPTO_ERROR; + + int result = connectToKey(idx, true); + if (result != OK) return result; + + // Use raw RSA (CKM_RSA_X_509) so libcdoc can apply RFC 8017 implicit + // rejection in user space. CKM_RSA_PKCS lets the token strip the + // padding, but most PKCS#11 tokens leak the success/failure bit + // through CKR_ENCRYPTED_DATA_INVALID vs CKR_OK and through the time + // taken; the only portable mitigation is to never let the token see + // the padding decision. CKM_RSA_X_509 is a baseline mechanism + // supported by every PKCS#11 token that supports RSA. + CK_MECHANISM mech { CKM_RSA_X_509, nullptr, 0 }; + if (d->f->C_DecryptInit(d->session, &mech, d->key) != CKR_OK) { + d->logout(); + return CRYPTO_ERROR; + } + + CK_ULONG em_size = 0; + if (d->f->C_Decrypt(d->session, CK_CHAR_PTR(data.data()), CK_ULONG(data.size()), nullptr, &em_size) != CKR_OK) { + d->logout(); + return CRYPTO_ERROR; + } + std::vector em(size_t(em_size), 0); + if (d->f->C_Decrypt(d->session, CK_CHAR_PTR(data.data()), CK_ULONG(data.size()), em.data(), &em_size) != CKR_OK) { + libcdoc::cleanse(em); + d->logout(); + return CRYPTO_ERROR; + } + em.resize(size_t(em_size)); + d->logout(); + + // Build a synthetic seed that does not require access to the private + // key (which never leaves the token). HMAC the ciphertext with a + // public-but-token-bound value (CKA_ID concatenated with the modulus) + // so the seed is stable per (token-key, ct) pair while still being + // unpredictable to attackers. + std::vector seed_key; + { + std::vector id_attr = d->attribute(d->session, d->key, CKA_ID); + std::vector mod_attr = d->attribute(d->session, d->key, CKA_MODULUS); + seed_key.reserve(id_attr.size() + mod_attr.size() + 16); + const std::string_view tag{"cdoc1-rsa-implicit-reject-pkcs11"}; + seed_key.insert(seed_key.end(), tag.begin(), tag.end()); + seed_key.insert(seed_key.end(), id_attr.begin(), id_attr.end()); + seed_key.insert(seed_key.end(), mod_attr.begin(), mod_attr.end()); + } + std::vector prk = libcdoc::Crypto::sign_hmac(seed_key, data); + libcdoc::cleanse(seed_key); + std::vector synth = libcdoc::Crypto::expand( + prk, "cdoc1-rsa-implicit-reject", int(expected_len)); + libcdoc::cleanse(prk); + if (synth.size() != expected_len) { + // Last-resort fallback: fixed zero seed. Worse than ideal but still + // length-uniform with the real-success path. + synth.assign(expected_len, 0); + } + + int rv = libcdoc::Crypto::rsaImplicitRejectFromEM(dst, em, data, synth, expected_len); + libcdoc::cleanse(em); + libcdoc::cleanse(synth); + return rv; +} + libcdoc::result_t libcdoc::PKCS11Backend::deriveECDH1(std::vector& dst, const std::vector &public_key, unsigned int idx) { diff --git a/cdoc/PKCS11Backend.h b/cdoc/PKCS11Backend.h index b0084ad8..1e34a8b4 100644 --- a/cdoc/PKCS11Backend.h +++ b/cdoc/PKCS11Backend.h @@ -149,6 +149,7 @@ struct CDOC_EXPORT PKCS11Backend : public CryptoBackend { virtual result_t deriveECDH1(std::vector& dst, const std::vector &public_key, unsigned int idx) override; virtual result_t decryptRSA(std::vector& dst, const std::vector &data, bool oaep, unsigned int idxl) override; + virtual result_t decryptRSACDoc1(std::vector& dst, const std::vector& data, size_t expected_len, unsigned int idx) override; virtual result_t extractHKDF(std::vector& kek, const std::vector& salt, const std::vector& pw_salt, int32_t kdf_iter, unsigned int idx) override; virtual result_t sign(std::vector& dst, HashAlgorithm algorithm, const std::vector &digest, unsigned int idx) override; private: diff --git a/cdoc/WinBackend.cpp b/cdoc/WinBackend.cpp index fbc8b55a..ee6a51d8 100644 --- a/cdoc/WinBackend.cpp +++ b/cdoc/WinBackend.cpp @@ -19,15 +19,43 @@ #include "WinBackend.h" #include "CDoc2.h" +#include "Crypto.h" #include "Logger.h" #include "Utils.h" +#include "utils/memory.h" #include #include +// Convert a UTF-8 std::string to a std::wstring (UTF-16) using the Windows +// API. The previous implementation zero-extended each byte into a wchar_t, +// which silently mangled any non-ASCII input - in particular non-ASCII PINs +// and key names. A single mis-converted PIN byte is enough to fail +// authentication; on smart cards this consumes a retry slot and can +// permanently lock the card after exhausting the retry counter. static std::wstring toWide(const std::string &in) { - return {in.cbegin(), in.cend()}; + if (in.empty()) return {}; + int needed = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + in.data(), int(in.size()), + nullptr, 0); + if (needed <= 0) { + LOG_ERROR("WinBackend::toWide: invalid UTF-8 input (GetLastError={})", + DWORD(GetLastError())); + return {}; + } + std::wstring out(size_t(needed), L'\0'); + int written = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + in.data(), int(in.size()), + out.data(), needed); + if (written != needed) { + LOG_ERROR("WinBackend::toWide: MultiByteToWideChar mismatch " + "(GetLastError={})", DWORD(GetLastError())); + // Wipe the partially-populated buffer before discarding it. + SecureZeroMemory(out.data(), out.size() * sizeof(wchar_t)); + return {}; + } + return out; } struct libcdoc::WinBackend::Private { @@ -123,16 +151,59 @@ libcdoc::WinBackend::useKey(const std::string& name, const std::string& pin) NCryptFreeObject(d->key); d->key = 0; } + + // Reject invalid UTF-8 in the key name early instead of silently + // truncating it. toWide() returns an empty string both for an empty + // input and for invalid UTF-8 - distinguish the two. + if (name.empty()) { + LOG_ERROR("WinBackend::useKey: empty key name"); + return WRONG_ARGUMENTS; + } std::wstring wname = toWide(name); + if (wname.empty()) { + LOG_ERROR("WinBackend::useKey: invalid UTF-8 in key name"); + return WRONG_ARGUMENTS; + } + SECURITY_STATUS err = NCryptOpenKey(d->prov, &d->key, wname.c_str(), 0, NCRYPT_SILENT_FLAG); - if (err != ERROR_SUCCESS) return CRYPTO_ERROR; - if (!pin.empty()) { - std::wstring wpin = toWide(pin); - err = NCryptSetProperty(d->key, NCRYPT_PIN_PROPERTY, PBYTE(wpin.data()), DWORD(wpin.size()), NCRYPT_SILENT_FLAG); - if (err != ERROR_SUCCESS) { - NCryptFreeObject(d->key); - d->key = 0; - } + if (err != ERROR_SUCCESS) { + LOG_ERROR("WinBackend::useKey: NCryptOpenKey failed (status={:#x})", DWORD(err)); + return CRYPTO_ERROR; + } + + if (pin.empty()) { + return OK; + } + + std::wstring wpin = toWide(pin); + if (wpin.empty()) { + // toWide() already logged the reason. Treat invalid UTF-8 in the PIN + // as a hard failure so we do NOT submit a partial / mangled PIN to + // the card and consume a retry slot. + NCryptFreeObject(d->key); + d->key = 0; + return WRONG_ARGUMENTS; + } + + // NCryptSetProperty(NCRYPT_PIN_PROPERTY) expects cbInput in *bytes*, + // not wide-character count. The previous code passed wpin.size(), which + // is the WCHAR count - i.e. half the actual byte length - so only half + // of the PIN was forwarded to the card. Pass the byte length explicitly, + // and include the trailing NUL since CNG documents the PIN as a + // null-terminated wide string. + const DWORD pin_bytes = DWORD((wpin.size() + 1) * sizeof(wchar_t)); + err = NCryptSetProperty(d->key, NCRYPT_PIN_PROPERTY, + PBYTE(wpin.data()), pin_bytes, + NCRYPT_SILENT_FLAG); + // Wipe the wide PIN buffer regardless of the outcome before discarding + // it. std::wstring::data() is contiguous and writeable since C++17. + SecureZeroMemory(wpin.data(), wpin.size() * sizeof(wchar_t)); + + if (err != ERROR_SUCCESS) { + LOG_ERROR("WinBackend::useKey: NCryptSetProperty(PIN) failed (status={:#x})", DWORD(err)); + NCryptFreeObject(d->key); + d->key = 0; + return CRYPTO_ERROR; } return OK; } @@ -152,10 +223,93 @@ libcdoc::WinBackend::decryptRSA(std::vector& dst, const std::vectorkey, PBYTE(data.data()), DWORD(data.size()), paddingInfo, PBYTE(dst.data()), DWORD(dst.size()), &size, flags); - if (err != ERROR_SUCCESS) return CRYPTO_ERROR; + if (err != ERROR_SUCCESS) { + libcdoc::cleanse(dst); + dst.clear(); + return CRYPTO_ERROR; + } return OK; } +libcdoc::result_t +libcdoc::WinBackend::decryptRSACDoc1(std::vector& dst, + const std::vector& data, + size_t expected_len, + unsigned int idx) +{ + if(!d->prov) return CRYPTO_ERROR; + if(expected_len == 0) return CRYPTO_ERROR; + int result = connectToKey(idx, true); + if (result != OK) return result; + + // Raw RSA decrypt: ask CNG NOT to strip the padding so we can apply the + // implicit-rejection countermeasure in user space. CNG exposes raw + // (textbook) RSA via paddingInfo=NULL and flags=0. + DWORD em_size = 0; + SECURITY_STATUS err = NCryptDecrypt(d->key, + PBYTE(data.data()), DWORD(data.size()), + nullptr, + nullptr, 0, &em_size, 0); + if (err != ERROR_SUCCESS) { + LOG_ERROR("WinBackend::decryptRSACDoc1: NCryptDecrypt(size) failed (status={:#x})", + DWORD(err)); + return CRYPTO_ERROR; + } + std::vector em(em_size, 0); + err = NCryptDecrypt(d->key, + PBYTE(data.data()), DWORD(data.size()), + nullptr, + PBYTE(em.data()), em_size, &em_size, 0); + if (err != ERROR_SUCCESS) { + libcdoc::cleanse(em); + LOG_ERROR("WinBackend::decryptRSACDoc1: NCryptDecrypt failed (status={:#x})", + DWORD(err)); + return CRYPTO_ERROR; + } + em.resize(em_size); + + // Build a stable synthetic seed from a token-bound public value (the + // public key blob) plus the ciphertext. The private key never leaves + // CNG's keystore, but the public modulus is exportable and serves the + // same role of "private to this key, public-immutable" that OpenSSL's + // i2d_PrivateKey gives us elsewhere. + std::vector seed_key; + { + DWORD blob_size = 0; + if (NCryptExportKey(d->key, 0, BCRYPT_RSAPUBLIC_BLOB, nullptr, + nullptr, 0, &blob_size, 0) == ERROR_SUCCESS && + blob_size > 0) { + std::vector blob(blob_size, 0); + if (NCryptExportKey(d->key, 0, BCRYPT_RSAPUBLIC_BLOB, nullptr, + blob.data(), blob_size, &blob_size, 0) == ERROR_SUCCESS) { + blob.resize(blob_size); + const std::string_view tag{"cdoc1-rsa-implicit-reject-cng"}; + seed_key.reserve(tag.size() + blob.size()); + seed_key.insert(seed_key.end(), tag.begin(), tag.end()); + seed_key.insert(seed_key.end(), blob.begin(), blob.end()); + } + } + // If export failed, fall back to a fixed tag - still length-uniform + // but slightly less unpredictable. Better than leaking the failure. + if (seed_key.empty()) { + const std::string_view tag{"cdoc1-rsa-implicit-reject-cng-fallback"}; + seed_key.assign(tag.begin(), tag.end()); + } + } + std::vector prk = libcdoc::Crypto::sign_hmac(seed_key, data); + libcdoc::cleanse(seed_key); + std::vector synth = libcdoc::Crypto::expand( + prk, "cdoc1-rsa-implicit-reject", int(expected_len)); + libcdoc::cleanse(prk); + if (synth.size() != expected_len) + synth.assign(expected_len, 0); + + int rv = libcdoc::Crypto::rsaImplicitRejectFromEM(dst, em, data, synth, expected_len); + libcdoc::cleanse(em); + libcdoc::cleanse(synth); + return rv; +} + libcdoc::result_t libcdoc::WinBackend::deriveConcatKDF(std::vector& dst, const std::vector &public_key, const std::string &digest, const std::vector &algorithm_id, const std::vector &party_uinfo, @@ -263,19 +417,30 @@ libcdoc::WinBackend::sign(std::vector& dst, HashAlgorithm algorithm, co int result = connectToKey(idx, true); if (result != OK) return result; - BCRYPT_PSS_PADDING_INFO rsaPSS { NCRYPT_SHA256_ALGORITHM, 32 }; - switch(algorithm) { - case libcdoc::CryptoBackend::HashAlgorithm::SHA_224: - rsaPSS = { L"SHA224", 24 }; break; - case libcdoc::CryptoBackend::HashAlgorithm::SHA_256: - rsaPSS = { NCRYPT_SHA256_ALGORITHM, 32 }; break; - case libcdoc::CryptoBackend::HashAlgorithm::SHA_384: - rsaPSS = { NCRYPT_SHA384_ALGORITHM, 48 }; break; - case libcdoc::CryptoBackend::HashAlgorithm::SHA_512: - rsaPSS = { NCRYPT_SHA256_ALGORITHM, 64 }; break; - default: + // BCRYPT_PSS_PADDING_INFO::pszAlgId selects BOTH the PSS hash and MGF1 + // hash. It must match the hash that produced `digest`, otherwise CNG + // either rejects the call (when the digest length disagrees with the + // declared hash) or silently produces a signature that no verifier + // will accept. Salt length conventionally equals the hash output size. + // + // Note: CNG (BCrypt/NCrypt) does not expose a SHA-224 PSS algorithm + // identifier, so SHA-224 is rejected here rather than silently + // forwarded with a non-standard string. + BCRYPT_PSS_PADDING_INFO rsaPSS { BCRYPT_SHA256_ALGORITHM, 32 }; + switch(algorithm) { + case libcdoc::CryptoBackend::HashAlgorithm::SHA_256: + rsaPSS = { BCRYPT_SHA256_ALGORITHM, 32 }; break; + case libcdoc::CryptoBackend::HashAlgorithm::SHA_384: + rsaPSS = { BCRYPT_SHA384_ALGORITHM, 48 }; break; + case libcdoc::CryptoBackend::HashAlgorithm::SHA_512: + rsaPSS = { BCRYPT_SHA512_ALGORITHM, 64 }; break; + case libcdoc::CryptoBackend::HashAlgorithm::SHA_224: + // SHA-224 is not supported by CNG's RSA-PSS implementation. + LOG_ERROR("WinBackend: RSA-PSS with SHA-224 is not supported by CNG"); + return NOT_IMPLEMENTED; + default: return INVALID_PARAMS; - } + } BCRYPT_PKCS1_PADDING_INFO rsaPKCS1 { rsaPSS.pszAlgId }; DWORD size; NCryptGetProperty(d->key, NCRYPT_ALGORITHM_GROUP_PROPERTY, nullptr, 0, &size, 0); diff --git a/cdoc/WinBackend.h b/cdoc/WinBackend.h index 5cd5851c..994d35eb 100644 --- a/cdoc/WinBackend.h +++ b/cdoc/WinBackend.h @@ -63,6 +63,7 @@ struct CDOC_EXPORT WinBackend : public CryptoBackend { virtual result_t usePSS(int idx) {return true;} virtual result_t decryptRSA(std::vector& dst, const std::vector& data, bool oaep, unsigned int idx); + virtual result_t decryptRSACDoc1(std::vector& dst, const std::vector& data, size_t expected_len, unsigned int idx) override; virtual result_t deriveConcatKDF(std::vector& dst, const std::vector &public_key, const std::string &digest, const std::vector &algorithm_id, const std::vector &party_uinfo, const std::vector &party_vinfo, unsigned int idx); diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index d629034d..d635d3e9 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -678,7 +678,7 @@ int main(int argc, char *argv[]) return 1; } - libcdoc::setLogLevel(LEVEL_TRACE); + libcdoc::setLogLevel(LEVEL_WARNING); string_view command(argv[1]); From 7e6cdaaf424986648201b523ec84522b43b2d09a Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Mon, 1 Jun 2026 14:07:03 +0300 Subject: [PATCH 26/65] Moved fix to main decryptRSA method --- cdoc/CDoc1Reader.cpp | 9 +-- cdoc/CDocCipher.cpp | 34 ++++------- cdoc/CryptoBackend.cpp | 29 --------- cdoc/CryptoBackend.h | 47 ++++----------- cdoc/PKCS11Backend.cpp | 131 +++++++++++++++++++---------------------- cdoc/PKCS11Backend.h | 1 - 6 files changed, 89 insertions(+), 162 deletions(-) diff --git a/cdoc/CDoc1Reader.cpp b/cdoc/CDoc1Reader.cpp index 94b97e32..4f25e25b 100644 --- a/cdoc/CDoc1Reader.cpp +++ b/cdoc/CDoc1Reader.cpp @@ -147,10 +147,11 @@ CDoc1Reader::getFMK(std::vector& fmk, unsigned int lock_idx) constexpr auto FAIL_MSG = "Failed to derive FMK"; if (lock.isRSA()) { - // Implicit-rejection-aware decrypt. Returns OK with synthetic - // bytes on padding failure; only a fundamental error (e.g. ct size - // mismatch with modulus) yields a non-OK result. - int result = crypto->decryptRSACDoc1(fmk, lock.encrypted_fmk, expected_fmk_len, lock_idx); + // If OAEP = false and and fmk.size() != 0, the decryptRSA always + // returns OK with synthetic bytes on padding failure; only a + // fundamental error (e.g. ct size mismatch with modulus) yields a non-OK result. + fmk.resize(expected_fmk_len); + int result = crypto->decryptRSA(fmk, lock.encrypted_fmk, false, lock_idx); if (result != libcdoc::OK) { libcdoc::cleanse(fmk); fmk.clear(); diff --git a/cdoc/CDocCipher.cpp b/cdoc/CDocCipher.cpp index 581a8937..6e16ec1d 100644 --- a/cdoc/CDocCipher.cpp +++ b/cdoc/CDocCipher.cpp @@ -110,19 +110,28 @@ struct ToolCrypto : public libcdoc::CryptoBackend { libcdoc::result_t decryptRSA(std::vector& dst, const std::vector &data, bool oaep, unsigned int idx) override final { if (p11) return p11->decryptRSA(dst, data, oaep, idx); + if (auto rv = validateRcptIdx(rcpts, idx); rv != libcdoc::OK) return rv; const libcdoc::RcptInfo& rcpt = rcpts[idx]; if (rcpt.secret.empty()) return libcdoc::CRYPTO_ERROR; + const uint8_t *p = rcpt.secret.data(); + auto key = make_unique_ptr(d2i_PrivateKey(EVP_PKEY_RSA, nullptr, &p, rcpt.secret.size())); + if (!key) return libcdoc::CRYPTO_ERROR; + // Note: EVP_PKEY_* functions return 1 on success, 0 on a (possibly // recoverable) failure such as RSA padding mismatch, and a negative // value on fatal errors. Anything other than 1 must be treated as // failure - returning 0 as success would leak partial/garbage // plaintext and create a Bleichenbacher-style padding oracle for // PKCS#1 v1.5 (CDoc1) decryption. - const uint8_t *p = rcpt.secret.data(); - auto key = make_unique_ptr(d2i_PrivateKey(EVP_PKEY_RSA, nullptr, &p, rcpt.secret.size())); - if (!key) return libcdoc::CRYPTO_ERROR; + + if (!oaep && !dst.empty()) { + // Implicit-rejection-aware decrypt. Returns OK on padding success + // AND on padding failure (with synthetic output). Only fatal errors + // (e.g. ct size mismatch with modulus) are surfaced as CRYPTO_ERROR. + return libcdoc::Crypto::decryptRSAv15_implicitReject(dst, key.get(), data, dst.size()); + } auto ctx = make_unique_ptr(EVP_PKEY_CTX_new(key.get(), nullptr)); if (!ctx) return libcdoc::CRYPTO_ERROR; @@ -157,25 +166,6 @@ struct ToolCrypto : public libcdoc::CryptoBackend { return libcdoc::OK; } - libcdoc::result_t decryptRSACDoc1(std::vector& dst, - const std::vector &data, - size_t expected_len, - unsigned int idx) override final { - if (p11) return p11->decryptRSACDoc1(dst, data, expected_len, idx); - if (auto rv = validateRcptIdx(rcpts, idx); rv != libcdoc::OK) return rv; - const libcdoc::RcptInfo& rcpt = rcpts[idx]; - if (rcpt.secret.empty()) return libcdoc::CRYPTO_ERROR; - - const uint8_t *p = rcpt.secret.data(); - auto key = make_unique_ptr(d2i_PrivateKey(EVP_PKEY_RSA, nullptr, &p, rcpt.secret.size())); - if (!key) return libcdoc::CRYPTO_ERROR; - - // Implicit-rejection-aware decrypt. Returns OK on padding success - // AND on padding failure (with synthetic output). Only fatal errors - // (e.g. ct size mismatch with modulus) are surfaced as CRYPTO_ERROR. - return libcdoc::Crypto::decryptRSAv15_implicitReject(dst, key.get(), data, expected_len); - } - libcdoc::result_t deriveECDH1(std::vector& dst, const std::vector &public_key, unsigned int idx) override final { if (auto rv = validateRcptIdx(rcpts, idx); rv != libcdoc::OK) return rv; const libcdoc::RcptInfo& rcpt = rcpts[idx]; diff --git a/cdoc/CryptoBackend.cpp b/cdoc/CryptoBackend.cpp index 68384289..d48ce72f 100644 --- a/cdoc/CryptoBackend.cpp +++ b/cdoc/CryptoBackend.cpp @@ -44,35 +44,6 @@ CryptoBackend::getLastErrorStr(result_t code) const return "Internal error"; } -libcdoc::result_t -CryptoBackend::decryptRSACDoc1(std::vector& dst, - const std::vector& data, - size_t expected_len, - unsigned int idx) -{ - // Default fallback for custom backends that do not implement the - // implicit-rejection countermeasure themselves. We invoke the legacy - // decryptRSA() for PKCS#1 v1.5 unwrap and enforce a fixed plaintext - // length here, so that callers see the same "OK / CRYPTO_ERROR" surface - // as the new path. NOTE: this fallback does NOT add any constant-time - // guarantee on top of whatever the legacy backend provides - custom - // backends that decrypt CDoc1 RSA SHOULD override this method. - int rv = decryptRSA(dst, data, /*oaep=*/false, idx); - if (rv != OK) { - if (!dst.empty()) { - libcdoc::cleanse(dst); - dst.clear(); - } - return rv < 0 ? rv : CRYPTO_ERROR; - } - if (dst.size() != expected_len) { - libcdoc::cleanse(dst); - dst.clear(); - return CRYPTO_ERROR; - } - return OK; -} - libcdoc::result_t CryptoBackend::random(std::vector& dst, unsigned int size) { diff --git a/cdoc/CryptoBackend.h b/cdoc/CryptoBackend.h index cfbceb75..6ade5592 100644 --- a/cdoc/CryptoBackend.h +++ b/cdoc/CryptoBackend.h @@ -80,48 +80,25 @@ struct CDOC_EXPORT CryptoBackend { */ virtual result_t deriveECDH1(std::vector& dst, const std::vector &public_key, unsigned int idx) { return NOT_IMPLEMENTED; } /** - * @brief decryptRSA + * @brief decrypt RSA ciphertext + * + * If oaep = false and dst.size() != 0, the method MUST always return OK with synthetic bytes on padding failure; only a fundamental + * error (e.g. ct size mismatch with modulus) yields a non-OK result. + * + * Implementations MUST apply the implicit-rejection countermeasure (RFC 8017 section 7.2.2 / OpenSSL 3.2's + * @c EVP_PKEY_CTX_set_rsa_implicit_rejection): on padding failure they MUST return @c OK with a deterministic synthetic plaintext of + * @p expected_len bytes derived from the private key, and otherwise the recovered plaintext, indistinguishable from a real one to an + * attacker who does not know the private key. The downstream AES decrypt acts as the authentication step that distinguishes a real key from a + * synthetic one. + * * @param dst the destination container for decrypted data - * @param data encrypted data + * @param data RSA ciphertext (wrapped FMK) * @param oaep use OAEP padding * @param idx lock index (0-based) in container * @return error code or OK */ virtual result_t decryptRSA(std::vector& dst, const std::vector& data, bool oaep, unsigned int idx) { return NOT_IMPLEMENTED; }; - /** - * @brief Decrypt a CDoc1 RSA-PKCS#1-v1.5-wrapped FMK with implicit rejection. - * - * CDoc1 wraps the AES File Master Key with raw RSA PKCS#1 v1.5 (no AES - * Key Wrap), which historically exposed a Bleichenbacher oracle. This - * method is the recommended decryption entry point for CDoc1 RSA recipients. - * - * Implementations MUST apply the implicit-rejection countermeasure - * (RFC 8017 section 7.2.2 / OpenSSL 3.2's - * @c EVP_PKEY_CTX_set_rsa_implicit_rejection): on padding failure they - * MUST return @c OK with a deterministic synthetic plaintext of - * @p expected_len bytes derived from the private key, and otherwise the - * recovered plaintext, indistinguishable from a real one to an - * attacker who does not know the private key. The downstream AES decrypt - * acts as the authentication step that distinguishes a real key from a - * synthetic one. - * - * The default implementation delegates to @ref decryptRSA and enforces - * @p expected_len. Backends provided by libcdoc override it to deliver - * the constant-time guarantee. Custom backends are encouraged to - * override it; the legacy @ref decryptRSA fallback is retained only for - * source compatibility. - * - * @param dst destination buffer (resized to @p expected_len) - * @param data RSA ciphertext (wrapped FMK) - * @param expected_len plaintext (FMK) length the caller expects - * @param idx lock index (0-based) in the container - * @return OK or CRYPTO_ERROR - */ - virtual result_t decryptRSACDoc1(std::vector& dst, - const std::vector& data, - size_t expected_len, - unsigned int idx); /** * @brief Derive key by ConcatKDF algorithm * diff --git a/cdoc/PKCS11Backend.cpp b/cdoc/PKCS11Backend.cpp index 7f9fb689..b02e0ddc 100644 --- a/cdoc/PKCS11Backend.cpp +++ b/cdoc/PKCS11Backend.cpp @@ -416,6 +416,66 @@ libcdoc::PKCS11Backend::decryptRSA(std::vector &dst, const std::vector< int result = connectToKey(idx, true); if (result != OK) return result; + if (!oaep && !dst.empty()) { + // Use raw RSA (CKM_RSA_X_509) so libcdoc can apply RFC 8017 implicit + // rejection in user space. CKM_RSA_PKCS lets the token strip the + // padding, but most PKCS#11 tokens leak the success/failure bit + // through CKR_ENCRYPTED_DATA_INVALID vs CKR_OK and through the time + // taken; the only portable mitigation is to never let the token see + // the padding decision. CKM_RSA_X_509 is a baseline mechanism + // supported by every PKCS#11 token that supports RSA. + CK_MECHANISM mech { CKM_RSA_X_509, nullptr, 0 }; + if (d->f->C_DecryptInit(d->session, &mech, d->key) != CKR_OK) { + d->logout(); + return CRYPTO_ERROR; + } + + CK_ULONG em_size = 0; + if (d->f->C_Decrypt(d->session, CK_CHAR_PTR(data.data()), CK_ULONG(data.size()), nullptr, &em_size) != CKR_OK) { + d->logout(); + return CRYPTO_ERROR; + } + std::vector em(size_t(em_size), 0); + if (d->f->C_Decrypt(d->session, CK_CHAR_PTR(data.data()), CK_ULONG(data.size()), em.data(), &em_size) != CKR_OK) { + libcdoc::cleanse(em); + d->logout(); + return CRYPTO_ERROR; + } + em.resize(size_t(em_size)); + d->logout(); + + // Build a synthetic seed that does not require access to the private + // key (which never leaves the token). HMAC the ciphertext with a + // public-but-token-bound value (CKA_ID concatenated with the modulus) + // so the seed is stable per (token-key, ct) pair while still being + // unpredictable to attackers. + std::vector seed_key; + { + std::vector id_attr = d->attribute(d->session, d->key, CKA_ID); + std::vector mod_attr = d->attribute(d->session, d->key, CKA_MODULUS); + seed_key.reserve(id_attr.size() + mod_attr.size() + 16); + const std::string_view tag{"cdoc1-rsa-implicit-reject-pkcs11"}; + seed_key.insert(seed_key.end(), tag.begin(), tag.end()); + seed_key.insert(seed_key.end(), id_attr.begin(), id_attr.end()); + seed_key.insert(seed_key.end(), mod_attr.begin(), mod_attr.end()); + } + std::vector prk = libcdoc::Crypto::sign_hmac(seed_key, data); + libcdoc::cleanse(seed_key); + std::vector synth = libcdoc::Crypto::expand( + prk, "cdoc1-rsa-implicit-reject", int(dst.size())); + libcdoc::cleanse(prk); + if (synth.size() != dst.size()) { + // Last-resort fallback: fixed zero seed. Worse than ideal but still + // length-uniform with the real-success path. + synth.assign(dst.size(), 0); + } + + int rv = libcdoc::Crypto::rsaImplicitRejectFromEM(dst, em, data, synth, dst.size()); + libcdoc::cleanse(em); + libcdoc::cleanse(synth); + return rv; + } + CK_RSA_PKCS_OAEP_PARAMS params { CKM_SHA256, CKG_MGF1_SHA256, 0, nullptr, 0 }; auto mech = oaep ? CK_MECHANISM{ CKM_RSA_PKCS_OAEP, ¶ms, sizeof(params) } : CK_MECHANISM{ CKM_RSA_PKCS, nullptr, 0 }; if(d->f->C_DecryptInit(d->session, &mech, d->key) != CKR_OK) { @@ -442,77 +502,6 @@ libcdoc::PKCS11Backend::decryptRSA(std::vector &dst, const std::vector< return OK; } -libcdoc::result_t -libcdoc::PKCS11Backend::decryptRSACDoc1(std::vector &dst, - const std::vector &data, - size_t expected_len, - unsigned int idx) -{ - if(!d) return CRYPTO_ERROR; - if(expected_len == 0) return CRYPTO_ERROR; - - int result = connectToKey(idx, true); - if (result != OK) return result; - - // Use raw RSA (CKM_RSA_X_509) so libcdoc can apply RFC 8017 implicit - // rejection in user space. CKM_RSA_PKCS lets the token strip the - // padding, but most PKCS#11 tokens leak the success/failure bit - // through CKR_ENCRYPTED_DATA_INVALID vs CKR_OK and through the time - // taken; the only portable mitigation is to never let the token see - // the padding decision. CKM_RSA_X_509 is a baseline mechanism - // supported by every PKCS#11 token that supports RSA. - CK_MECHANISM mech { CKM_RSA_X_509, nullptr, 0 }; - if (d->f->C_DecryptInit(d->session, &mech, d->key) != CKR_OK) { - d->logout(); - return CRYPTO_ERROR; - } - - CK_ULONG em_size = 0; - if (d->f->C_Decrypt(d->session, CK_CHAR_PTR(data.data()), CK_ULONG(data.size()), nullptr, &em_size) != CKR_OK) { - d->logout(); - return CRYPTO_ERROR; - } - std::vector em(size_t(em_size), 0); - if (d->f->C_Decrypt(d->session, CK_CHAR_PTR(data.data()), CK_ULONG(data.size()), em.data(), &em_size) != CKR_OK) { - libcdoc::cleanse(em); - d->logout(); - return CRYPTO_ERROR; - } - em.resize(size_t(em_size)); - d->logout(); - - // Build a synthetic seed that does not require access to the private - // key (which never leaves the token). HMAC the ciphertext with a - // public-but-token-bound value (CKA_ID concatenated with the modulus) - // so the seed is stable per (token-key, ct) pair while still being - // unpredictable to attackers. - std::vector seed_key; - { - std::vector id_attr = d->attribute(d->session, d->key, CKA_ID); - std::vector mod_attr = d->attribute(d->session, d->key, CKA_MODULUS); - seed_key.reserve(id_attr.size() + mod_attr.size() + 16); - const std::string_view tag{"cdoc1-rsa-implicit-reject-pkcs11"}; - seed_key.insert(seed_key.end(), tag.begin(), tag.end()); - seed_key.insert(seed_key.end(), id_attr.begin(), id_attr.end()); - seed_key.insert(seed_key.end(), mod_attr.begin(), mod_attr.end()); - } - std::vector prk = libcdoc::Crypto::sign_hmac(seed_key, data); - libcdoc::cleanse(seed_key); - std::vector synth = libcdoc::Crypto::expand( - prk, "cdoc1-rsa-implicit-reject", int(expected_len)); - libcdoc::cleanse(prk); - if (synth.size() != expected_len) { - // Last-resort fallback: fixed zero seed. Worse than ideal but still - // length-uniform with the real-success path. - synth.assign(expected_len, 0); - } - - int rv = libcdoc::Crypto::rsaImplicitRejectFromEM(dst, em, data, synth, expected_len); - libcdoc::cleanse(em); - libcdoc::cleanse(synth); - return rv; -} - libcdoc::result_t libcdoc::PKCS11Backend::deriveECDH1(std::vector& dst, const std::vector &public_key, unsigned int idx) { diff --git a/cdoc/PKCS11Backend.h b/cdoc/PKCS11Backend.h index 1e34a8b4..b0084ad8 100644 --- a/cdoc/PKCS11Backend.h +++ b/cdoc/PKCS11Backend.h @@ -149,7 +149,6 @@ struct CDOC_EXPORT PKCS11Backend : public CryptoBackend { virtual result_t deriveECDH1(std::vector& dst, const std::vector &public_key, unsigned int idx) override; virtual result_t decryptRSA(std::vector& dst, const std::vector &data, bool oaep, unsigned int idxl) override; - virtual result_t decryptRSACDoc1(std::vector& dst, const std::vector& data, size_t expected_len, unsigned int idx) override; virtual result_t extractHKDF(std::vector& kek, const std::vector& salt, const std::vector& pw_salt, int32_t kdf_iter, unsigned int idx) override; virtual result_t sign(std::vector& dst, HashAlgorithm algorithm, const std::vector &digest, unsigned int idx) override; private: From f01510a044a8031a9f9e2a86b233a6430dd7d58a Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Mon, 1 Jun 2026 15:26:42 +0300 Subject: [PATCH 27/65] Added ct.h --- cdoc/utils/ct.h | 72 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 cdoc/utils/ct.h diff --git a/cdoc/utils/ct.h b/cdoc/utils/ct.h new file mode 100644 index 00000000..2106100a --- /dev/null +++ b/cdoc/utils/ct.h @@ -0,0 +1,72 @@ +/* + * libcdoc + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include + +// Branch-free, data-independent helpers used by the constant-time PKCS#1 v1.5 +// unpadding implementation. +// +// The compiler is allowed - in principle - to "optimise" any of these into +// branches; in practice, on GCC/Clang/MSVC at any reasonable optimisation +// level, none of them produce conditional jumps because the inputs are +// integer expressions without short-circuit operators. We rely on this +// observation, periodically verify it via dudect-style timing tests, and +// avoid further hardening (assembly, OPENSSL_cleanse-style barriers) to keep +// the code portable across the platforms libcdoc targets. + +namespace libcdoc::ct { + +// Returns 0xFF when a == b, otherwise 0x00. Branch-free for 8-bit inputs. +constexpr uint8_t eq8(uint8_t a, uint8_t b) noexcept { + // x is 0 iff a == b; otherwise 1..255. Subtracting 1 underflows to a + // very large value when x == 0, so the high byte of (x - 1) is 0xFF + // exactly when a == b. + uint16_t x = uint16_t(a ^ b); + return uint8_t(((uint32_t(x) - 1u) >> 8) & 0xFFu); +} + +// Returns 0xFF when a >= b, otherwise 0x00. Branch-free for size_t inputs. +constexpr uint8_t ge_size(size_t a, size_t b) noexcept { + // (b - a - 1) wraps to a huge value when a >= b, putting 1 in the top + // bit. We sample the top bit, invert, and broadcast to a byte. + constexpr size_t shift = sizeof(size_t) * 8u - 1u; + size_t top_bit = (b - a - 1u) >> shift; // 1 if a < b, 0 if a >= b + return uint8_t((top_bit ^ 1u) * 0xFFu); +} + +// Returns 0xFF when a == b, otherwise 0x00 (32-bit operands). +constexpr uint8_t eq32(uint32_t a, uint32_t b) noexcept { + uint32_t x = a ^ b; + // (x - 1) >> 31 is 1 iff x == 0 + return uint8_t(((x - 1u) >> 31) & 1u) * 0xFFu; +} + +// Constant-time conditional-copy: out[i] = mask ? a[i] : b[i] for n bytes. +// `mask` must be 0x00 or 0xFF. +inline void cmov(uint8_t *out, const uint8_t *a, const uint8_t *b, + size_t n, uint8_t mask) noexcept { + const uint8_t inv = uint8_t(~mask); + for (size_t i = 0; i < n; ++i) { + out[i] = uint8_t((a[i] & mask) | (b[i] & inv)); + } +} + +} // namespace libcdoc::ct From 0aec4a1d7d987e6774867f02e41323da71742638 Mon Sep 17 00:00:00 2001 From: lauris71 Date: Mon, 1 Jun 2026 15:46:48 +0300 Subject: [PATCH 28/65] Bleichenbacher fix for NCrypt backend --- cdoc/WinBackend.cpp | 141 +++++++++++++++++++------------------------- cdoc/WinBackend.h | 1 - 2 files changed, 62 insertions(+), 80 deletions(-) diff --git a/cdoc/WinBackend.cpp b/cdoc/WinBackend.cpp index ee6a51d8..c3a6d881 100644 --- a/cdoc/WinBackend.cpp +++ b/cdoc/WinBackend.cpp @@ -215,6 +215,68 @@ libcdoc::WinBackend::decryptRSA(std::vector& dst, const std::vectorkey, PBYTE(data.data()), DWORD(data.size()), nullptr, nullptr, 0, &em_size, 0); + if (err != ERROR_SUCCESS) { + LOG_ERROR("WinBackend::decryptRSACDoc1: NCryptDecrypt(size) failed (status={:#x})", DWORD(err)); + return CRYPTO_ERROR; + } + std::vector em(em_size, 0); + err = NCryptDecrypt(d->key, PBYTE(data.data()), DWORD(data.size()), nullptr, PBYTE(em.data()), em_size, &em_size, 0); + if (err != ERROR_SUCCESS) { + libcdoc::cleanse(em); + LOG_ERROR("WinBackend::decryptRSACDoc1: NCryptDecrypt failed (status={:#x})", DWORD(err)); + return CRYPTO_ERROR; + } + em.resize(em_size); + + // Build a stable synthetic seed from a token-bound public value (the + // public key blob) plus the ciphertext. The private key never leaves + // CNG's keystore, but the public modulus is exportable and serves the + // same role of "private to this key, public-immutable" that OpenSSL's + // i2d_PrivateKey gives us elsewhere. + std::vector seed_key; + { + DWORD blob_size = 0; + if (NCryptExportKey(d->key, 0, BCRYPT_RSAPUBLIC_BLOB, nullptr, nullptr, 0, &blob_size, 0) == ERROR_SUCCESS && + blob_size > 0) { + std::vector blob(blob_size, 0); + if (NCryptExportKey(d->key, 0, BCRYPT_RSAPUBLIC_BLOB, nullptr, blob.data(), blob_size, &blob_size, 0) == ERROR_SUCCESS) { + blob.resize(blob_size); + const std::string_view tag{"cdoc1-rsa-implicit-reject-cng"}; + seed_key.reserve(tag.size() + blob.size()); + seed_key.insert(seed_key.end(), tag.begin(), tag.end()); + seed_key.insert(seed_key.end(), blob.begin(), blob.end()); + } + } + // If export failed, fall back to a fixed tag - still length-uniform + // but slightly less unpredictable. Better than leaking the failure. + if (seed_key.empty()) { + const std::string_view tag{"cdoc1-rsa-implicit-reject-cng-fallback"}; + seed_key.assign(tag.begin(), tag.end()); + } + } + std::vector prk = libcdoc::Crypto::sign_hmac(seed_key, data); + libcdoc::cleanse(seed_key); + std::vector synth = libcdoc::Crypto::expand(prk, "cdoc1-rsa-implicit-reject", int(dst.size())); + libcdoc::cleanse(prk); + if (synth.size() != dst.size()) + synth.assign(dst.size(), 0); + + int rv = libcdoc::Crypto::rsaImplicitRejectFromEM(dst, em, data, synth, dst.size()); + libcdoc::cleanse(em); + libcdoc::cleanse(synth); + return rv; + } + // With oaep == true CNG will apply OAEP padding and the implicit-rejection countermeasure internally, + // so we can just call NCryptDecrypt directly with the right flags. BCRYPT_OAEP_PADDING_INFO padding {BCRYPT_SHA256_ALGORITHM, nullptr, 0}; PVOID paddingInfo = oaep ? &padding : nullptr; DWORD flags = oaep ? NCRYPT_PAD_OAEP_FLAG : NCRYPT_PAD_PKCS1_FLAG; @@ -231,85 +293,6 @@ libcdoc::WinBackend::decryptRSA(std::vector& dst, const std::vector& dst, - const std::vector& data, - size_t expected_len, - unsigned int idx) -{ - if(!d->prov) return CRYPTO_ERROR; - if(expected_len == 0) return CRYPTO_ERROR; - int result = connectToKey(idx, true); - if (result != OK) return result; - - // Raw RSA decrypt: ask CNG NOT to strip the padding so we can apply the - // implicit-rejection countermeasure in user space. CNG exposes raw - // (textbook) RSA via paddingInfo=NULL and flags=0. - DWORD em_size = 0; - SECURITY_STATUS err = NCryptDecrypt(d->key, - PBYTE(data.data()), DWORD(data.size()), - nullptr, - nullptr, 0, &em_size, 0); - if (err != ERROR_SUCCESS) { - LOG_ERROR("WinBackend::decryptRSACDoc1: NCryptDecrypt(size) failed (status={:#x})", - DWORD(err)); - return CRYPTO_ERROR; - } - std::vector em(em_size, 0); - err = NCryptDecrypt(d->key, - PBYTE(data.data()), DWORD(data.size()), - nullptr, - PBYTE(em.data()), em_size, &em_size, 0); - if (err != ERROR_SUCCESS) { - libcdoc::cleanse(em); - LOG_ERROR("WinBackend::decryptRSACDoc1: NCryptDecrypt failed (status={:#x})", - DWORD(err)); - return CRYPTO_ERROR; - } - em.resize(em_size); - - // Build a stable synthetic seed from a token-bound public value (the - // public key blob) plus the ciphertext. The private key never leaves - // CNG's keystore, but the public modulus is exportable and serves the - // same role of "private to this key, public-immutable" that OpenSSL's - // i2d_PrivateKey gives us elsewhere. - std::vector seed_key; - { - DWORD blob_size = 0; - if (NCryptExportKey(d->key, 0, BCRYPT_RSAPUBLIC_BLOB, nullptr, - nullptr, 0, &blob_size, 0) == ERROR_SUCCESS && - blob_size > 0) { - std::vector blob(blob_size, 0); - if (NCryptExportKey(d->key, 0, BCRYPT_RSAPUBLIC_BLOB, nullptr, - blob.data(), blob_size, &blob_size, 0) == ERROR_SUCCESS) { - blob.resize(blob_size); - const std::string_view tag{"cdoc1-rsa-implicit-reject-cng"}; - seed_key.reserve(tag.size() + blob.size()); - seed_key.insert(seed_key.end(), tag.begin(), tag.end()); - seed_key.insert(seed_key.end(), blob.begin(), blob.end()); - } - } - // If export failed, fall back to a fixed tag - still length-uniform - // but slightly less unpredictable. Better than leaking the failure. - if (seed_key.empty()) { - const std::string_view tag{"cdoc1-rsa-implicit-reject-cng-fallback"}; - seed_key.assign(tag.begin(), tag.end()); - } - } - std::vector prk = libcdoc::Crypto::sign_hmac(seed_key, data); - libcdoc::cleanse(seed_key); - std::vector synth = libcdoc::Crypto::expand( - prk, "cdoc1-rsa-implicit-reject", int(expected_len)); - libcdoc::cleanse(prk); - if (synth.size() != expected_len) - synth.assign(expected_len, 0); - - int rv = libcdoc::Crypto::rsaImplicitRejectFromEM(dst, em, data, synth, expected_len); - libcdoc::cleanse(em); - libcdoc::cleanse(synth); - return rv; -} - libcdoc::result_t libcdoc::WinBackend::deriveConcatKDF(std::vector& dst, const std::vector &public_key, const std::string &digest, const std::vector &algorithm_id, const std::vector &party_uinfo, diff --git a/cdoc/WinBackend.h b/cdoc/WinBackend.h index 994d35eb..5cd5851c 100644 --- a/cdoc/WinBackend.h +++ b/cdoc/WinBackend.h @@ -63,7 +63,6 @@ struct CDOC_EXPORT WinBackend : public CryptoBackend { virtual result_t usePSS(int idx) {return true;} virtual result_t decryptRSA(std::vector& dst, const std::vector& data, bool oaep, unsigned int idx); - virtual result_t decryptRSACDoc1(std::vector& dst, const std::vector& data, size_t expected_len, unsigned int idx) override; virtual result_t deriveConcatKDF(std::vector& dst, const std::vector &public_key, const std::string &digest, const std::vector &algorithm_id, const std::vector &party_uinfo, const std::vector &party_vinfo, unsigned int idx); From 6f3776151221d055eb1d35a6f9607bd5d689568c Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Mon, 1 Jun 2026 16:35:54 +0300 Subject: [PATCH 29/65] Some cleanups --- cdoc/CDocCipher.cpp | 8 +++++++- cdoc/CryptoBackend.h | 15 ++++++--------- cdoc/PKCS11Backend.cpp | 8 +++++++- cdoc/WinBackend.cpp | 9 ++++++--- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/cdoc/CDocCipher.cpp b/cdoc/CDocCipher.cpp index 6e16ec1d..c4e13f09 100644 --- a/cdoc/CDocCipher.cpp +++ b/cdoc/CDocCipher.cpp @@ -126,7 +126,13 @@ struct ToolCrypto : public libcdoc::CryptoBackend { // plaintext and create a Bleichenbacher-style padding oracle for // PKCS#1 v1.5 (CDoc1) decryption. - if (!oaep && !dst.empty()) { + if (!oaep) { + // If oaep is false, dst must be pre-allocated to the expected length. + // This is required to apply the implicit-rejection countermeasure on padding failure. + if (dst.empty()) { + LOG_ERROR("ToolCrypto::decryptRSA: dst must be pre-allocated for PKCS#1 v1.5 decryption"); + return libcdoc::CRYPTO_ERROR; + } // Implicit-rejection-aware decrypt. Returns OK on padding success // AND on padding failure (with synthetic output). Only fatal errors // (e.g. ct size mismatch with modulus) are surfaced as CRYPTO_ERROR. diff --git a/cdoc/CryptoBackend.h b/cdoc/CryptoBackend.h index 6ade5592..d3ce3c62 100644 --- a/cdoc/CryptoBackend.h +++ b/cdoc/CryptoBackend.h @@ -82,16 +82,13 @@ struct CDOC_EXPORT CryptoBackend { /** * @brief decrypt RSA ciphertext * - * If oaep = false and dst.size() != 0, the method MUST always return OK with synthetic bytes on padding failure; only a fundamental - * error (e.g. ct size mismatch with modulus) yields a non-OK result. - * - * Implementations MUST apply the implicit-rejection countermeasure (RFC 8017 section 7.2.2 / OpenSSL 3.2's - * @c EVP_PKEY_CTX_set_rsa_implicit_rejection): on padding failure they MUST return @c OK with a deterministic synthetic plaintext of - * @p expected_len bytes derived from the private key, and otherwise the recovered plaintext, indistinguishable from a real one to an - * attacker who does not know the private key. The downstream AES decrypt acts as the authentication step that distinguishes a real key from a - * synthetic one. + * If @c oaep == false the implementations MUST apply the implicit-rejection countermeasure (RFC 8017 section 7.2.2 / OpenSSL 3.2's + * @c EVP_PKEY_CTX_set_rsa_implicit_rejection): on padding failure they MUST return @c OK with @c dst filled with deterministic synthetic + * plaintext derived from the private key, and otherwise the recovered plaintext, both indistinguishable to an attacker who does not know the + * private key. The downstream AES decrypt acts as the authentication step that distinguishes a real key from a + * synthetic one. The @c dst has to be pre-allocated to the expected plaintext length by caller. * - * @param dst the destination container for decrypted data + * @param dst the destination container for decrypted data (has to be pre-allocated if @c oaep == false) * @param data RSA ciphertext (wrapped FMK) * @param oaep use OAEP padding * @param idx lock index (0-based) in container diff --git a/cdoc/PKCS11Backend.cpp b/cdoc/PKCS11Backend.cpp index b02e0ddc..eabe3cb1 100644 --- a/cdoc/PKCS11Backend.cpp +++ b/cdoc/PKCS11Backend.cpp @@ -416,7 +416,13 @@ libcdoc::PKCS11Backend::decryptRSA(std::vector &dst, const std::vector< int result = connectToKey(idx, true); if (result != OK) return result; - if (!oaep && !dst.empty()) { + if (!oaep) { + // If oaep is false, dst must be pre-allocated to the expected length. + // This is required to apply the implicit-rejection countermeasure on padding failure. + if (dst.empty()) { + LOG_ERROR("PKCS11Backend::decryptRSA: dst must be pre-allocated for PKCS#1 v1.5 decryption"); + return CRYPTO_ERROR; + } // Use raw RSA (CKM_RSA_X_509) so libcdoc can apply RFC 8017 implicit // rejection in user space. CKM_RSA_PKCS lets the token strip the // padding, but most PKCS#11 tokens leak the success/failure bit diff --git a/cdoc/WinBackend.cpp b/cdoc/WinBackend.cpp index c3a6d881..9fb9634c 100644 --- a/cdoc/WinBackend.cpp +++ b/cdoc/WinBackend.cpp @@ -218,21 +218,24 @@ libcdoc::WinBackend::decryptRSA(std::vector& dst, const std::vectorkey, PBYTE(data.data()), DWORD(data.size()), nullptr, nullptr, 0, &em_size, 0); if (err != ERROR_SUCCESS) { - LOG_ERROR("WinBackend::decryptRSACDoc1: NCryptDecrypt(size) failed (status={:#x})", DWORD(err)); + LOG_ERROR("WinBackend::decryptRSA: NCryptDecrypt(size) failed (status={:#x})", DWORD(err)); return CRYPTO_ERROR; } std::vector em(em_size, 0); err = NCryptDecrypt(d->key, PBYTE(data.data()), DWORD(data.size()), nullptr, PBYTE(em.data()), em_size, &em_size, 0); if (err != ERROR_SUCCESS) { libcdoc::cleanse(em); - LOG_ERROR("WinBackend::decryptRSACDoc1: NCryptDecrypt failed (status={:#x})", DWORD(err)); + LOG_ERROR("WinBackend::decryptRSA: NCryptDecrypt failed (status={:#x})", DWORD(err)); return CRYPTO_ERROR; } em.resize(em_size); From cc191334349bf76fffc6d5e5bdf052d23b6f24b7 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 5 Jun 2026 13:02:00 +0300 Subject: [PATCH 30/65] All C,H & M fixes from Caludo Opus review --- cdoc/CDoc2Reader.cpp | 34 +++- cdoc/CDoc2Writer.cpp | 32 ++++ cdoc/CDocCipher.cpp | 41 ++++- cdoc/CryptoBackend.cpp | 18 +- cdoc/Io.cpp | 45 ++++- cdoc/Lock.cpp | 4 + cdoc/NetworkBackend.cpp | 124 +++++++++++-- cdoc/Tar.cpp | 29 +++- cdoc/Utils.cpp | 150 +++++++++++++++- cdoc/Utils.h | 81 +++++++++ cdoc/XmlReader.cpp | 16 ++ cdoc/utils/memory.h | 43 +++++ test/CMakeLists.txt | 1 + test/libcdoc_boost.cpp | 375 ++++++++++++++++++++++++++++++++++++++++ 14 files changed, 948 insertions(+), 45 deletions(-) diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index 495708b0..caf6a75b 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -142,13 +142,21 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) LOG_DBG("CDoc2Reader::num locks: {}", priv->locks.size()); const Lock& lock = priv->locks.at(lock_idx); LOG_DBG("Label: {}", lock.label); + + // RAII-cleanse `kek` on every exit from this function (including + // exceptions). All early returns below previously had to remember to + // call libcdoc::cleanse(kek) - which several of them did not. With the + // guard the wipe is unconditional. std::vector kek; + libcdoc::Cleanser kek_guard(kek); + if (lock.type == Lock::Type::PASSWORD) { // Password LOG_DBG("password"); std::string info_str = libcdoc::CDoc2::getSaltForExpand(lock.label); LOG_DBG("info: {}", toHex(info_str)); std::vector kek_pm; + libcdoc::Cleanser kek_pm_guard(kek_pm); if (auto rv = crypto->extractHKDF(kek_pm, lock.getBytes(Lock::SALT), lock.getBytes(Lock::PW_SALT), lock.getInt(Lock::KDF_ITER), lock_idx); rv != libcdoc::OK) { setLastError(crypto->getLastErrorStr(rv)); LOG_ERROR("{}", last_error); @@ -163,6 +171,7 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) std::string info_str = libcdoc::CDoc2::getSaltForExpand(lock.label); LOG_DBG("info: {}", toHex(info_str)); std::vector kek_pm; + libcdoc::Cleanser kek_pm_guard(kek_pm); if (auto rv = crypto->extractHKDF(kek_pm, lock.getBytes(Lock::SALT), {}, 0, lock_idx); rv != libcdoc::OK) { setLastError(crypto->getLastErrorStr(rv)); LOG_ERROR("{}", last_error); @@ -174,6 +183,10 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) } else if ((lock.type == Lock::Type::PUBLIC_KEY) || (lock.type == Lock::Type::SERVER)) { // Public/private key std::vector key_material; + // SERVER path fetches key_material over the network; PUBLIC_KEY + // takes it from the lock. Either way it gets fed into ECDH or RSA + // and is sensitive enough to wipe in-scope. + libcdoc::Cleanser key_material_guard(key_material); if(lock.type == Lock::Type::SERVER) { if(!conf) { setLastError("Configuration is missing"); @@ -214,6 +227,7 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) } } else { std::vector kek_pm; + libcdoc::Cleanser kek_pm_guard(kek_pm); int result = crypto->deriveHMACExtract(kek_pm, key_material, toUint8Vector(libcdoc::CDoc2::KEKPREMASTER), lock_idx); if (result < 0) { setLastError(crypto->getLastErrorStr(result)); @@ -318,6 +332,10 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) LOG_ERROR("Cannot fetch share {}", i); return result; } + // Each individual share is itself sensitive: combined with the + // remaining shares it reconstructs the KEK. Wipe it after + // XOR-ing it into kek so it does not linger on the heap. + libcdoc::Cleanser share_guard(share.share); if (auto err = libcdoc::Crypto::xor_data(kek, kek, share.share); err != libcdoc::OK) { setLastError("Failed to derive kek"); LOG_ERROR("Failed to derive kek"); @@ -342,10 +360,13 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) if (auto err = libcdoc::Crypto::xor_data(fmk, lock.encrypted_fmk, kek); err != libcdoc::OK) { setLastError(t_("Failed to decrypt/derive fmk")); LOG_ERROR("{}", last_error); - libcdoc::cleanse(kek); + // Wipe any partial XOR result before surfacing the error. + libcdoc::cleanse(fmk); + fmk.clear(); return err; } std::vector hhk = libcdoc::Crypto::expand(fmk, libcdoc::CDoc2::HMAC); + libcdoc::Cleanser hhk_guard(hhk); LOG_TRACE_KEY("xor: {}", lock.encrypted_fmk); LOG_TRACE_KEY("fmk: {}", fmk); @@ -355,12 +376,13 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) if(!libcdoc::constant_time_compare(libcdoc::Crypto::sign_hmac(hhk, priv->header_data), priv->headerHMAC)) { setLastError(t_("Wrong decryption key (user key)")); LOG_ERROR("{}", last_error); - libcdoc::cleanse(kek); - libcdoc::cleanse(hhk); + // Authentication failed: the FMK we computed is for the wrong + // recipient. Wipe it before returning so the caller cannot leak + // it (e.g. via a logging hook that sees "fmk" in scope). + libcdoc::cleanse(fmk); + fmk.clear(); return libcdoc::WRONG_KEY; } - libcdoc::cleanse(kek); - libcdoc::cleanse(hhk); setLastError({}); return libcdoc::OK; } @@ -653,7 +675,7 @@ CDoc2Reader::CDoc2Reader(libcdoc::DataSource *src, bool take_ownership) LOG_ERROR("{}", last_error); return; } - uint32_t header_len = (c[0] << 24) | (c[1] << 16) | c[2] << 8 | c[3]; + uint32_t header_len = (uint32_t(c[0]) << 24) | (uint32_t(c[1]) << 16) | uint32_t(c[2]) << 8 | c[3]; if (constexpr uint32_t MAX_LEN = (1 << 20); header_len > MAX_LEN) { LOG_ERROR("{}", last_error); return; diff --git a/cdoc/CDoc2Writer.cpp b/cdoc/CDoc2Writer.cpp index c516c11e..e480d522 100644 --- a/cdoc/CDoc2Writer.cpp +++ b/cdoc/CDoc2Writer.cpp @@ -208,11 +208,23 @@ CDoc2Writer::buildHeader(std::vector& header, const std::vector> fb_rcpts; + // xor_key is XOR(fmk, kek). It is published in the header as the + // "encrypted FMK" so it is not itself a long-term secret, but while in + // scope it is bitwise-paired with the secret KEK and we wipe it on + // exit anyway as a hygiene measure. std::vector xor_key; + libcdoc::Cleanser xor_key_guard(xor_key); + for (unsigned int rcpt_idx = 0; rcpt_idx < recipients.size(); rcpt_idx++) { const libcdoc::Recipient& rcpt = recipients.at(rcpt_idx); if (rcpt.isPKI()) { std::vector key_material, kek; + // Per-iteration RAII cleanse: even if FAIL(...) (which expands + // to `return fail(...);`) shortcuts the loop, both buffers are + // wiped during stack unwind. The same applies to all other + // iteration-local secrets below. + libcdoc::Cleanser key_material_guard(key_material); + libcdoc::Cleanser kek_guard(kek); std::string send_url; if(rcpt.isKeyServer()) { if(!conf) @@ -259,8 +271,10 @@ CDoc2Writer::buildHeader(std::vector& header, const std::vector sharedSecret = libcdoc::Crypto::deriveSharedSecret(ephKey.get(), publicKey.get()); + libcdoc::Cleanser sharedSecret_guard(sharedSecret); key_material = libcdoc::Crypto::toPublicKeyDer(ephKey.get()); std::vector kekPm = libcdoc::Crypto::extract(sharedSecret, std::vector(libcdoc::CDoc2::KEKPREMASTER.cbegin(), libcdoc::CDoc2::KEKPREMASTER.cend())); + libcdoc::Cleanser kekPm_guard(kekPm); std::string info_str = libcdoc::CDoc2::getSaltForExpand(key_material, rcpt.rcpt_key); kek = libcdoc::Crypto::expand(kekPm, info_str, fmk.size()); @@ -290,6 +304,7 @@ CDoc2Writer::buildHeader(std::vector& header, const std::vector kek_pm(libcdoc::CDoc2::KEY_LEN); + libcdoc::Cleanser kek_pm_guard(kek_pm); std::vector salt; int64_t result = crypto->random(salt, libcdoc::CDoc2::KEY_LEN); if (result < 0) @@ -302,6 +317,7 @@ CDoc2Writer::buildHeader(std::vector& header, const std::vectorgetLastErrorStr(result), result); std::vector kek = libcdoc::Crypto::expand(kek_pm, info_str, libcdoc::CDoc2::KEY_LEN); + libcdoc::Cleanser kek_guard(kek); LOG_DBG("Label: {}", rcpt.label); LOG_DBG("KDF iter: {}", rcpt.kdf_iter); @@ -345,14 +361,18 @@ CDoc2Writer::buildHeader(std::vector& header, const std::vector key_material; crypto->random(key_material, libcdoc::CDoc2::KEY_LEN); + // key_material is split-share-input material; wipe on exit. + libcdoc::Cleanser key_material_guard(key_material); //KEK_i_pm = HKDF_Extract(KeyMaterialSalt_i, KeyMaterial_i) std::vector kek_pm = libcdoc::Crypto::extract(key_material_salt, key_material); + libcdoc::Cleanser kek_pm_guard(kek_pm); // KEK_i = HKDF_Expand(KEK_i_pm, "CDOC2kek" + FMKEncryptionMethod + RecipientInfo_i, L) std::string info_str = std::string("CDOC2kek") + cdoc20::header::EnumNameFMKEncryptionMethod(cdoc20::header::FMKEncryptionMethod::XOR) + RecipientInfo_i; LOG_DBG("Info: {}", info_str); std::vector kek = libcdoc::Crypto::expand(kek_pm, info_str); + libcdoc::Cleanser kek_guard(kek); LOG_TRACE_KEY("kek: {}", kek); if (kek.empty()) return libcdoc::CRYPTO_ERROR; if (auto err = libcdoc::Crypto::xor_data(xor_key, fmk, kek); err != libcdoc::OK) @@ -361,6 +381,18 @@ CDoc2Writer::buildHeader(std::vector& header, const std::vector> kek_shares(N_SHARES); + // Each individual share is itself sensitive: combined with the + // remaining shares it reconstructs KEK_i (and thus the FMK). + // Wipe every kek_shares[i] on exit. The lambda runs from the + // destructor of `shares_guard` regardless of how we leave + // scope (early return, exception). + struct KekSharesCleanser { + std::vector>& v; + ~KekSharesCleanser() noexcept { + for (auto &s : v) libcdoc::cleanse(s); + } + } shares_guard{kek_shares}; + for (int i = 1; i < N_SHARES; i++) { // KEK_i_share_j = CSRNG(256) crypto->random(kek_shares[i], libcdoc::CDoc2::KEY_LEN); diff --git a/cdoc/CDocCipher.cpp b/cdoc/CDocCipher.cpp index c4e13f09..042f43fd 100644 --- a/cdoc/CDocCipher.cpp +++ b/cdoc/CDocCipher.cpp @@ -489,15 +489,40 @@ int CDocCipher::Decrypt(const unique_ptr& rdr, unsigned int lock_idx result = rdr->nextFile(name, size); while (result == libcdoc::OK) { LOG_DBG("Got file: {} {}", name, size); - filesystem::path fpath(name); - if (fpath.is_absolute()) { - LOG_WARN("File has absolute path, stripping"); - fpath = fpath.filename(); - } else if (fpath.has_parent_path()) { - LOG_WARN("File has parent path, stripping"); - fpath = fpath.filename(); + + // Sanitise the attacker-controlled file name before composing the + // extraction path. See libcdoc::sanitiseExtractedFilename for the + // exact set of rejections (path separators, "..", drive letters, + // NUL bytes, reserved Windows device names, etc.). + std::string safeName = libcdoc::sanitiseExtractedFilename(name); + if (safeName.empty()) { + LOG_ERROR("Refusing unsafe entry name '{}'", name); + return 1; } - fpath = base_path / fpath; + filesystem::path fpath = base_path / filesystem::path(libcdoc::encodeName(safeName)); + + // Defence in depth: ensure the lexically-resolved target stays + // under base_path, even if a previously-extracted entry placed a + // symlink there. + std::error_code ec; + filesystem::path canonicalBase = filesystem::weakly_canonical(base_path, ec); + if (ec) { + LOG_ERROR("Cannot canonicalise base path {}: {}", + base_path.string(), ec.message()); + return 1; + } + filesystem::path canonicalTarget = filesystem::weakly_canonical(fpath, ec); + if (ec) { + LOG_ERROR("Cannot canonicalise target path {}: {}", + fpath.string(), ec.message()); + return 1; + } + if (canonicalTarget.parent_path() != canonicalBase) { + LOG_ERROR("Refusing entry '{}': target {} escapes base {}", + name, canonicalTarget.string(), canonicalBase.string()); + return 1; + } + std::ofstream ofs(fpath, std::ios_base::binary); if (ofs.bad()) { LOG_ERROR("Cannot open file {} for writing", fpath.string()); diff --git a/cdoc/CryptoBackend.cpp b/cdoc/CryptoBackend.cpp index d48ce72f..17694cfe 100644 --- a/cdoc/CryptoBackend.cpp +++ b/cdoc/CryptoBackend.cpp @@ -115,14 +115,18 @@ libcdoc::result_t CryptoBackend::extractHKDF(std::vector& kek_pm, const std::vector& salt, const std::vector& pw_salt, int32_t kdf_iter, unsigned int idx) { - if (salt.empty()) return INVALID_PARAMS; - if ((kdf_iter > 0) && pw_salt.empty()) return INVALID_PARAMS; - std::vector key_material; + if (salt.empty()) return INVALID_PARAMS; + if ((kdf_iter > 0) && pw_salt.empty()) return INVALID_PARAMS; + std::vector key_material; int result = getKeyMaterial(key_material, pw_salt, kdf_iter, idx); - if (result) return result; - kek_pm = libcdoc::Crypto::extract(key_material, salt); - libcdoc::cleanse(key_material); - if (kek_pm.empty()) return OPENSSL_ERROR; + if (result) return result; + kek_pm = libcdoc::Crypto::extract(key_material, salt); + libcdoc::cleanse(key_material); + if (kek_pm.empty()) return OPENSSL_ERROR; + if (kek_pm.size() != 32) { + LOG_ERROR("KEK has incorrect size: {} (expected {})", kek_pm.size(), 32); + return INVALID_PARAMS; + } LOG_TRACE_KEY("Extract: {}", kek_pm); diff --git a/cdoc/Io.cpp b/cdoc/Io.cpp index bc1ac845..5106212c 100644 --- a/cdoc/Io.cpp +++ b/cdoc/Io.cpp @@ -102,17 +102,48 @@ OStreamConsumer::OStreamConsumer(const std::string& path) } result_t FileListConsumer::open(const std::string &name, int64_t size) { - std::string_view fileName = name; if (ofs.is_open()) { ofs.close(); } - size_t lastSlashPos = fileName.find_last_of("\\/"); - if (lastSlashPos != std::string::npos) { - fileName = fileName.substr(lastSlashPos + 1); + + // The file name comes from inside the (encrypted) container and is + // therefore fully attacker-controlled. Run it through the central + // sanitiser; reject the entry rather than write to a tampered path. + std::string safeName = libcdoc::sanitiseExtractedFilename(name); + if (safeName.empty()) { + LOG_ERROR("FileListConsumer::open: refusing unsafe entry name '{}'", name); + return DATA_FORMAT_ERROR; + } + + fs::path target = base / fs::path(encodeName(safeName)); + + // Defence in depth: even after sanitising the leaf name, an attacker + // who can plant a symlink at `base` (e.g. by extracting an earlier + // entry that the host application created earlier) could redirect + // writes outside `base`. weakly_canonical resolves any symlinks that + // already exist in the path; we then verify the parent directory of + // the target equals the canonical base. + std::error_code ec; + fs::path canonicalBase = fs::weakly_canonical(base, ec); + if (ec) { + LOG_ERROR("FileListConsumer::open: cannot canonicalise base {}: {}", + base.string(), ec.message()); + return OUTPUT_STREAM_ERROR; + } + fs::path canonicalTarget = fs::weakly_canonical(target, ec); + if (ec) { + LOG_ERROR("FileListConsumer::open: cannot canonicalise target {}: {}", + target.string(), ec.message()); + return OUTPUT_STREAM_ERROR; } - fs::path path(base); - path /= encodeName(fileName); - ofs.open(path, std::ios_base::binary); + if (canonicalTarget.parent_path() != canonicalBase) { + LOG_ERROR("FileListConsumer::open: refusing entry '{}' - target {} " + "escapes base {}", + name, canonicalTarget.string(), canonicalBase.string()); + return DATA_FORMAT_ERROR; + } + + ofs.open(target, std::ios_base::binary); return ofs.bad() ? OUTPUT_STREAM_ERROR : OK; } diff --git a/cdoc/Lock.cpp b/cdoc/Lock.cpp index 37322529..fecadc27 100644 --- a/cdoc/Lock.cpp +++ b/cdoc/Lock.cpp @@ -102,6 +102,10 @@ Lock::parseLabel(const std::string& label) std::string key = urlDecode(range_to_sv(*it)); std::ranges::transform(key, key.begin(), [](unsigned char c){ return std::tolower(c); }); ++it; + if (it == label_data_parts.end()) { + LOG_ERROR("The label '{}' has no value for key '{}'", label, key); + continue; + } std::string value = urlDecode(range_to_sv(*it)); parsed_label[std::move(key)] = std::move(value); } diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 478066bc..f3569a25 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -138,6 +138,39 @@ getMIDSIDDescription(libcdoc::result_t code) } return {}; } + +// Map a CryptoBackend::HashAlgorithm to the algorithm name string the +// SK Smart-ID / Mobile-ID JSON API expects ("SHA224", "SHA256", +// "SHA384", "SHA512"). Returns an empty string_view when the algorithm +// is not in the supported set; callers MUST treat that as a hard error +// rather than indexing an array - foreign-language bindings (SWIG / Java +// / C#) and any future addition to the HashAlgorithm enum can otherwise +// drive the previous `algo_names[(int)algo]` lookup out of bounds. +// +// The function is constexpr so that the static_assert block below can +// verify at compile time that every documented enumerator maps to a +// non-empty string. Any new HashAlgorithm value added to CryptoBackend.h +// will trigger -Wswitch (no default branch covers it) and the +// static_asserts will catch it explicitly. +static constexpr std::string_view +hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm algo) noexcept +{ + switch (algo) { + case libcdoc::CryptoBackend::HashAlgorithm::SHA_224: return "SHA224"; + case libcdoc::CryptoBackend::HashAlgorithm::SHA_256: return "SHA256"; + case libcdoc::CryptoBackend::HashAlgorithm::SHA_384: return "SHA384"; + case libcdoc::CryptoBackend::HashAlgorithm::SHA_512: return "SHA512"; + } + return {}; +} + +static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_224) == "SHA224"); +static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_256) == "SHA256"); +static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_384) == "SHA384"); +static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_512) == "SHA512"); +// Out-of-range value (e.g. coming from a SWIG-generated foreign caller) +// must produce an empty result rather than reading past the array. +static_assert(hashAlgorithmToSidMidName(static_cast(99)).empty()); #endif thread_local std::string error; @@ -455,7 +488,12 @@ libcdoc::NetworkBackend::fetchNonce(std::vector& dst, const std::string LOG_DBG("Response: {}", rsp.body); picojson::value rsp_json; - picojson::parse(rsp_json, rsp.body); + std::string parse_err = picojson::parse(rsp_json, rsp.body); + if (!parse_err.empty()) { + error = FORMAT("JSON parse error: {}", parse_err); + LOG_ERROR("{}", error); + return NETWORK_ERROR; + } picojson::value v = rsp_json.get("nonce"); if (!v.is()) { error = FORMAT("No 'nonce' in response"); @@ -750,7 +788,12 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector LOG_DBG("Response: {}", rsp.body); picojson::value v; - picojson::parse(v, rsp.body); + std::string parse_err = picojson::parse(v, rsp.body); + if (!parse_err.empty()) { + error = FORMAT("JSON parse error: {}", parse_err); + LOG_ERROR("{}", error); + return NetworkBackend::NETWORK_ERROR; + } if (!v.is()) { error = "Invalid SmartID response"; LOG_WARN("Invalid SmartID response"); @@ -773,8 +816,19 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector // // Sign // - std::string algo_names[] = {"SHA224", "SHA256", "SHA384", "SHA512"}; - std::string algo_name = algo_names[(int) algo]; + std::string_view algo_name = hashAlgorithmToSidMidName(algo); + if (algo_name.empty()) { + error = "Unsupported hash algorithm for Smart-ID"; + LOG_ERROR("Unsupported hash algorithm for Smart-ID: {}", + static_cast(algo)); + return libcdoc::WRONG_ARGUMENTS; + } + + if (digest.empty()) { + error = "Empty digest"; + LOG_ERROR("Empty digest passed to signSID"); + return libcdoc::WRONG_ARGUMENTS; + } // Generate code uint8_t b[32]; @@ -794,7 +848,7 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector {"relyingPartyUUID", picojson::value(rp_uuid)}, {"relyingPartyName", picojson::value(rp_name)}, {"hash", picojson::value(toBase64(digest))}, - {"hashType", picojson::value(algo_name)}, + {"hashType", picojson::value(std::string(algo_name))}, {"allowedInteractionsOrder", picojson::value(aio) } @@ -809,7 +863,12 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector result = post(cli, full, hdrs, query.serialize(), rsp); if (result != libcdoc::OK) return result; LOG_DBG("Response: {}", rsp.body); - picojson::parse(v, rsp.body); + parse_err = picojson::parse(v, rsp.body); + if (!parse_err.empty()) { + error = FORMAT("JSON parse error: {}", parse_err); + LOG_ERROR("{}", error); + return NetworkBackend::NETWORK_ERROR; + } if (!v.is()) { error = "Invalid SmartID response"; LOG_WARN("Invalid SmartID response"); @@ -841,6 +900,31 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector const std::string& url, const std::string& rp_uuid, const std::string& rp_name, const std::string& phone, const std::string& rcpt_id, const std::vector& digest, CryptoBackend::HashAlgorithm algo) { + // Validate rcpt_id BEFORE doing anything else (network setup, key + // material, etc.). The previous implementation called + // rcpt_id.substr(11, 11) which throws std::out_of_range when + // rcpt_id.size() < 11 and silently returns a too-short identifier + // when 11 <= size < 22 - both of which would propagate to the SK + // Mobile-ID service as garbage and (worse) leak partially-filled + // payloads to the network in the latter case. + libcdoc::EtsiRecipientId parsed = libcdoc::parseEtsiRecipientId(rcpt_id); + if (!parsed.valid()) { + error = "Invalid Mobile ID recipient identifier"; + LOG_ERROR("Invalid Mobile ID recipient identifier: '{}'", rcpt_id); + return libcdoc::WRONG_ARGUMENTS; + } + + // The SK Mobile-ID API expects `nationalIdentityNumber` to be the + // bare digits with no country prefix, so we use the parsed national + // identifier directly. + const std::string &id_num = parsed.national_id; + + if (digest.empty()) { + error = "Empty digest"; + LOG_ERROR("Empty digest passed to signMID"); + return libcdoc::WRONG_ARGUMENTS; + } + std::string certificateLevel = "QUALIFIED"; std::string nonce = libcdoc::toBase64(Crypto::random(16)); @@ -863,23 +947,26 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector // // Authenticate // - std::string algo_names[] = {"SHA224", "SHA256", "SHA384", "SHA512"}; - std::string algo_name = algo_names[(int) algo]; + std::string_view algo_name = hashAlgorithmToSidMidName(algo); + if (algo_name.empty()) { + error = "Unsupported hash algorithm for Mobile-ID"; + LOG_ERROR("Unsupported hash algorithm for Mobile-ID: {}", + static_cast(algo)); + return libcdoc::WRONG_ARGUMENTS; + } - // Generate code + // Generate verification code. digest is guaranteed non-empty above. unsigned int code = (((digest[0] & 0xfc) << 5) | (digest[digest.size() - 1] & 0x7f)); result = showVerificationCode(code); if (result != OK) return result; - // etsi/PNOEE-01234567890 - std::string id_num = rcpt_id.substr(11, 11); picojson::object qobj = { {"relyingPartyUUID", picojson::value(rp_uuid)}, {"relyingPartyName", picojson::value(rp_name)}, {"phoneNumber", picojson::value(phone)}, {"nationalIdentityNumber", picojson::value(id_num)}, {"hash", picojson::value(toBase64(digest))}, - {"hashType", picojson::value(algo_name)}, + {"hashType", picojson::value(std::string(algo_name))}, {"language", picojson::value("ENG")}, {"displayText", picojson::value("Tahad dekryptida?")}, {"displayTextFormat", picojson::value("GSM-7")} @@ -898,15 +985,22 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector LOG_DBG("Response: {}", rsp.body); picojson::value v; - picojson::parse(v, rsp.body); + parse_err = picojson::parse(v, rsp.body); + if (!parse_err.empty()) { + error = FORMAT("JSON parse error: {}", parse_err); + LOG_ERROR("{}", error); + return NetworkBackend::NETWORK_ERROR; + } if (!v.is()) { error = "Invalid Mobile ID response"; - LOG_WARN("Invalid Monbile ID response"); + LOG_WARN("Invalid Mobile ID response"); + return NetworkBackend::NETWORK_ERROR; } picojson::value w = v.get("sessionID"); if (!w.is()) { error = "Invalid Mobile ID response"; - LOG_WARN("Invalid Monbile ID response"); + LOG_WARN("Invalid Mobile ID response"); + return NetworkBackend::NETWORK_ERROR; } std::string sessionID = w.get(); LOG_DBG("SessionID: {}", sessionID); diff --git a/cdoc/Tar.cpp b/cdoc/Tar.cpp index 64c7e403..f6688063 100644 --- a/cdoc/Tar.cpp +++ b/cdoc/Tar.cpp @@ -30,6 +30,16 @@ constexpr unsigned int BLOCKSIZE = 512; constexpr int64_t CDOC2_MAX_FILE_SIZE = 8LL * 1024 * 1024 * 1024; +// Cap on the declared size of an "auxiliary" tar header - i.e. extended +// PAX header ('x') or global PAX header ('g'). The PAX standard places no +// formal upper bound on these, but realistic records produced by tar(1) +// are O(KB) (one entry per path/size override). A malicious archive could +// otherwise declare an 8 GiB PAX header and force the decryption pipeline +// to either allocate that much memory (readPaxHeader) or spin through it +// in skip() (next()). 64 KiB is well above anything legitimate while +// keeping per-entry memory and stream-skip work bounded. +constexpr int64_t MAX_AUX_HEADER_SIZE = 64 * 1024; + template [[nodiscard]] static constexpr bool svtoi(std::string_view data, T& result) noexcept { @@ -316,6 +326,15 @@ libcdoc::result_t libcdoc::TarSource::readPaxHeader(const Header& hdr, std::string& name, int64_t& size) { int64_t h_size = hdr.getSize(); + // Validate the declared size BEFORE allocating the buffer. getSize() + // already returns -1 for malformed octal or sizes above + // CDOC2_MAX_FILE_SIZE, but that 8 GiB ceiling is meant for payload + // files; PAX headers themselves must be much smaller. See the + // MAX_AUX_HEADER_SIZE comment near the top of this file. + if (h_size < 0 || h_size > MAX_AUX_HEADER_SIZE) { + _error = DATA_FORMAT_ERROR; + return _error; + } std::string paxData(h_size, 0); result_t result = _src->read((uint8_t *) paxData.data(), paxData.size()); if (result != h_size) { @@ -428,8 +447,16 @@ libcdoc::TarSource::next(std::string& name, int64_t& size) _eof = false; return OK; } - // Skip other header types ('g') + // Skip other header types ('g' = global PAX header, plus any tar + // type we don't recognise as data). Cap the declared size at the + // same ceiling we use for 'x' headers so an attacker cannot force + // the upstream decryption pipeline to spin through gigabytes of + // payload bytes per malicious header. h_size = h.getSize(); + if (h_size < 0 || h_size > MAX_AUX_HEADER_SIZE) { + _error = DATA_FORMAT_ERROR; + return _error; + } _src->skip(h_size + padding(h_size)); } return END_OF_STREAM; diff --git a/cdoc/Utils.cpp b/cdoc/Utils.cpp index 26ad0d86..4541c932 100644 --- a/cdoc/Utils.cpp +++ b/cdoc/Utils.cpp @@ -104,8 +104,9 @@ parseURL(const std::string& url, std::string& host, int& port, std::string& path { char *phost, *ppath; int pport; + int pssl; if (!OSSL_HTTP_parse_url(url.c_str(), - nullptr, // SSL + &pssl, nullptr, // user &phost, nullptr, // port (str) @@ -116,6 +117,13 @@ parseURL(const std::string& url, std::string& host, int& port, std::string& path )) { return libcdoc::DATA_FORMAT_ERROR; } + bool is_https = (pssl == 1); + if (!is_https) { + OPENSSL_free(phost); + OPENSSL_free(ppath); + LOG_ERROR("URL scheme must be https: {}", url); + return libcdoc::DATA_FORMAT_ERROR; + } host = phost; port = pport; path = ppath; @@ -160,6 +168,146 @@ operator<<(std::ostream& escaped, urlEncode src) return escaped; } +EtsiRecipientId +parseEtsiRecipientId(std::string_view rcpt_id) +{ + constexpr std::string_view kPrefix{"etsi/PNO"}; + constexpr size_t kCountryCodeLen = 2; + constexpr size_t kSeparatorLen = 1; + constexpr size_t kMaxNationalIdLen = 32; + + // Need at least: prefix + 2 country chars + '-' + 1 id digit. + if (rcpt_id.size() < kPrefix.size() + kCountryCodeLen + kSeparatorLen + 1) { + return {}; + } + if (rcpt_id.substr(0, kPrefix.size()) != kPrefix) { + return {}; + } + + std::string_view cc = rcpt_id.substr(kPrefix.size(), kCountryCodeLen); + for (char c : cc) { + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) { + return {}; + } + } + + if (rcpt_id[kPrefix.size() + kCountryCodeLen] != '-') { + return {}; + } + + std::string_view nat_id = rcpt_id.substr(kPrefix.size() + kCountryCodeLen + kSeparatorLen); + if (nat_id.empty() || nat_id.size() > kMaxNationalIdLen) { + return {}; + } + for (char c : nat_id) { + if (c < '0' || c > '9') { + return {}; + } + } + + EtsiRecipientId out; + out.country.reserve(kCountryCodeLen); + for (char c : cc) { + out.country.push_back(char((c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c)); + } + out.national_id.assign(nat_id); + return out; +} + +std::string +sanitiseExtractedFilename(std::string_view name) +{ + // 1. Reject anything whose UTF-8 is malformed or contains NUL/control + // characters. NUL is particularly dangerous: many Windows APIs + // truncate at NUL while the filesystem treats the full name, which + // has historically been used to mask malicious extensions. + if (name.empty()) return {}; + for (unsigned char c : name) { + if (c == 0u) return {}; + if (c < 0x20u && c != '\t') return {}; // strip ASCII control bytes + } + + // 2. Strip every directory component. We split on BOTH '/' and '\\' + // on every platform: an attacker who crafts a Windows-style path on + // Linux is still trying to escape, and vice versa. We always take + // the last non-empty component. + size_t last_sep = name.find_last_of("\\/"); + std::string_view base = (last_sep == std::string_view::npos) + ? name + : name.substr(last_sep + 1); + + // 3. Reject Windows drive-letter prefixes that survived the slash split + // (e.g. "C:foo.txt" with no slash is drive-relative on Windows and + // refers to the current directory of drive C:, NOT the current + // working directory). We strip "X:" if the prefix looks like one. + if (base.size() >= 2 && base[1] == ':' && + ((base[0] >= 'A' && base[0] <= 'Z') || + (base[0] >= 'a' && base[0] <= 'z'))) { + base = base.substr(2); + } + + // 4. Trim trailing dots and whitespace. Windows silently strips these + // when creating files, so "evil.exe.." resolves to "evil.exe" and + // can collide with or hide a legitimate file. Trim leading + // whitespace too, for symmetry. + while (!base.empty() && (base.back() == '.' || base.back() == ' ')) + base.remove_suffix(1); + while (!base.empty() && (base.front() == ' ' || base.front() == '\t')) + base.remove_prefix(1); + + // 5. Reject "." and ".." outright. These appear standalone after + // stripping a leading directory component (e.g. name == ".."). + if (base.empty() || base == "." || base == "..") return {}; + + // 6. Reject reserved Windows device names. The check is case-insensitive + // and applies to both the bare name and the name before any extension. + { + size_t dot = base.find('.'); + std::string_view stem = base.substr(0, dot); + std::string upper(stem.size(), '\0'); + for (size_t i = 0; i < stem.size(); ++i) { + unsigned char ch = uint8_t(stem[i]); + upper[i] = char((ch >= 'a' && ch <= 'z') ? (ch - 'a' + 'A') : ch); + } + static constexpr std::string_view reserved[] = { + "CON", "PRN", "AUX", "NUL", + "COM1", "COM2", "COM3", "COM4", "COM5", + "COM6", "COM7", "COM8", "COM9", + "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", + "LPT6", "LPT7", "LPT8", "LPT9", + }; + for (const auto &r : reserved) { + if (upper == r) return {}; + } + } + + // 7. Cap to a sensible byte length. The practical filename limit on + // every filesystem libcdoc supports is 255 bytes (NTFS, ext4, APFS). + // A name longer than that would fail filesystem operations anyway; + // truncating up-front gives a uniform error mode. We truncate from + // the end while keeping the file extension if there is one. + constexpr size_t MAX_BYTES = 255; + if (base.size() > MAX_BYTES) { + size_t dot = base.find_last_of('.'); + if (dot != std::string_view::npos && + dot > 0 && + base.size() - dot < 16) { + // Preserve a short extension; truncate the stem. + std::string_view ext = base.substr(dot); + std::string_view stem = base.substr(0, dot); + size_t keep_stem = MAX_BYTES - ext.size(); + std::string out; + out.reserve(MAX_BYTES); + out.assign(stem.data(), keep_stem); + out.append(ext.data(), ext.size()); + return out; + } + return std::string(base.substr(0, MAX_BYTES)); + } + + return std::string(base); +} + std::vector JsonToStringArray(std::string_view json) { diff --git a/cdoc/Utils.h b/cdoc/Utils.h index 62600dbc..68dda1c6 100644 --- a/cdoc/Utils.h +++ b/cdoc/Utils.h @@ -143,6 +143,87 @@ readAllBytes(std::string_view filename) int parseURL(const std::string& url, std::string& host, int& port, std::string& path, bool end_with_slash = false); std::string buildURL(const std::string& host, int port); +/** + * @brief Sanitise an attacker-controlled file name for safe extraction. + * + * @p name comes from a CDoc1/DDoc/CDoc2 archive header and is fully under the + * control of whoever produced the container. The function strips every + * filesystem-significant component that could let the path escape the + * caller-supplied @p base directory or trick a Windows API into doing + * something other than "create a normal file inside @p base": + * + * - all leading directory components (slashes, backslashes, drive letters), + * - "." and ".." segments, + * - NUL bytes and other ASCII control characters, + * - leading/trailing whitespace and dots (Windows trims these silently), + * - reserved Windows device names (CON, PRN, AUX, NUL, COM1..COM9, LPT1..LPT9), + * - excessively long names (capped at 255 bytes after sanitisation, the + * practical filename limit on every filesystem libcdoc supports). + * + * The returned string is a relative file name (no slashes), or empty if no + * safe name could be derived. A caller that gets an empty return value MUST + * either skip the entry or replace it with a generated placeholder; it MUST + * NOT fall back to the raw @p name. This function does not consult the + * filesystem; the caller is still expected to verify, after composing + * @p base / sanitisedName, that the resulting absolute path stays within + * @p base (e.g. by comparing weakly_canonical(base / safe).parent_path() + * against weakly_canonical(base)). The two checks are complementary: + * sanitisation eliminates known-malicious shapes up-front, the post-compose + * check protects against symlinks pointed at by previously-extracted files. + * + * @param name the unsafe input file name + * @return a relative file name guaranteed not to contain path-traversal + * elements, or an empty string when no safe name can be produced. + */ +CDOC_EXPORT std::string sanitiseExtractedFilename(std::string_view name); + +/** + * @brief Parsed components of an ETSI Smart-ID / Mobile-ID recipient identifier. + * + * The on-the-wire format used by SK's Smart-ID and Mobile-ID services is + * @c etsi/PNO-, e.g. @c etsi/PNOEE-30303039914. The + * @c field is the ISO-3166-1 alpha-2 country code; the + * @c field is the personal identifier issued by that + * country (in Estonia: 11 ASCII digits). + * + * @ref parseEtsiRecipientId returns this struct after validating the + * shape of the input; an empty @ref country / @ref national_id pair + * indicates a parse failure. + */ +struct EtsiRecipientId { + /// ISO-3166-1 alpha-2 country code (e.g. "EE"). Empty on parse failure. + std::string country; + /// National identifier portion (digits only). Empty on parse failure. + std::string national_id; + + /// Convenience: true iff the input parsed cleanly. + [[nodiscard]] bool valid() const noexcept { + return !country.empty() && !national_id.empty(); + } +}; + +/** + * @brief Parse an ETSI recipient identifier into its country and national-id parts. + * + * The accepted shape is @c etsi/PNO-: + * + * - exactly the literal prefix @c "etsi/PNO"; + * - exactly two ASCII letters of country code (case-insensitive on input, + * normalised to upper case in the result); + * - a literal @c '-' separator; + * - a non-empty national identifier composed of ASCII digits and at + * most 32 characters total (a generous upper bound that comfortably + * covers all current SK formats while rejecting megabyte payloads). + * + * Returns an @ref EtsiRecipientId with empty fields if any of the above + * is violated. The function never throws, never logs, and never reads + * past the end of the input. + * + * @param rcpt_id the recipient identifier to parse + * @return parsed components; check @ref EtsiRecipientId::valid() to test + */ +CDOC_EXPORT EtsiRecipientId parseEtsiRecipientId(std::string_view rcpt_id); + struct urlEncode { std::string_view src; friend std::ostream& operator<<(std::ostream& escaped, urlEncode src); diff --git a/cdoc/XmlReader.cpp b/cdoc/XmlReader.cpp index 4f138f5f..60cc3b6d 100644 --- a/cdoc/XmlReader.cpp +++ b/cdoc/XmlReader.cpp @@ -43,6 +43,22 @@ static std::string tostring(pcxmlChar tmp) return result; } +#if LIBXML_VERSION < 21300 +static xmlParserInputPtr +nullExternalEntityLoader(const char *, const char *, xmlParserCtxtPtr) +{ + return nullptr; +} + +struct XmlInit { + XmlInit() { + xmlSetExternalEntityLoader(nullExternalEntityLoader); + xmlSubstituteEntitiesDefault(0); + } +}; +static XmlInit xmlInit; +#endif + XMLReader::XMLReader(libcdoc::DataSource &src) : d(xmlReaderForIO([](void *context, char *buffer, int len) -> int { auto *src = reinterpret_cast(context); diff --git a/cdoc/utils/memory.h b/cdoc/utils/memory.h index 42e027dd..05fbca1c 100644 --- a/cdoc/utils/memory.h +++ b/cdoc/utils/memory.h @@ -200,6 +200,49 @@ void cleanse(std::array& a) noexcept OPENSSL_cleanse(a.data(), a.size() * sizeof(T)); } +/** + * @brief Scope guard that wipes a contiguous secret on destruction. + * + * Wraps a reference to a @c std::vector (or @c std::array) + * and calls @ref libcdoc::cleanse on it from the destructor, including the + * exceptional and early-return paths. Intended for the short-lived KEK / FMK + * pre-master / shared-secret buffers in CDoc2Reader / CDoc2Writer where every + * function has multiple early-return branches and remembering to cleanse at + * each one is fragile. + * + * Note: this only wipes the *currently-allocated* storage. It does NOT wipe + * earlier allocations that @c std::vector may have freed during a resize. + * For long-lived secrets that get assigned over multiple times, use + * @ref SecureBytes (which serialises through cleanse/unlock on each resize) + * or a fixed-size container. + * + * Usage: + * @code + * std::vector kek; + * Cleanser kek_guard(kek); // wipes `kek` on every exit from this scope + * ... + * if (failure) return ERROR; // kek is wiped before unwind + * ... + * @endcode + */ +template +class Cleanser { +public: + explicit Cleanser(Container& c) noexcept : c_(c) {} + ~Cleanser() noexcept { libcdoc::cleanse(c_); } + + Cleanser(const Cleanser&) = delete; + Cleanser& operator=(const Cleanser&) = delete; + Cleanser(Cleanser&&) = delete; + Cleanser& operator=(Cleanser&&) = delete; +private: + Container& c_; +}; + +// Class template argument deduction: `Cleanser g(vec);` infers the type. +template +Cleanser(Container&) -> Cleanser; + inline bool constant_time_compare(const std::vector& a, const std::vector& b) noexcept { if (a.size() != b.size()) return false; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a5850d1c..31f758d0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,6 +1,7 @@ add_executable(unittests libcdoc_boost.cpp ../cdoc/Crypto.cpp + ../cdoc/Tar.cpp ) target_link_libraries(unittests diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 2b476b55..ba6e17d9 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -860,6 +861,120 @@ BOOST_FIXTURE_TEST_CASE(NonAsciiFilename, PaxFixture) BOOST_TEST(fs::exists(outDir / namePath)); } +// Build a single 512-byte ustar header block with the given typeflag, +// name and declared size. The checksum is computed correctly so the +// header passes Header::verify(). Returns a 512-byte vector. +static std::vector +makeTarHeader(char typeflag, std::string_view name, int64_t size) +{ + std::vector block(512, 0); + + // name (100 bytes, NUL-terminated within the field) + std::copy(name.begin(), + name.begin() + std::min(name.size(), 99), + block.begin()); + + // mode "0000600\0", uid "0000000\0", gid "0000000\0" + auto write_octal_field = [&](size_t offset, size_t width, int64_t value) { + std::string s(width - 1, '0'); + for (size_t i = 0; i < width - 1 && value > 0; ++i) { + s[width - 2 - i] = char('0' + (value & 7)); + value >>= 3; + } + std::copy(s.begin(), s.end(), block.begin() + offset); + // trailing NUL is already zero-filled + }; + write_octal_field(100, 8, 0600); // mode + write_octal_field(108, 8, 0); // uid + write_octal_field(116, 8, 0); // gid + write_octal_field(124, 12, size); // size <-- attacker-tamperable + write_octal_field(136, 12, 0); // mtime + + // chksum field: 8 spaces during checksum calculation + std::fill(block.begin() + 148, block.begin() + 156, uint8_t(' ')); + + // typeflag + block[156] = uint8_t(typeflag); + + // ustar magic + version + constexpr std::string_view magic{"ustar\0", 6}; + std::copy(magic.begin(), magic.end(), block.begin() + 257); + block[263] = '0'; + block[264] = '0'; + + // Compute and write the checksum: unsigned sum of all bytes with + // chksum replaced by spaces. Field is 6 octal digits + NUL + space. + int64_t sum = 0; + for (uint8_t b : block) sum += b; + std::string chk(7, '0'); + for (size_t i = 0; i < 6 && sum > 0; ++i) { + chk[5 - i] = char('0' + (sum & 7)); + sum >>= 3; + } + chk[6] = '\0'; + std::copy(chk.begin(), chk.end(), block.begin() + 148); + block[155] = ' '; + + return block; +} + +BOOST_AUTO_TEST_CASE(RejectsOversizedPaxExtendedHeader) +{ + // Craft a valid 'x' (extended PAX) header that declares a 100 MiB + // payload. The traditional ustar size field is 12 bytes (11 octal + // digits + NUL), capping the directly-encoded size at ~8 GiB minus + // one; we pick a value comfortably below that ceiling but still + // many orders of magnitude above the 64 KiB cap on auxiliary + // headers. Without H-2 in place, TarSource::readPaxHeader would + // happily allocate 100 MiB and try to read 100 MiB from the stream + // - times every malicious 'x' header, which is the DoS the cap + // exists to prevent. + constexpr int64_t kBadSize = 100LL * 1024 * 1024; + std::vector stream = makeTarHeader('x', "PaxHeaders/x", kBadSize); + + libcdoc::VectorSource src(stream); + libcdoc::TarSource tar_src(&src, /*take_ownership=*/false); + std::string name; + int64_t size = 0; + libcdoc::result_t rv = tar_src.next(name, size); + + BOOST_CHECK_EQUAL(rv, libcdoc::DATA_FORMAT_ERROR); + BOOST_CHECK(tar_src.isError()); +} + +BOOST_AUTO_TEST_CASE(RejectsOversizedGlobalPaxHeader) +{ + // Same defence on the 'g' (global PAX) skip path. next() must reject + // the header without spinning the upstream source through 100 MiB. + constexpr int64_t kBadSize = 100LL * 1024 * 1024; + std::vector stream = makeTarHeader('g', "PaxHeaders/g", kBadSize); + + libcdoc::VectorSource src(stream); + libcdoc::TarSource tar_src(&src, /*take_ownership=*/false); + std::string name; + int64_t size = 0; + libcdoc::result_t rv = tar_src.next(name, size); + + BOOST_CHECK_EQUAL(rv, libcdoc::DATA_FORMAT_ERROR); + BOOST_CHECK(tar_src.isError()); +} + +BOOST_AUTO_TEST_CASE(AllowsReasonablePaxHeaderSize) +{ + // Sanity check: a PAX header with a small, plausible size (one + // 'path' record for a 50-byte name) must still parse. We do not + // include the actual data in the stream, so readPaxHeader will + // surface INPUT_STREAM_ERROR after the cap check passes - the + // important thing is that DATA_FORMAT_ERROR is NOT returned. + std::vector stream = makeTarHeader('x', "PaxHeaders/x", 60); + libcdoc::VectorSource src(stream); + libcdoc::TarSource tar_src(&src, /*take_ownership=*/false); + std::string name; + int64_t size = 0; + libcdoc::result_t rv = tar_src.next(name, size); + BOOST_CHECK_NE(rv, libcdoc::DATA_FORMAT_ERROR); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(StreamingDecryption) @@ -908,3 +1023,263 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(constructor, Buf, BufTypes) } BOOST_AUTO_TEST_SUITE_END() + +// Regression coverage for libcdoc::sanitiseExtractedFilename(). All inputs +// here come from attacker-controlled archive headers (tar / DDoc); the +// helper is the single chokepoint that decides whether an entry can ever +// reach the filesystem. +BOOST_AUTO_TEST_SUITE(SanitiseExtractedFilename) + +BOOST_AUTO_TEST_CASE(PassesThroughOrdinaryNames) +{ + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("hello.txt"), "hello.txt"); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("a-b_c.dat"), "a-b_c.dat"); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("file with spaces.txt"), + "file with spaces.txt"); + // Non-ASCII (UTF-8) names must round-trip - libcdoc treats names as + // opaque UTF-8. + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("\xC3\xB5\xC3\xA4\xC3\xB6.txt"), + "\xC3\xB5\xC3\xA4\xC3\xB6.txt"); +} + +BOOST_AUTO_TEST_CASE(StripsLeadingDirectoryComponents) +{ + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("a/b/c.txt"), "c.txt"); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("a\\b\\c.txt"), "c.txt"); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("/etc/passwd"), "passwd"); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("../foo.txt"), "foo.txt"); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("a/../foo.txt"), "foo.txt"); +} + +BOOST_AUTO_TEST_CASE(RejectsTraversalAndEmpty) +{ + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename(""), ""); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("."), ""); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename(".."), ""); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("../"), ""); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("..\\"), ""); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("foo/.."), ""); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("/"), ""); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("a/b/"), ""); +} + +BOOST_AUTO_TEST_CASE(StripsWindowsDriveRelativeNames) +{ + // "C:foo" with NO slash is a drive-relative path on Windows. On POSIX + // it would normally pass through, but libcdoc applies the same filter + // on every platform so a malicious archive cannot rely on platform- + // specific quirks. + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("C:foo.txt"), "foo.txt"); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("z:bar"), "bar"); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("C:"), ""); + // After a slash strip, the drive prefix on the leaf is also handled. + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("a/C:foo"), "foo"); +} + +BOOST_AUTO_TEST_CASE(RejectsControlCharsAndNul) +{ + // Embedded NUL is a Windows API truncation hazard. + std::string with_nul("foo\0bar.txt", 11); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename(with_nul), ""); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename(std::string("a\x01" "b")), ""); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename(std::string("a\x1F" "b")), ""); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename(std::string("a\nb")), ""); + // Tab is allowed (whitespace, not a control character that breaks + // filesystems on the platforms libcdoc supports). + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("a\tb"), "a\tb"); +} + +BOOST_AUTO_TEST_CASE(TrimsTrailingDotsAndSpaces) +{ + // Windows silently strips trailing dots/spaces when creating files, + // so "evil.exe " and "evil.exe." both resolve to "evil.exe". Strip + // them before composing the path so we can't be tricked into + // colliding with a legitimate name. + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("foo.txt..."), "foo.txt"); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("foo.txt "), "foo.txt"); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("foo.txt . . "), "foo.txt"); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("..."), ""); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename(" "), ""); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename(" hello "), "hello"); +} + +BOOST_AUTO_TEST_CASE(RejectsReservedWindowsDeviceNames) +{ + // On Windows these are device handles regardless of working + // directory. They would not actually create a file at base/CON, but + // would open the console device and any subsequent write goes there. + for (auto name : {"CON", "PRN", "AUX", "NUL", + "com1", "Com2", "LPT1", "lpt9"}) { + BOOST_TEST_INFO("name=" << name); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename(name), ""); + } + // Reserved name with extension is also reserved on Windows. + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("CON.txt"), ""); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("nul.tar.gz"), ""); + // Names that *contain* a reserved word as a substring are NOT + // reserved (e.g. "console.log"). + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("console.log"), "console.log"); + BOOST_CHECK_EQUAL(libcdoc::sanitiseExtractedFilename("nullable"), "nullable"); +} + +BOOST_AUTO_TEST_CASE(TruncatesOverlongNames) +{ + std::string long_stem(300, 'a'); + auto result = libcdoc::sanitiseExtractedFilename(long_stem + ".dat"); + BOOST_CHECK_LE(result.size(), 255u); + BOOST_CHECK(result.ends_with(".dat")); // extension preserved + // No-extension version simply truncates. + auto truncated = libcdoc::sanitiseExtractedFilename(std::string(400, 'b')); + BOOST_CHECK_EQUAL(truncated.size(), 255u); +} + +BOOST_AUTO_TEST_SUITE_END() + +// Coverage for libcdoc::Cleanser, the RAII guard used by CDoc2Reader::getFMK +// and CDoc2Writer::buildHeader to wipe short-lived KEK / FMK material on +// every exit including exceptions. +BOOST_AUTO_TEST_SUITE(CleanserGuard) + +BOOST_AUTO_TEST_CASE(WipesVectorOnScopeExit) +{ + std::vector secret(32, 0xAA); + { + libcdoc::Cleanser guard(secret); + BOOST_CHECK_EQUAL(secret.front(), 0xAA); // not yet wiped + } + // After the scope exits the destructor runs OPENSSL_cleanse on the + // current allocation; the vector keeps its size but every byte is 0. + BOOST_CHECK_EQUAL(secret.size(), 32u); + for (uint8_t b : secret) + BOOST_CHECK_EQUAL(b, 0u); +} + +BOOST_AUTO_TEST_CASE(WipesArrayOnScopeExit) +{ + std::array secret{}; + secret.fill(0x55); + { + libcdoc::Cleanser guard(secret); + } + for (uint8_t b : secret) + BOOST_CHECK_EQUAL(b, 0u); +} + +BOOST_AUTO_TEST_CASE(WipesOnException) +{ + // The whole point of the RAII guard: on an exception thrown out of + // the protected scope, the destructor still fires and the secret is + // wiped before the exception unwinds past the caller. This is the + // failure mode where the audit found the missing cleanses in + // CDoc2Reader::getFMK. + std::vector secret(8, 0xCC); + auto throws = [&]{ + libcdoc::Cleanser guard(secret); + throw std::runtime_error("boom"); + }; + BOOST_CHECK_THROW(throws(), std::runtime_error); + for (uint8_t b : secret) + BOOST_CHECK_EQUAL(b, 0u); +} + +BOOST_AUTO_TEST_CASE(EmptyVectorIsHarmless) +{ + // Edge case: cleanse() short-circuits on an empty container. The + // guard must not crash or call OPENSSL_cleanse with a null pointer. + std::vector empty; + { + libcdoc::Cleanser guard(empty); + } + BOOST_CHECK(empty.empty()); +} + +BOOST_AUTO_TEST_SUITE_END() + +// Coverage for libcdoc::parseEtsiRecipientId. The helper is the input- +// validation chokepoint for the Mobile-ID / Smart-ID code paths; +// signMID in particular previously called rcpt_id.substr(11, 11) +// without checking the input, which threw std::out_of_range on short +// ids and silently truncated medium-length ones. +BOOST_AUTO_TEST_SUITE(EtsiRecipientIdParsing) + +BOOST_AUTO_TEST_CASE(AcceptsCanonicalEstonian) +{ + auto p = libcdoc::parseEtsiRecipientId("etsi/PNOEE-30303039914"); + BOOST_TEST_REQUIRE(p.valid()); + BOOST_CHECK_EQUAL(p.country, "EE"); + BOOST_CHECK_EQUAL(p.national_id, "30303039914"); +} + +BOOST_AUTO_TEST_CASE(AcceptsOtherCountryCodes) +{ + // The PNO format is shared across SK markets; all that matters is + // that the country code is two ASCII letters. + auto p = libcdoc::parseEtsiRecipientId("etsi/PNOLT-12345678901"); + BOOST_TEST_REQUIRE(p.valid()); + BOOST_CHECK_EQUAL(p.country, "LT"); + BOOST_CHECK_EQUAL(p.national_id, "12345678901"); +} + +BOOST_AUTO_TEST_CASE(NormalisesCountryToUpperCase) +{ + auto p = libcdoc::parseEtsiRecipientId("etsi/PNOee-30303039914"); + BOOST_TEST_REQUIRE(p.valid()); + BOOST_CHECK_EQUAL(p.country, "EE"); +} + +BOOST_AUTO_TEST_CASE(RejectsShortInput) +{ + // The previous implementation in signMID threw std::out_of_range + // for any input shorter than 11 characters. The helper must reject + // these cleanly with .valid() == false. + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("").valid()); + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/").valid()); + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/PNO").valid()); + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/PNOEE").valid()); + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/PNOEE-").valid()); + // 11 characters but not the right shape. + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/short!").valid()); +} + +BOOST_AUTO_TEST_CASE(RejectsBadPrefix) +{ + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("ETSI/PNOEE-30303039914").valid()); // case-sensitive prefix + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/IDEE-30303039914").valid()); + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("foo/PNOEE-30303039914").valid()); +} + +BOOST_AUTO_TEST_CASE(RejectsNonLetterCountryCode) +{ + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/PNO12-30303039914").valid()); + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/PNO-E-30303039914").valid()); + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/PNOE -30303039914").valid()); +} + +BOOST_AUTO_TEST_CASE(RejectsMissingSeparator) +{ + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/PNOEE.30303039914").valid()); + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/PNOEE/30303039914").valid()); + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/PNOEEX0303039914").valid()); +} + +BOOST_AUTO_TEST_CASE(RejectsNonDigitNationalId) +{ + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/PNOEE-30303039 14").valid()); + BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/PNOEE-3030303991a").valid()); + // Embedded NUL. + BOOST_CHECK(!libcdoc::parseEtsiRecipientId(std::string("etsi/PNOEE-3030\0039914", 22)).valid()); +} + +BOOST_AUTO_TEST_CASE(RejectsOversizedNationalId) +{ + // 32-byte national id is the documented upper bound; one byte more + // is rejected. + auto p32 = libcdoc::parseEtsiRecipientId("etsi/PNOEE-" + std::string(32, '1')); + BOOST_CHECK(p32.valid()); + auto p33 = libcdoc::parseEtsiRecipientId("etsi/PNOEE-" + std::string(33, '1')); + BOOST_CHECK(!p33.valid()); + auto pHuge = libcdoc::parseEtsiRecipientId("etsi/PNOEE-" + std::string(1024, '1')); + BOOST_CHECK(!pHuge.valid()); +} + +BOOST_AUTO_TEST_SUITE_END() From 08ca9e53d82497f1cecf0bea15798cba7241a582 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Thu, 11 Jun 2026 14:59:17 +0300 Subject: [PATCH 31/65] Make default KDF iter 600000 --- cdoc/CDocCipher.cpp | 2 +- cdoc/RcptInfo.h | 19 ++++++++++++++++++- cdoc/Utils.cpp | 5 ++--- .../src/main/java/ee/ria/cdoc/CDocTool.java | 4 ++-- test/libcdoc_boost.cpp | 4 ++-- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/cdoc/CDocCipher.cpp b/cdoc/CDocCipher.cpp index 6c7bcc94..89924b2d 100644 --- a/cdoc/CDocCipher.cpp +++ b/cdoc/CDocCipher.cpp @@ -389,7 +389,7 @@ fill_recipients_from_rcpt_info(ToolConf& conf, ToolCrypto& crypto, std::vector key_id; @@ -33,29 +35,44 @@ struct RcptInfo { }; enum Type { + // For decryption (use the lock type) LOCK, // For encryption // Certificate from file CERT, + // Password from command line PASSWORD, + // Symetric key from command line SKEY, + // Public key from command line PKEY, + // Symetric key from PKCS11 device P11_SYMMETRIC, + // Public key from PKC11 device P11_PKI, + // Windows NCRYPT, + // N of n SHARE }; Type type; + // Locks label std::string label; + // Certificate for encryption std::vector cert; + // Pin or password SecureBytes secret; + // PKCS11-specific info PKCS11Info p11; + // Keyfile name for automatic labels std::string key_file_name; + // ID code for shares server std::string id; + // Lock index int lock_idx = -1; - int resolved_lock_idx = -1; + bool isPKCS11() const { return p11.slot >= 0; } bool needPassword() const { return (type == PASSWORD || type == P11_SYMMETRIC || type == P11_PKI) && !secret.empty() && secret[0] == '?'; } }; diff --git a/cdoc/Utils.cpp b/cdoc/Utils.cpp index 4541c932..b422d596 100644 --- a/cdoc/Utils.cpp +++ b/cdoc/Utils.cpp @@ -117,12 +117,11 @@ parseURL(const std::string& url, std::string& host, int& port, std::string& path )) { return libcdoc::DATA_FORMAT_ERROR; } - bool is_https = (pssl == 1); - if (!is_https) { + if (!pssl) { OPENSSL_free(phost); OPENSSL_free(ppath); LOG_ERROR("URL scheme must be https: {}", url); - return libcdoc::DATA_FORMAT_ERROR; + return libcdoc::CONFIGURATION_ERROR; } host = phost; port = pport; diff --git a/examples/java/src/main/java/ee/ria/cdoc/CDocTool.java b/examples/java/src/main/java/ee/ria/cdoc/CDocTool.java index e2a2f7c5..2b0cf6be 100644 --- a/examples/java/src/main/java/ee/ria/cdoc/CDocTool.java +++ b/examples/java/src/main/java/ee/ria/cdoc/CDocTool.java @@ -301,7 +301,7 @@ static void encrypt(String file, String label, String password, CollectionfinishEncryption() == libcdoc::WORKFLOW_ERROR); // Add recipient - libcdoc::Recipient rcpt = libcdoc::Recipient::makeSymmetric("test-recipient", 65536); + libcdoc::Recipient rcpt = libcdoc::Recipient::makeSymmetric("test-recipient", 600000); BOOST_TEST(wrt->addRecipient(rcpt) == libcdoc::OK); // Encryption cannot proceed before beginEncryption is called BOOST_TEST(wrt->addFile("testfile", 1024) == libcdoc::WORKFLOW_ERROR); @@ -662,7 +662,7 @@ BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithPasswordAndLabel, FixtureBase, * u // Create writer libcdoc::CDocWriter *writer = libcdoc::CDocWriter::createWriter(2, &pipec, false, nullptr, &pcrypto, nullptr); BOOST_TEST(writer != nullptr); - libcdoc::Recipient rcpt = libcdoc::Recipient::makeSymmetric("test", 65536); + libcdoc::Recipient rcpt = libcdoc::Recipient::makeSymmetric("test", 600000); BOOST_TEST(writer->addRecipient(rcpt) == libcdoc::OK); BOOST_TEST(writer->beginEncryption() == libcdoc::OK); From 004ce5704f3b3c08d36ae2e450293ea5e7e4f214 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Tue, 16 Jun 2026 14:34:57 +0300 Subject: [PATCH 32/65] Added std_string_view.i --- std_string_view.i | 138 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 std_string_view.i diff --git a/std_string_view.i b/std_string_view.i new file mode 100644 index 00000000..51076bda --- /dev/null +++ b/std_string_view.i @@ -0,0 +1,138 @@ +/* ----------------------------------------------------------------------------- + * std_string_view.i + * + * Typemaps for std::string_view and const std::string_view& + * These are mapped to a Java String and are passed around by value. + * + * To use non-const std::string_view references use the following %apply. Note + * that they are passed by value. + * %apply const std::string_view & {std::string_view &}; + * ----------------------------------------------------------------------------- */ + +%{ +#include +#include +%} + +namespace std { + +%naturalvar string_view; + +class string_view; + +// string_view +%typemap(jni) string_view "jstring" +%typemap(jtype) string_view "String" +%typemap(jstype) string_view "String" +%typemap(javadirectorin) string_view "$jniinput" +%typemap(javadirectorout) string_view "$javacall" + +%typemap(in) string_view +%{ if(!$input) { + SWIG_JavaThrowException(jenv, SWIG_JavaNullPointerException, "null string"); + return $null; + } + const char *$1_pstr = jenv->GetStringUTFChars($input, 0); + if (!$1_pstr) return $null; + $1 = std::string_view($1_pstr); %} + +/* std::string_view requires the string data to remain valid while the + * string_view is in use. */ +%typemap(freearg) string_view +%{ jenv->ReleaseStringUTFChars($input, $1_pstr); %} + +%typemap(directorout,warning=SWIGWARN_TYPEMAP_THREAD_UNSAFE_MSG) string_view +%{ if(!$input) { + if (!jenv->ExceptionCheck()) { + SWIG_JavaThrowException(jenv, SWIG_JavaNullPointerException, "null string"); + } + return $null; + } + const char *$1_pstr = jenv->GetStringUTFChars($input, 0); + if (!$1_pstr) return $null; + /* possible thread/reentrant code problem */ + thread_local std::string $1_str; + $1_str = $1_pstr; + $result = std::string_view($1_str); + jenv->ReleaseStringUTFChars($input, $1_pstr); %} + +/* std::string_view::data() isn't zero-byte terminated, but NewStringUTF() + * requires a zero byte so it seems we have to make a copy (ick). The + * cleanest way to do that seems to be via a temporary std::string. + */ +%typemap(directorin,descriptor="Ljava/lang/String;") string_view +%{ $input = jenv->NewStringUTF(std::string($1).c_str()); + Swig::LocalRefGuard $1_refguard(jenv, $input); %} + +%typemap(out) string_view +%{ $result = jenv->NewStringUTF(std::string($1).c_str()); %} + +%typemap(javain) string_view "$javainput" + +%typemap(javaout) string_view { + return $jnicall; + } + +%typemap(typecheck) string_view = char *; + +%typemap(throws) string_view +%{ SWIG_JavaThrowException(jenv, SWIG_JavaRuntimeException, std::string($1).c_str()); + return $null; %} + +// const string_view & +%typemap(jni) const string_view & "jstring" +%typemap(jtype) const string_view & "String" +%typemap(jstype) const string_view & "String" +%typemap(javadirectorin) const string_view & "$jniinput" +%typemap(javadirectorout) const string_view & "$javacall" + +%typemap(in) const string_view & +%{ if(!$input) { + SWIG_JavaThrowException(jenv, SWIG_JavaNullPointerException, "null string"); + return $null; + } + const char *$1_pstr = jenv->GetStringUTFChars($input, 0); + if (!$1_pstr) return $null; + $*1_ltype $1_str($1_pstr); + $1 = &$1_str; %} + +/* std::string_view requires the string data to remain valid while the + * string_view is in use. */ +%typemap(freearg) const string_view & +%{ jenv->ReleaseStringUTFChars($input, $1_pstr); %} + +%typemap(directorout,warning=SWIGWARN_TYPEMAP_THREAD_UNSAFE_MSG) const string_view & +%{ if(!$input) { + SWIG_JavaThrowException(jenv, SWIG_JavaNullPointerException, "null string"); + return $null; + } + const char *$1_pstr = jenv->GetStringUTFChars($input, 0); + if (!$1_pstr) return $null; + /* possible thread/reentrant code problem */ + thread_local std::string $1_str; + $1_str = $1_pstr; + thread_local $*1_ltype $1_strview; + $1_strview = $1_str; + $result = &$1_strview; + jenv->ReleaseStringUTFChars($input, $1_pstr); %} + +%typemap(directorin,descriptor="Ljava/lang/String;") const string_view & +%{ $input = jenv->NewStringUTF(std::string($1).c_str()); + Swig::LocalRefGuard $1_refguard(jenv, $input); %} + +%typemap(out) const string_view & +%{ $result = jenv->NewStringUTF(std::string(*$1).c_str()); %} + +%typemap(javain) const string_view & "$javainput" + +%typemap(javaout) const string_view & { + return $jnicall; + } + +%typemap(typecheck) const string_view & = char *; + +%typemap(throws) const string_view & +%{ SWIG_JavaThrowException(jenv, SWIG_JavaRuntimeException, std::string($1).c_str()); + return $null; %} + +} From 212825a321c9be3ef081435e05731b15d77626e9 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Tue, 16 Jun 2026 15:32:05 +0300 Subject: [PATCH 33/65] Fixed label parsing on Ubuntu 22 --- cdoc/Lock.cpp | 9 +++++---- cdoc/utils/memory.h | 41 +++++++++++++++-------------------------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/cdoc/Lock.cpp b/cdoc/Lock.cpp index fecadc27..983e7c3c 100644 --- a/cdoc/Lock.cpp +++ b/cdoc/Lock.cpp @@ -102,12 +102,13 @@ Lock::parseLabel(const std::string& label) std::string key = urlDecode(range_to_sv(*it)); std::ranges::transform(key, key.begin(), [](unsigned char c){ return std::tolower(c); }); ++it; + // Ubuntu 22 ranges behave wrongly if (it == label_data_parts.end()) { - LOG_ERROR("The label '{}' has no value for key '{}'", label, key); - continue; + parsed_label[std::move(key)] = {}; + } else { + std::string value = urlDecode(range_to_sv(*it)); + parsed_label[std::move(key)] = std::move(value); } - std::string value = urlDecode(range_to_sv(*it)); - parsed_label[std::move(key)] = std::move(value); } return parsed_label; diff --git a/cdoc/utils/memory.h b/cdoc/utils/memory.h index 05fbca1c..fc8421e1 100644 --- a/cdoc/utils/memory.h +++ b/cdoc/utils/memory.h @@ -35,6 +35,20 @@ namespace libcdoc { +template +void cleanse(std::vector& v) noexcept +{ + if (!v.empty()) { + memset_s(v.data(), v.size() * sizeof(T), 0, v.size() * sizeof(T)); + } +} + +template +void cleanse(std::array& a) noexcept +{ + memset_s(a.data(), a.size() * sizeof(T), 0, a.size() * sizeof(T)); +} + class SecureBytes { std::vector data_; bool locked_ = false; @@ -159,19 +173,8 @@ class SecureBytes { data_.clear(); } - static inline void secure_cleanse(void* ptr, size_t len) noexcept { -#if defined(_WIN32) - SecureZeroMemory(ptr, len); -#else - volatile unsigned char* p = static_cast(ptr); - while (len--) *p++ = 0; -#endif - } - void cleanse() noexcept { - if (!data_.empty()) { - secure_cleanse(data_.data(), data_.size()); - } + ::libcdoc::cleanse(data_); } [[nodiscard]] operator const std::vector&() const noexcept { return data_; } @@ -186,20 +189,6 @@ class SecureBytes { } }; -template -void cleanse(std::vector& v) noexcept -{ - if (!v.empty()) { - OPENSSL_cleanse(v.data(), v.size() * sizeof(T)); - } -} - -template -void cleanse(std::array& a) noexcept -{ - OPENSSL_cleanse(a.data(), a.size() * sizeof(T)); -} - /** * @brief Scope guard that wipes a contiguous secret on destruction. * From 6db337271ba028fe07b89f8694e1dd513fb8b383 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 17 Jun 2026 09:41:09 +0300 Subject: [PATCH 34/65] Include --- cdoc/utils/memory.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cdoc/utils/memory.h b/cdoc/utils/memory.h index fc8421e1..e8801e41 100644 --- a/cdoc/utils/memory.h +++ b/cdoc/utils/memory.h @@ -18,6 +18,8 @@ #pragma once +#include + #include #include #include From 8a2f49479f906e59305aa977fdfc2705d59881c8 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 17 Jun 2026 10:01:56 +0300 Subject: [PATCH 35/65] Use explicit_bzero on glibc --- cdoc/utils/memory.h | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cdoc/utils/memory.h b/cdoc/utils/memory.h index e8801e41..684ca494 100644 --- a/cdoc/utils/memory.h +++ b/cdoc/utils/memory.h @@ -41,14 +41,24 @@ template void cleanse(std::vector& v) noexcept { if (!v.empty()) { +#if defined(__GLIBC__) + explicit_bzero(v.data(), v.size() * sizeof(T)); +#else memset_s(v.data(), v.size() * sizeof(T), 0, v.size() * sizeof(T)); +#endif } } template void cleanse(std::array& a) noexcept { - memset_s(a.data(), a.size() * sizeof(T), 0, a.size() * sizeof(T)); + if (!a.empty()) { +#if defined(__GLIBC__) + explicit_bzero(v.data(), v.size() * sizeof(T)); +#else + memset_s(a.data(), a.size() * sizeof(T), 0, a.size() * sizeof(T)); +#endif + } } class SecureBytes { From 573f14734a3fe1464108e0ca0081c34135cee15f Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 17 Jun 2026 10:30:31 +0300 Subject: [PATCH 36/65] Use SecureZeroMemory on windows --- cdoc/utils/memory.h | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cdoc/utils/memory.h b/cdoc/utils/memory.h index 684ca494..ef8b047b 100644 --- a/cdoc/utils/memory.h +++ b/cdoc/utils/memory.h @@ -35,17 +35,21 @@ #include #endif +#if defined(_WIN32) +#define libcdoc_zero SecureZeroMemory +#elif defined(__GLIBC__) +#define libcdoc_zero explicit_bzero +#else +#define libcdoc_zero(p,s) memset_s(p,s,0,s) +#endif + namespace libcdoc { template void cleanse(std::vector& v) noexcept { if (!v.empty()) { -#if defined(__GLIBC__) - explicit_bzero(v.data(), v.size() * sizeof(T)); -#else - memset_s(v.data(), v.size() * sizeof(T), 0, v.size() * sizeof(T)); -#endif + libcdoc_zero(v.data(), v.size() * sizeof(T)); } } @@ -53,11 +57,7 @@ template void cleanse(std::array& a) noexcept { if (!a.empty()) { -#if defined(__GLIBC__) - explicit_bzero(v.data(), v.size() * sizeof(T)); -#else - memset_s(a.data(), a.size() * sizeof(T), 0, a.size() * sizeof(T)); -#endif + libcdoc_zero(a.data(), a.size() * sizeof(T)); } } From 2d552cb1a832919a13ae628f9c6de276dfb253c8 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 17 Jun 2026 10:56:53 +0300 Subject: [PATCH 37/65] Use OPENSSL_cleanse for secure cleanup --- cdoc/CMakeLists.txt | 2 +- cdoc/utils/memory.h | 14 ++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/cdoc/CMakeLists.txt b/cdoc/CMakeLists.txt index b6ca68ef..44c87a68 100644 --- a/cdoc/CMakeLists.txt +++ b/cdoc/CMakeLists.txt @@ -102,7 +102,7 @@ target_link_libraries(cdoc PRIVATE if(BUILD_TOOLS) add_executable(cdoc-tool cdoc-tool.cpp) target_include_directories(cdoc-tool PRIVATE ${OPENSSL_INCLUDE_DIR}) - target_link_libraries(cdoc-tool cdoc_ver cdoc) + target_link_libraries(cdoc-tool cdoc_ver cdoc OpenSSL::SSL) target_link_options(cdoc-tool PRIVATE $<$: /MANIFEST:NO /MANIFEST:EMBED /MANIFESTINPUT:${CMAKE_CURRENT_SOURCE_DIR}/cdoc-tool.manifest> ) diff --git a/cdoc/utils/memory.h b/cdoc/utils/memory.h index ef8b047b..c441a5a2 100644 --- a/cdoc/utils/memory.h +++ b/cdoc/utils/memory.h @@ -18,8 +18,6 @@ #pragma once -#include - #include #include #include @@ -35,21 +33,13 @@ #include #endif -#if defined(_WIN32) -#define libcdoc_zero SecureZeroMemory -#elif defined(__GLIBC__) -#define libcdoc_zero explicit_bzero -#else -#define libcdoc_zero(p,s) memset_s(p,s,0,s) -#endif - namespace libcdoc { template void cleanse(std::vector& v) noexcept { if (!v.empty()) { - libcdoc_zero(v.data(), v.size() * sizeof(T)); + OPENSSL_cleanse(v.data(), v.size() * sizeof(T)); } } @@ -57,7 +47,7 @@ template void cleanse(std::array& a) noexcept { if (!a.empty()) { - libcdoc_zero(a.data(), a.size() * sizeof(T)); + OPENSSL_cleanse(a.data(), a.size() * sizeof(T)); } } From f63ed7775b147d204c42ccd74131a46870d45222 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 17 Jun 2026 13:15:42 +0300 Subject: [PATCH 38/65] Fixed inverted constant-time comparison --- cdoc/utils/ct.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cdoc/utils/ct.h b/cdoc/utils/ct.h index 2106100a..375a9721 100644 --- a/cdoc/utils/ct.h +++ b/cdoc/utils/ct.h @@ -45,11 +45,10 @@ constexpr uint8_t eq8(uint8_t a, uint8_t b) noexcept { // Returns 0xFF when a >= b, otherwise 0x00. Branch-free for size_t inputs. constexpr uint8_t ge_size(size_t a, size_t b) noexcept { - // (b - a - 1) wraps to a huge value when a >= b, putting 1 in the top - // bit. We sample the top bit, invert, and broadcast to a byte. + // (b - a - 1) wraps to a huge value when a >= b, putting 1 in the top bit constexpr size_t shift = sizeof(size_t) * 8u - 1u; size_t top_bit = (b - a - 1u) >> shift; // 1 if a < b, 0 if a >= b - return uint8_t((top_bit ^ 1u) * 0xFFu); + return uint8_t(top_bit * 0xFFu); } // Returns 0xFF when a == b, otherwise 0x00 (32-bit operands). From c0626a053ba7de20e339f17d05e3ab8e0450f59a Mon Sep 17 00:00:00 2001 From: Raul Metsma Date: Thu, 2 Jul 2026 12:27:38 +0300 Subject: [PATCH 39/65] Update cdoc/CDocCipher.cpp --- cdoc/CDocCipher.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdoc/CDocCipher.cpp b/cdoc/CDocCipher.cpp index 89924b2d..b98b938e 100644 --- a/cdoc/CDocCipher.cpp +++ b/cdoc/CDocCipher.cpp @@ -73,7 +73,7 @@ struct ToolPKCS11 : public libcdoc::PKCS11Backend { ToolPKCS11(const std::string& library, const CipherInfo& info) : PKCS11Backend(library), c_info(info) {} libcdoc::result_t connectToKey(int idx, bool priv) override final { - const libcdoc::RcptInfo *rcpt = c_info.getRcpt(idx); + const libcdoc::RcptInfo *rcpt = c_info.getRcpt(idx); if (!rcpt) return libcdoc::INTERNAL_ERROR; if (!priv) { return useSecretKey(long(rcpt->p11.slot), rcpt->secret, rcpt->p11.key_id, rcpt->p11.key_label); From eb091c3d6d2e0b1d817779ae46da8997e8cca4c0 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Thu, 2 Jul 2026 12:49:52 +0300 Subject: [PATCH 40/65] Fix build for SIDMID --- cdoc/CMakeLists.txt | 4 ++++ cdoc/NetworkBackend.cpp | 8 +++++++- cdoc/NetworkBackend.h | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cdoc/CMakeLists.txt b/cdoc/CMakeLists.txt index 44c87a68..806c531c 100644 --- a/cdoc/CMakeLists.txt +++ b/cdoc/CMakeLists.txt @@ -50,6 +50,7 @@ add_library(cdoc CDoc2Writer.cpp CDoc2Writer.h DDocReader.cpp DDocReader.h DDocWriter.cpp DDocWriter.h + KeyShares.cpp KeyShares.h # KeyShares.cpp KeyShares.h XmlReader.cpp XmlReader.h XmlWriter.cpp XmlWriter.h @@ -79,6 +80,9 @@ target_include_directories(cdoc PUBLIC PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ) +# Enable SID/MID +target_compile_definitions(cdoc PRIVATE HAS_KEYSHARES) + if(NOT BUILD_SHARED_LIBS) target_compile_definitions(cdoc PUBLIC cdoc_STATIC) endif() diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index dd01344f..c0b2263d 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -463,6 +463,12 @@ libcdoc::NetworkBackend::fetchKey (std::vector& dst, const std::string& } #ifdef HAS_KEYSHARES +libcdoc::result_t +libcdoc::NetworkBackend::authenticateForShares(std::vector& dst) +{ + return NOT_IMPLEMENTED; +} + libcdoc::result_t libcdoc::NetworkBackend::fetchNonce(std::vector& dst, const std::string& url, const std::string& share_id) { @@ -987,7 +993,7 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector LOG_DBG("Response: {}", rsp.body); picojson::value v; - parse_err = picojson::parse(v, rsp.body); + std::string parse_err = picojson::parse(v, rsp.body); if (!parse_err.empty()) { error = FORMAT("JSON parse error: {}", parse_err); LOG_ERROR("{}", error); diff --git a/cdoc/NetworkBackend.h b/cdoc/NetworkBackend.h index 0666f210..390c06f9 100644 --- a/cdoc/NetworkBackend.h +++ b/cdoc/NetworkBackend.h @@ -181,6 +181,7 @@ struct CDOC_EXPORT NetworkBackend { */ virtual result_t fetchKey (std::vector& dst, const std::string& url, const std::string& transaction_id); #ifdef HAS_KEYSHARES + virtual result_t authenticateForShares(std::vector& dst); /** * @brief fetch authentication nonce from share server * @param dst a destination container for nonce From 0e544fcbe6850c020cc94fc58ce91c9d75f3e23c Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Thu, 2 Jul 2026 14:11:32 +0300 Subject: [PATCH 41/65] Fixed PKCS11 and NCrypt RSA handling and check all random() calls for success --- cdoc/CDoc2Writer.cpp | 21 +++++++++++++++------ cdoc/Crypto.cpp | 38 ++++++++++++++++++++++++++++++++----- cdoc/Crypto.h | 4 ++++ cdoc/KeyShares.cpp | 8 ++++++-- cdoc/NetworkBackend.cpp | 10 ++++++++-- cdoc/PKCS11Backend.cpp | 29 ++++------------------------ cdoc/WinBackend.cpp | 42 +++++++---------------------------------- 7 files changed, 77 insertions(+), 75 deletions(-) diff --git a/cdoc/CDoc2Writer.cpp b/cdoc/CDoc2Writer.cpp index e480d522..5247d05e 100644 --- a/cdoc/CDoc2Writer.cpp +++ b/cdoc/CDoc2Writer.cpp @@ -80,7 +80,10 @@ CDoc2Writer::writeHeader(const std::vector &recipients) dst->write(headerHMAC.data(), headerHMAC.size()); std::vector nonce; - crypto->random(nonce, libcdoc::CDoc2::NONCE_LEN); + if (auto rv = crypto->random(nonce, libcdoc::CDoc2::NONCE_LEN); rv < 0) + return rv; + if (nonce.size() != libcdoc::CDoc2::NONCE_LEN) + FAIL("RNG failure: nonce too short", libcdoc::CRYPTO_ERROR); LOG_TRACE_KEY("nonce: {}", nonce); auto cipher = std::make_unique(*dst, EVP_chacha20_poly1305(), Crypto::Key(std::move(cek), std::move(nonce))); for(const auto &aad: {libcdoc::CDoc2::PAYLOAD, std::move(header), std::move(headerHMAC)}) { @@ -236,7 +239,8 @@ CDoc2Writer::buildHeader(std::vector& header, const std::vectorrandom(kek, libcdoc::CDoc2::KEY_LEN); + if (auto rv = crypto->random(kek, libcdoc::CDoc2::KEY_LEN); rv < 0) + FAIL("RNG failure", rv); if (auto err = libcdoc::Crypto::xor_data(xor_key, fmk, kek); err != libcdoc::OK) FAIL("Internal error", err); auto publicKey = libcdoc::Crypto::fromRSAPublicKeyDer(rcpt.rcpt_key); @@ -356,11 +360,15 @@ CDoc2Writer::buildHeader(std::vector& header, const std::vector key_material_salt; - crypto->random(key_material_salt, libcdoc::CDoc2::KEY_LEN); + if (auto rv = crypto->random(key_material_salt, libcdoc::CDoc2::KEY_LEN); rv < 0) + FAIL("RNG failure", rv); //KeyMaterial_i = CSRNG(256) std::vector key_material; - crypto->random(key_material, libcdoc::CDoc2::KEY_LEN); + if (auto rv = crypto->random(key_material, libcdoc::CDoc2::KEY_LEN); rv < 0) { + libcdoc::cleanse(key_material_salt); + FAIL("RNG failure", rv); + } // key_material is split-share-input material; wipe on exit. libcdoc::Cleanser key_material_guard(key_material); @@ -395,7 +403,8 @@ CDoc2Writer::buildHeader(std::vector& header, const std::vectorrandom(kek_shares[i], libcdoc::CDoc2::KEY_LEN); + if (auto rv = crypto->random(kek_shares[i], libcdoc::CDoc2::KEY_LEN); rv < 0) + FAIL("RNG failure", rv); } // KEK_i_share_1 = XOR(KEK_i, KEK_i_share_2, KEK_i_share_3,..., KEK_i_share_n) kek_shares[0] = std::move(kek); @@ -410,7 +419,7 @@ CDoc2Writer::buildHeader(std::vector& header, const std::vector> transaction_ids(N_SHARES); for (int i = 0; i < N_SHARES; i++) { std::string send_url = urls[i]; - LOG_TRACE("Sending share: {} {} {}", i, send_url, libcdoc::toHex(kek_shares[i])); + LOG_TRACE_KEY("Sending share: {} {} {}", i, send_url, libcdoc::toHex(kek_shares[i])); int result = network->sendShare(transaction_ids[i], send_url, RecipientInfo_i, kek_shares[i]); if (result < 0) FAIL(network->getLastErrorStr(result), result); diff --git a/cdoc/Crypto.cpp b/cdoc/Crypto.cpp index daa72e70..bcf6314e 100644 --- a/cdoc/Crypto.cpp +++ b/cdoc/Crypto.cpp @@ -639,6 +639,34 @@ void unpadPKCS1v15CT(const std::vector &em, } // anonymous namespace +std::vector Crypto::syntheticPlaintextFromEM(const std::vector& em, + const std::vector& ct, + size_t out_len) +{ + if (em.empty() || ct.empty() || out_len == 0) + return std::vector(out_len, 0); + + std::vector seed_key; + { + const std::string_view tag{"cdoc1-rsa-implicit-reject"}; + seed_key.reserve(tag.size() + em.size()); + seed_key.insert(seed_key.end(), tag.begin(), tag.end()); + seed_key.insert(seed_key.end(), em.begin(), em.end()); + } + std::vector prk = Crypto::sign_hmac(seed_key, ct); + libcdoc::cleanse(seed_key); + if (prk.empty()) + return std::vector(out_len, 0); + + auto synth = Crypto::expand(prk, "cdoc1-rsa-implicit-reject", int(out_len)); + libcdoc::cleanse(prk); + if (synth.size() != out_len) { + libcdoc::cleanse(synth); + return std::vector(out_len, 0); + } + return synth; +} + int Crypto::rsaImplicitRejectFromEM(std::vector& dst, const std::vector& em, const std::vector& /*ct*/, @@ -646,11 +674,11 @@ int Crypto::rsaImplicitRejectFromEM(std::vector& dst, size_t expected_len) { // The caller passes a key-derived synthetic seed already sized to - // `expected_len`. We don't recompute it here so that PKCS#11 / CNG - // callers who only have access to a public key (the private key never - // leaves the token) can still produce a stable synthetic output by - // seeding from any private-key-derived material they have - typically - // the certificate fingerprint plus the ciphertext. + // `expected_len`. For token backends (PKCS#11, CNG) the seed is + // produced by syntheticPlaintextFromEM(); for the software path by + // syntheticPlaintext() to be consistent with OpenSSL implementation. + // Both derive from private-key-dependent + // material that the caller has access to. if (synth_seed.size() != expected_len) return CRYPTO_ERROR; diff --git a/cdoc/Crypto.h b/cdoc/Crypto.h index 03f93789..58a7e346 100644 --- a/cdoc/Crypto.h +++ b/cdoc/Crypto.h @@ -182,6 +182,10 @@ class Crypto * @param expected_len length of plaintext the caller expects to receive * @return OK on success */ + static std::vector syntheticPlaintextFromEM(const std::vector& em, + const std::vector& ct, + size_t out_len); + static int rsaImplicitRejectFromEM(std::vector& dst, const std::vector& em, const std::vector& ct, diff --git a/cdoc/KeyShares.cpp b/cdoc/KeyShares.cpp index 7e77aa93..23789012 100644 --- a/cdoc/KeyShares.cpp +++ b/cdoc/KeyShares.cpp @@ -95,7 +95,9 @@ struct Disclosure { Disclosure::Disclosure(const std::string name, const std::string& val) { - salt64 = toBase64URL(libcdoc::Crypto::random(16)); + auto rand_bytes = libcdoc::Crypto::random(16); + if (rand_bytes.empty()) return; + salt64 = toBase64URL(rand_bytes); // // [SALT, HASH] // [SALT, NAME, HASH] @@ -118,7 +120,9 @@ Disclosure::Disclosure(const std::string name, const std::string& val) Disclosure::Disclosure(const std::string name, std::vector& val) { - salt64 = toBase64URL(libcdoc::Crypto::random(16)); + auto rand_bytes = libcdoc::Crypto::random(16); + if (rand_bytes.empty()) return; + salt64 = toBase64URL(rand_bytes); // // [SALT, [{..., HASH}, {..., HASH}...] // [SALT, NAME, [{..., HASH}, {..., HASH}...] diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index f3569a25..27ccbb7c 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -748,7 +748,10 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector const std::string& rcpt_id, const std::vector& digest, CryptoBackend::HashAlgorithm algo) { std::string certificateLevel = "QUALIFIED"; - std::string nonce = libcdoc::toBase64(Crypto::random(16)); + auto nonce_bytes = Crypto::random(16); + if (nonce_bytes.empty()) + return libcdoc::CRYPTO_ERROR; + std::string nonce = libcdoc::toBase64(nonce_bytes); picojson::object obj = { {"relyingPartyUUID", picojson::value(rp_uuid)}, @@ -926,7 +929,10 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector } std::string certificateLevel = "QUALIFIED"; - std::string nonce = libcdoc::toBase64(Crypto::random(16)); + auto nonce_bytes = Crypto::random(16); + if (nonce_bytes.empty()) + return libcdoc::CRYPTO_ERROR; + std::string nonce = libcdoc::toBase64(nonce_bytes); std::string host, path; int port; diff --git a/cdoc/PKCS11Backend.cpp b/cdoc/PKCS11Backend.cpp index 90d996e4..db1f154d 100644 --- a/cdoc/PKCS11Backend.cpp +++ b/cdoc/PKCS11Backend.cpp @@ -469,31 +469,10 @@ libcdoc::PKCS11Backend::decryptRSA(std::vector &dst, const std::vector< em.resize(size_t(em_size)); d->logout(); - // Build a synthetic seed that does not require access to the private - // key (which never leaves the token). HMAC the ciphertext with a - // public-but-token-bound value (CKA_ID concatenated with the modulus) - // so the seed is stable per (token-key, ct) pair while still being - // unpredictable to attackers. - std::vector seed_key; - { - std::vector id_attr = d->attribute(d->session, d->key, CKA_ID); - std::vector mod_attr = d->attribute(d->session, d->key, CKA_MODULUS); - seed_key.reserve(id_attr.size() + mod_attr.size() + 16); - const std::string_view tag{"cdoc1-rsa-implicit-reject-pkcs11"}; - seed_key.insert(seed_key.end(), tag.begin(), tag.end()); - seed_key.insert(seed_key.end(), id_attr.begin(), id_attr.end()); - seed_key.insert(seed_key.end(), mod_attr.begin(), mod_attr.end()); - } - std::vector prk = libcdoc::Crypto::sign_hmac(seed_key, data); - libcdoc::cleanse(seed_key); - std::vector synth = libcdoc::Crypto::expand( - prk, "cdoc1-rsa-implicit-reject", int(dst.size())); - libcdoc::cleanse(prk); - if (synth.size() != dst.size()) { - // Last-resort fallback: fixed zero seed. Worse than ideal but still - // length-uniform with the real-success path. - synth.assign(dst.size(), 0); - } + // Derive a per-(key, ct) synthetic plaintext from the raw RSA + // output (EM). EM is private-key-dependent and unpredictable to + // attackers who do not know the private key. + std::vector synth = libcdoc::Crypto::syntheticPlaintextFromEM(em, data, dst.size()); int rv = libcdoc::Crypto::rsaImplicitRejectFromEM(dst, em, data, synth, dst.size()); libcdoc::cleanse(em); diff --git a/cdoc/WinBackend.cpp b/cdoc/WinBackend.cpp index 9fb9634c..5aa62c42 100644 --- a/cdoc/WinBackend.cpp +++ b/cdoc/WinBackend.cpp @@ -240,38 +240,10 @@ libcdoc::WinBackend::decryptRSA(std::vector& dst, const std::vector seed_key; - { - DWORD blob_size = 0; - if (NCryptExportKey(d->key, 0, BCRYPT_RSAPUBLIC_BLOB, nullptr, nullptr, 0, &blob_size, 0) == ERROR_SUCCESS && - blob_size > 0) { - std::vector blob(blob_size, 0); - if (NCryptExportKey(d->key, 0, BCRYPT_RSAPUBLIC_BLOB, nullptr, blob.data(), blob_size, &blob_size, 0) == ERROR_SUCCESS) { - blob.resize(blob_size); - const std::string_view tag{"cdoc1-rsa-implicit-reject-cng"}; - seed_key.reserve(tag.size() + blob.size()); - seed_key.insert(seed_key.end(), tag.begin(), tag.end()); - seed_key.insert(seed_key.end(), blob.begin(), blob.end()); - } - } - // If export failed, fall back to a fixed tag - still length-uniform - // but slightly less unpredictable. Better than leaking the failure. - if (seed_key.empty()) { - const std::string_view tag{"cdoc1-rsa-implicit-reject-cng-fallback"}; - seed_key.assign(tag.begin(), tag.end()); - } - } - std::vector prk = libcdoc::Crypto::sign_hmac(seed_key, data); - libcdoc::cleanse(seed_key); - std::vector synth = libcdoc::Crypto::expand(prk, "cdoc1-rsa-implicit-reject", int(dst.size())); - libcdoc::cleanse(prk); - if (synth.size() != dst.size()) - synth.assign(dst.size(), 0); + // Derive a per-(key, ct) synthetic plaintext from the raw RSA + // output (EM). EM is private-key-dependent and unpredictable to + // attackers who do not know the private key. + std::vector synth = libcdoc::Crypto::syntheticPlaintextFromEM(em, data, dst.size()); int rv = libcdoc::Crypto::rsaImplicitRejectFromEM(dst, em, data, synth, dst.size()); libcdoc::cleanse(em); @@ -415,11 +387,11 @@ libcdoc::WinBackend::sign(std::vector& dst, HashAlgorithm algorithm, co BCRYPT_PSS_PADDING_INFO rsaPSS { BCRYPT_SHA256_ALGORITHM, 32 }; switch(algorithm) { case libcdoc::CryptoBackend::HashAlgorithm::SHA_256: - rsaPSS = { BCRYPT_SHA256_ALGORITHM, 32 }; break; + rsaPSS = { NCRYPT_SHA256_ALGORITHM, 32 }; break; case libcdoc::CryptoBackend::HashAlgorithm::SHA_384: - rsaPSS = { BCRYPT_SHA384_ALGORITHM, 48 }; break; + rsaPSS = { NCRYPT_SHA384_ALGORITHM, 48 }; break; case libcdoc::CryptoBackend::HashAlgorithm::SHA_512: - rsaPSS = { BCRYPT_SHA512_ALGORITHM, 64 }; break; + rsaPSS = { NCRYPT_SHA512_ALGORITHM, 64 }; break; case libcdoc::CryptoBackend::HashAlgorithm::SHA_224: // SHA-224 is not supported by CNG's RSA-PSS implementation. LOG_ERROR("WinBackend: RSA-PSS with SHA-224 is not supported by CNG"); From 364167915aeddbdc9ebce2ef2c480cb8981c5da1 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 3 Jul 2026 13:20:48 +0300 Subject: [PATCH 42/65] Fix share encryption --- cdoc/CDoc2Writer.cpp | 8 ++++++++ cdoc/CMakeLists.txt | 4 ++++ cdoc/Recipient.cpp | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/cdoc/CDoc2Writer.cpp b/cdoc/CDoc2Writer.cpp index e480d522..c9da99be 100644 --- a/cdoc/CDoc2Writer.cpp +++ b/cdoc/CDoc2Writer.cpp @@ -470,6 +470,14 @@ CDoc2Writer::addRecipient(const libcdoc::Recipient& rcpt) if(!rcpt.validate()) FAIL("Invalid recipient parameters", libcdoc::WRONG_ARGUMENTS); break; +#ifdef HAS_KEYSHARES + case Recipient::KEYSHARE: + if (!network) + FAIL("KeyShares require NetworkBackend", libcdoc::WORKFLOW_ERROR); + if (!rcpt.validate()) + FAIL("Invalid recipient parameters", libcdoc::WRONG_ARGUMENTS); + break; +#endif default: FAIL("Invalid recipient type", WRONG_ARGUMENTS); } diff --git a/cdoc/CMakeLists.txt b/cdoc/CMakeLists.txt index 806c531c..d82d3c4f 100644 --- a/cdoc/CMakeLists.txt +++ b/cdoc/CMakeLists.txt @@ -106,6 +106,10 @@ target_link_libraries(cdoc PRIVATE if(BUILD_TOOLS) add_executable(cdoc-tool cdoc-tool.cpp) target_include_directories(cdoc-tool PRIVATE ${OPENSSL_INCLUDE_DIR}) + + # Enable SID/MID + target_compile_definitions(cdoc-tool PRIVATE HAS_KEYSHARES) + target_link_libraries(cdoc-tool cdoc_ver cdoc OpenSSL::SSL) target_link_options(cdoc-tool PRIVATE $<$: /MANIFEST:NO /MANIFEST:EMBED /MANIFESTINPUT:${CMAKE_CURRENT_SOURCE_DIR}/cdoc-tool.manifest> diff --git a/cdoc/Recipient.cpp b/cdoc/Recipient.cpp index 89e4f4c1..0274dc70 100644 --- a/cdoc/Recipient.cpp +++ b/cdoc/Recipient.cpp @@ -237,6 +237,11 @@ Recipient::validate() const case PUBLIC_KEY: // Public key should not be empty return !rcpt_key.empty(); +#ifdef HAS_KEYSHARES + case KEYSHARE: + // Server ID and recipient ID should not be empty + return !server_id.empty() && !id.empty(); +#endif default: return false; } From 3e62abeb96cd19458ec56edf61bcf82085926a63 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 3 Jul 2026 13:59:36 +0300 Subject: [PATCH 43/65] Fixed PKCS11 public key loading --- cdoc/PKCS11Backend.cpp | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/cdoc/PKCS11Backend.cpp b/cdoc/PKCS11Backend.cpp index db1f154d..7a2bf1c7 100644 --- a/cdoc/PKCS11Backend.cpp +++ b/cdoc/PKCS11Backend.cpp @@ -29,6 +29,7 @@ #define OPENSSL_SUPPRESS_DEPRECATED +#include #include #include #include @@ -379,8 +380,8 @@ libcdoc::PKCS11Backend::getPublicKey(std::vector& val, int slot, const return CRYPTO_ERROR; } std::vector w = d->attribute(d->session, handle, CKA_EC_POINT); - if (w.size() < 2) { - LOG_DBG("PKCS11: getValue CKA_EC_POINT too short"); + if (w.empty()) { + LOG_DBG("PKCS11: getValue CKA_EC_POINT empty"); d->logout(); return CRYPTO_ERROR; } @@ -396,7 +397,33 @@ libcdoc::PKCS11Backend::getPublicKey(std::vector& val, int slot, const EC_GROUP_free(group); return CRYPTO_ERROR; } - if (EC_POINT_oct2point(group, pub_key_point, w.data() + 2, w.size() - 2, NULL) != 1) { + // CKA_EC_POINT is DER-encoded per PKCS#11: an OCTET STRING TLV wrapping + // the ANSI X9.62 point. Parse the TLV with ASN1_get_object to extract the + // payload rather than blindly skipping 2 bytes (wrong for lengths >= 128, + // and wrong for tokens that omit the TLV and return raw point bytes). + const uint8_t *point_buf = nullptr; + long point_len = 0; + bool parsed = false; + { + const unsigned char *pp = w.data(); + long plen = long(w.size()); + int ptag = 0, pclass = 0; + long payload_len = 0; + int ret = ASN1_get_object(&pp, &payload_len, &ptag, &pclass, plen); + if (ret >= 0 && ptag == V_ASN1_OCTET_STRING && pclass == V_ASN1_UNIVERSAL + && payload_len > 0 && (pp + payload_len) <= (w.data() + plen)) { + point_buf = pp; + point_len = payload_len; + parsed = true; + } + } + if (!parsed) { + // Fallback: some tokens return raw point bytes (0x04 || x || y) + // without DER OCTET STRING wrapping. + point_buf = w.data(); + point_len = long(w.size()); + } + if (EC_POINT_oct2point(group, pub_key_point, point_buf, size_t(point_len), NULL) != 1) { LOG_DBG("PKCS11: EC_POINT_oct2point error"); EC_POINT_free(pub_key_point); EC_GROUP_free(group); From ed722213895336cdc8686124c7e0a5bdf04256ca Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 15 Jul 2026 14:41:00 +0300 Subject: [PATCH 44/65] fixes --- cdoc/CDoc2Reader.cpp | 10 ++++++++++ cdoc/NetworkBackend.cpp | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index caf6a75b..9cc9782d 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -254,6 +254,16 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) LOG_ERROR("{}", last_error); return libcdoc::DATA_FORMAT_ERROR; } + + // Get authentication token + std::vector auth_token; + if (auto rv = network->authenticateForShares(auth_token); rv != OK) { + setLastError(network->getLastErrorStr(rv)); + LOG_ERROR("{}", last_error); + return rv; + } + return NOT_IMPLEMENTED; + std::vector shares; for (auto& str : strs) { std::vector parts = split(str, ','); diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 39eae5ad..788c63ff 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -466,6 +466,43 @@ libcdoc::NetworkBackend::fetchKey (std::vector& dst, const std::string& libcdoc::result_t libcdoc::NetworkBackend::authenticateForShares(std::vector& dst) { + static const std::string url = "https://cdoc2-auth.test.riaint.ee"; + // Start authentication + std::string host, path; + int port; + int result = libcdoc::parseURL(url, host, port, path); + if (result != libcdoc::OK) return result; + + LOG_DBG("Starting client: {} {}", host, port); + httplib::SSLClient cli(host, port); + if (result = applySSLTimeout(cli, this); result != OK) return result; + result = setPeerCertificates(cli, this, buildURL(host, port)); + if (result != OK) return result; + if (result = setProxy(cli, this); result != OK) return result; + + picojson::object obj = { + {"identifier", picojson::value("etsi/PNOEE-37104082710")}, + }; + picojson::value req_json(obj); + std::string req_str = req_json.serialize(); + LOG_DBG("POST authentication request to: {}", url); + LOG_DBG("{}", req_str); + + std::string full = path + "/auth/start"; + httplib::Headers hdrs; + httplib::Response rsp; + result = post(cli, full, hdrs, req_str, rsp); + if (result != libcdoc::OK) return result; + + LOG_DBG("Response: {}", rsp.body); + picojson::value rsp_json; + std::string parse_err = picojson::parse(rsp_json, rsp.body); + if (!parse_err.empty()) { + error = FORMAT("JSON parse error: {}", parse_err); + LOG_ERROR("{}", error); + return NETWORK_ERROR; + } + return NOT_IMPLEMENTED; } From 57694470727384a42b77c8830baf4c350b3b0b69 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 15 Jul 2026 16:05:24 +0300 Subject: [PATCH 45/65] Fixed label UTF-8 escaping if locale is not C --- cdoc/Utils.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cdoc/Utils.cpp b/cdoc/Utils.cpp index b422d596..9495e2d0 100644 --- a/cdoc/Utils.cpp +++ b/cdoc/Utils.cpp @@ -26,6 +26,7 @@ #include #include +#include namespace libcdoc { @@ -145,6 +146,7 @@ buildURL(const std::string& host, int port) std::ostream& operator<<(std::ostream& escaped, urlEncode src) { + static const std::locale locC("C"); restoreFlags rf(escaped); escaped.fill('0'); escaped << std::hex; @@ -155,7 +157,7 @@ operator<<(std::ostream& escaped, urlEncode src) continue; } // Keep alphanumeric and other accepted characters intact - if (isalnum(uint8_t(c)) || c == '-' || c == '_' || c == '.' || c == '~') { + if (std::isalnum(c, locC) || c == '-' || c == '_' || c == '.' || c == '~') { escaped << c; continue; } From 12253559e0c1c2cd49e69f43e13167371ac71ee9 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 15 Jul 2026 16:07:37 +0300 Subject: [PATCH 46/65] Fixed label generation for non-C locales --- cdoc/Utils.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cdoc/Utils.cpp b/cdoc/Utils.cpp index b422d596..9495e2d0 100644 --- a/cdoc/Utils.cpp +++ b/cdoc/Utils.cpp @@ -26,6 +26,7 @@ #include #include +#include namespace libcdoc { @@ -145,6 +146,7 @@ buildURL(const std::string& host, int port) std::ostream& operator<<(std::ostream& escaped, urlEncode src) { + static const std::locale locC("C"); restoreFlags rf(escaped); escaped.fill('0'); escaped << std::hex; @@ -155,7 +157,7 @@ operator<<(std::ostream& escaped, urlEncode src) continue; } // Keep alphanumeric and other accepted characters intact - if (isalnum(uint8_t(c)) || c == '-' || c == '_' || c == '.' || c == '~') { + if (std::isalnum(c, locC) || c == '-' || c == '_' || c == '.' || c == '~') { escaped << c; continue; } From 1df498c39af0d6bbca740d21537f972b45850891 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 24 Jul 2026 14:21:38 +0300 Subject: [PATCH 47/65] Working authentication --- cdoc/CDoc2Reader.cpp | 30 +-- cdoc/KeyShares.cpp | 60 +++++- cdoc/KeyShares.h | 30 ++- cdoc/NetworkBackend.cpp | 420 ++++++++++++++++++++++++++++++---------- cdoc/NetworkBackend.h | 8 +- cdoc/Utils.cpp | 8 + cdoc/Utils.h | 24 +-- 7 files changed, 435 insertions(+), 145 deletions(-) diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index 9cc9782d..ef39b378 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -248,25 +248,26 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) /* SHARE_URLS */ /* url,share_id;url,share_id... */ std::string all = lock.getString(Lock::SHARE_URLS); - std::vector strs = split(all, ';'); - if (strs.empty()){ + std::vector servers = split(all, ';'); + if (servers.empty()){ setLastError("Lock does not contain server info"); LOG_ERROR("{}", last_error); return libcdoc::DATA_FORMAT_ERROR; } // Get authentication token - std::vector auth_token; - if (auto rv = network->authenticateForShares(auth_token); rv != OK) { + std::string auth_token; + std::string auth_cert; + if (auto rv = network->authenticateForShares(auth_token, auth_cert); rv != OK) { setLastError(network->getLastErrorStr(rv)); LOG_ERROR("{}", last_error); return rv; } - return NOT_IMPLEMENTED; + // Get nonces and initialize share array std::vector shares; - for (auto& str : strs) { - std::vector parts = split(str, ','); + for (auto& server : servers) { + std::vector parts = split(server, ','); if (parts.size() != 2) { setLastError("Invalid server info in lock"); LOG_ERROR("{}", last_error); @@ -277,7 +278,7 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) LOG_DBG("Share {} url {}", id, url); std::vector nonce; - result_t result = network->fetchNonce(nonce, url, id); + result_t result = network->fetchNonce(nonce, url, id, auth_token, auth_cert); if (result != libcdoc::OK) { setLastError(network->getLastErrorStr(result)); LOG_ERROR("Cannot fetch nonce from server {}", url); @@ -287,20 +288,23 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) ShareData acc(url, id, std::string(nonce.cbegin(), nonce.cend())); shares.push_back(std::move(acc)); } + /* Create tickets from shares */ std::vector tickets; std::vector cert; result_t result = NOT_IMPLEMENTED; - std::string signer = conf->getValue(Configuration::SHARE_SIGNER); + // fixme: + std::string signer = "SMART_ID";// conf->getValue(Configuration::SHARE_SIGNER); LOG_DBG("Signer: {}", signer); if (signer == "SMART_ID") { // "https://sid.demo.sk.ee/smart-id-rp/v2" - std::string url = conf->getValue(Configuration::SID_DOMAIN, Configuration::BASE_URL); + // std::string url = conf->getValue(Configuration::SID_DOMAIN, Configuration::BASE_URL); + std::string url = "https://cdoc2-rp.test.riaint.ee"; // "00000000-0000-0000-0000-000000000000" - std::string relyingPartyUUID = conf->getValue(Configuration::SID_DOMAIN, Configuration::RP_UUID); + //std::string relyingPartyUUID = conf->getValue(Configuration::SID_DOMAIN, Configuration::RP_UUID); // "DEMO" - std::string relyingPartyName = conf->getValue(Configuration::SID_DOMAIN, Configuration::RP_NAME); - SIDSigner signer(url, relyingPartyUUID, relyingPartyName, rcpt_id, network); + //std::string relyingPartyName = conf->getValue(Configuration::SID_DOMAIN, Configuration::RP_NAME); + SIDSigner signer(url, auth_token, auth_cert, rcpt_id, network); result = signer.generateTickets(tickets, shares); if (result != OK) { setLastError(signer.error); diff --git a/cdoc/KeyShares.cpp b/cdoc/KeyShares.cpp index 23789012..4ddae332 100644 --- a/cdoc/KeyShares.cpp +++ b/cdoc/KeyShares.cpp @@ -210,7 +210,7 @@ SIDSigner::signDigest(std::vector& dst, const std::vector& dig { LOG_TRACE_KEY("SID signing: {}", digest); - result_t result = network->signSID(dst, cert, url, rp_uuid, rp_name, rcpt_id, digest, libcdoc::CryptoBackend::SHA_256); + result_t result = network->signSID(dst, cert, url, session_token, auth_cert, rcpt_id, digest, libcdoc::CryptoBackend::SHA_256); if (result != OK) { error = network->getLastErrorStr(result); } @@ -240,6 +240,64 @@ libcdoc::MIDSigner::signDigest(std::vector& dst, const std::vector 2) { + jwt = parts[0]; + aud = parts[1]; + for (size_t i = 2; i < parts.size(); i++) { + disclosures.push_back(parts[i]); + } + } +} + +std::string +SessionToken::discloseForUrl(std::string_view url) +{ + LOG_DBG("Building token for: {}", url); + for (auto& d : disclosures) { + std::vector decoded_part = fromBase64URL(d); + std::string json_str(decoded_part.begin(), decoded_part.end()); + picojson::value json; + if (!picojson::parse(json, json_str).empty()) { + return {}; + } + if (!json.is()) { + return {}; + } + picojson::array arr = json.get(); + if (arr.size() < 2) continue; + if (!arr[1].is()) { + return {}; + } + std::string target_url = arr[1].get(); + if (target_url.find(url) != std::string::npos) { + std::string token = jwt + "~" + aud + "~" + d + "~"; + LOG_DBG("Disclosed token: {}", token); + return token; + } + } + return {}; +} + +std::string +decodeTicket(const std::string& ticket) +{ + auto decoded = jwt::decode(ticket); + auto a = decoded.get_header_json(); + for (auto t : a) { + LOG_DBG("Header {}: {}", t.first, t.second.to_str()); + } + a = decoded.get_payload_json(); + for (auto t : a) { + LOG_DBG("Payload {}: {}", t.first, t.second.to_str()); + } + auto b = decoded.get_signature(); + LOG_DBG("Signature: {}", b); + return picojson::value(decoded.get_payload_json()).serialize(); +} + } // namespace libcdoc diff --git a/cdoc/KeyShares.h b/cdoc/KeyShares.h index 219fc0a3..86885ba1 100644 --- a/cdoc/KeyShares.h +++ b/cdoc/KeyShares.h @@ -121,15 +121,13 @@ struct SIDSigner : public Signer { */ const std::string url; /** - * @brief Relying party UUID + * @brief Full session token * */ - const std::string rp_uuid; - /** - * @brief Relying party name - * - */ - const std::string rp_name; + // fixme: parsed? + const std::string session_token; + // fixme: + const std::string auth_cert; /** * @brief Construct a new SIDSigner object * @@ -138,8 +136,8 @@ struct SIDSigner : public Signer { * @param _rp_name Relying party name * @param _rcpt_id Recipient full id in etsi format (ets/PNOEE-XYZXYZXYZXY) */ - SIDSigner(const std::string& _url, const std::string& _rp_uuid, const std::string& _rp_name, const std::string& _rcpt_id, NetworkBackend *network) - : Signer(_rcpt_id, "RS256", network), url(_url), rp_uuid(_rp_uuid), rp_name(_rp_name) {} + SIDSigner(const std::string& _url, const std::string& _session_token, const std::string& _cert, const std::string& _rcpt_id, NetworkBackend *network) + : Signer(_rcpt_id, "RS256", network), url(_url), session_token(_session_token), auth_cert(_cert) {} result_t signDigest(std::vector& dst, const std::vector& digest) final; }; @@ -183,6 +181,18 @@ struct MIDSigner : public Signer { result_t signDigest(std::vector& dst, const std::vector& digest) final; }; +struct SessionToken { + std::string jwt; + std::string aud; + std::vector disclosures; + // fixme: Keep parsed data? + + SessionToken(std::string_view str); + std::string discloseForUrl(std::string_view url); +}; + +std::string decodeTicket(const std::string& ticket); + } // namespace libcdoc -#endif // LOCK_H +#endif // KEYSHARES_H diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 788c63ff..dc7002ce 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -22,6 +22,7 @@ #include "Crypto.h" #include "CryptoBackend.h" #include "Utils.h" +#include "KeyShares.h" #define OPENSSL_SUPPRESS_DEPRECATED @@ -156,18 +157,18 @@ static constexpr std::string_view hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm algo) noexcept { switch (algo) { - case libcdoc::CryptoBackend::HashAlgorithm::SHA_224: return "SHA224"; - case libcdoc::CryptoBackend::HashAlgorithm::SHA_256: return "SHA256"; - case libcdoc::CryptoBackend::HashAlgorithm::SHA_384: return "SHA384"; - case libcdoc::CryptoBackend::HashAlgorithm::SHA_512: return "SHA512"; + case libcdoc::CryptoBackend::HashAlgorithm::SHA_256: return "SHA-256"; + case libcdoc::CryptoBackend::HashAlgorithm::SHA_384: return "SHA-384"; + case libcdoc::CryptoBackend::HashAlgorithm::SHA_512: return "SHA-512"; + default: + break; } return {}; } -static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_224) == "SHA224"); -static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_256) == "SHA256"); -static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_384) == "SHA384"); -static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_512) == "SHA512"); +static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_256) == "SHA-256"); +static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_384) == "SHA-384"); +static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_512) == "SHA-512"); // Out-of-range value (e.g. coming from a SWIG-generated foreign caller) // must produce an empty result rather than reading past the array. static_assert(hashAlgorithmToSidMidName(static_cast(99)).empty()); @@ -274,13 +275,22 @@ static libcdoc::result_t post(httplib::SSLClient& cli, const std::string& path, httplib::Headers& hdrs, const std::string& req, httplib::Response& rsp) { // Capture TLS and HTTP errors - libcdoc::LOG_DBG("POST: {} {}", path, req); + LOG_DBG("POST: {}", path); + LOG_TRACE(" Body: {}", req); + for (auto h : hdrs) { + LOG_TRACE(" Header {}: {}", h.first, h.second); + } httplib::Result res = cli.Post(path, hdrs, req, "application/json"); if (!res) { error = FORMAT("Cannot connect to https://{}:{}{}", cli.host(), cli.port(), path); return libcdoc::NetworkBackend::NETWORK_ERROR; } int status = res->status; + LOG_DBG("Status: {}", status); + LOG_TRACE(" Body: {}", res->body); + for (auto h : res->headers) { + LOG_TRACE(" Header {}: {}", h.first, h.second); + } if ((status < 200) || (status >= 300)) { error = FORMAT("Http status {}", status); return libcdoc::NetworkBackend::NETWORK_ERROR; @@ -374,11 +384,50 @@ libcdoc::NetworkBackend::sendKey (CapsuleInfo& dst, const std::string& url, cons return OK; } +libcdoc::result_t +libcdoc::NetworkBackend::fetchKey (std::vector& dst, const std::string& url, const std::string& transaction_id) +{ + std::string host, path; + int port; + int result = libcdoc::parseURL(url, host, port, path); + if (result != libcdoc::OK) return result; + + std::vector cert; + result = getClientTLSCertificate(cert); + if (result != OK) return result; + std::unique_ptr d = std::make_unique(this, cert); + if (!cert.empty() && (!d->x509 || !d->pkey)) return CRYPTO_ERROR; + + httplib::SSLClient cli(host, port, d->x509.handle(), d->pkey); + if (result = applySSLTimeout(cli, this); result != OK) return result; + result = setPeerCertificates(cli, this, buildURL(host, port)); + if (result != OK) return result; + if (result = setProxy(cli, this); result != OK) return result; + + std::string full = path + "/key-capsules/" + transaction_id; + httplib::Headers hdrs; + picojson::value rsp_json; + result = get(cli, hdrs, full, rsp_json); + if (result != libcdoc::OK) return result; + + picojson::value v = rsp_json.get("ephemeral_key_material"); + if (!v.is()) { + error = FORMAT("No 'ephemeral_key_material' in response"); + return NETWORK_ERROR; + } + error = {}; + std::string ks = v.get(); + dst = fromBase64(ks); + + return libcdoc::OK; +} + #ifdef HAS_KEYSHARES libcdoc::result_t libcdoc::NetworkBackend::sendShare(std::vector& dst, const std::string& url, const std::string& recipient, const std::vector& share) { // Create KeyShare container + LOG_DBG("Creating keyshare for recipient: {}", recipient); picojson::object obj = { {"share", picojson::value(libcdoc::toBase64(share))}, {"recipient", picojson::value(recipient)} @@ -386,7 +435,7 @@ libcdoc::NetworkBackend::sendShare(std::vector& dst, const std::string& picojson::value req_json(obj); std::string req_str = req_json.serialize(); LOG_DBG("POST keyshare to: {}", url); - LOG_DBG("{}", req_str); + LOG_TRACE_KEY("{}", req_str); std::string host, path; int port; @@ -422,50 +471,99 @@ libcdoc::NetworkBackend::sendShare(std::vector& dst, const std::string& return OK; } -#endif - -libcdoc::result_t -libcdoc::NetworkBackend::fetchKey (std::vector& dst, const std::string& url, const std::string& transaction_id) -{ - std::string host, path; - int port; - int result = libcdoc::parseURL(url, host, port, path); - if (result != libcdoc::OK) return result; - std::vector cert; - result = getClientTLSCertificate(cert); - if (result != OK) return result; - std::unique_ptr d = std::make_unique(this, cert); - if (!cert.empty() && (!d->x509 || !d->pkey)) return CRYPTO_ERROR; +namespace libcdoc { - httplib::SSLClient cli(host, port, d->x509.handle(), d->pkey); - if (result = applySSLTimeout(cli, this); result != OK) return result; - result = setPeerCertificates(cli, this, buildURL(host, port)); - if (result != OK) return result; - if (result = setProxy(cli, this); result != OK) return result; +struct AuthResponse { + std::string status; + std::string endResult; + std::string sessionToken; + std::string cert; +}; - std::string full = path + "/key-capsules/" + transaction_id; +static result_t +waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& path, const std::string& auth_proc_uuid, double seconds) +{ httplib::Headers hdrs; - picojson::value rsp_json; - result = get(cli, hdrs, full, rsp_json); - if (result != libcdoc::OK) return result; - picojson::value v = rsp_json.get("ephemeral_key_material"); - if (!v.is()) { - error = FORMAT("No 'ephemeral_key_material' in response"); - return NETWORK_ERROR; + double end = getTime() + seconds; + std::string full = path + auth_proc_uuid; + LOG_DBG("SID/MID authentication query path: {}", full); + while (getTime() < end) { + picojson::value rsp; + result_t result = get(cli, hdrs, full, rsp); + if (result != OK) return result; + if (!rsp.is()) { + error = "Response is not a JSON object"; + LOG_WARN("{}", error); + return NetworkBackend::NETWORK_ERROR; + } + // State + picojson::value v = rsp.get("status"); + if (!v.is()) { + error = "Status is not a string"; + LOG_WARN("{}", error); + return NetworkBackend::NETWORK_ERROR; + } + dst.status = v.get(); + LOG_DBG("Status: {}", dst.status); + if (dst.status == "RUNNING") { + // Pause for 0.5 seconds and repeat + std::chrono::milliseconds duration(500); + std::this_thread::sleep_for(duration); + continue; + } else if (dst.status != "COMPLETE") { + error = FORMAT("Invalid SmartID state: {}", dst.status); + LOG_WARN("{}", error); + return NetworkBackend::NETWORK_ERROR; + } + // State is complete, check for end result + v = rsp.get("endResult"); + if (!v.is()) { + error = "endResult is not a JSON object"; + LOG_WARN("{}", error); + return NetworkBackend::NETWORK_ERROR; + } + dst.endResult = v.get(); + LOG_DBG("EndResult: {}", dst.endResult); + if (dst.endResult != "OK") { + LOG_WARN("EndResult is not OK: {}", dst.endResult); + return NetworkBackend::NETWORK_ERROR; + } + // Signature + v = rsp.get("sessionToken"); + if (!v.is()) { + error = "sessionToken is not a string"; + LOG_WARN("{}", error); + return NetworkBackend::NETWORK_ERROR; + } + dst.sessionToken = v.get(); + LOG_DBG("Session token: {}", dst.sessionToken); + + // Certificate + v = rsp.get("signingCertificate"); + if (!v.is()) { + error = "signingCertificate is not a string"; + LOG_WARN("{}", error); + return NetworkBackend::NETWORK_ERROR; + } + dst.cert = v.get(); + LOG_DBG("Certificate: {}", dst.cert); + error = {}; + return OK; } - error = {}; - std::string ks = v.get(); - dst = fromBase64(ks); + // Timeout + error = "Timeout waiting SID/MID result"; + LOG_WARN("{}", error); + return UNSPECIFIED_ERROR; +} - return libcdoc::OK; } -#ifdef HAS_KEYSHARES libcdoc::result_t -libcdoc::NetworkBackend::authenticateForShares(std::vector& dst) +libcdoc::NetworkBackend::authenticateForShares(std::string& token, std::string& cert) { +#if 1 static const std::string url = "https://cdoc2-auth.test.riaint.ee"; // Start authentication std::string host, path; @@ -493,8 +591,22 @@ libcdoc::NetworkBackend::authenticateForShares(std::vector& dst) httplib::Response rsp; result = post(cli, full, hdrs, req_str, rsp); if (result != libcdoc::OK) return result; - + LOG_DBG("Status: {}", rsp.status); LOG_DBG("Response: {}", rsp.body); + + std::string location = rsp.get_header_value("Location"); + LOG_DBG("Location: {}", location); + if (location.empty()) { + error = FORMAT("No Location header in response"); + return NETWORK_ERROR; + } + constexpr std::string_view prefix = "/auth/status/"; + if (location.compare(0, prefix.size(), prefix) != 0) { + error = FORMAT("Unexpected Location header value"); + return NETWORK_ERROR; + } + location.erase(0, prefix.size()); + picojson::value rsp_json; std::string parse_err = picojson::parse(rsp_json, rsp.body); if (!parse_err.empty()) { @@ -502,12 +614,79 @@ libcdoc::NetworkBackend::authenticateForShares(std::vector& dst) LOG_ERROR("{}", error); return NETWORK_ERROR; } + if (!rsp_json.is()) { + error = "Invalid Authentication response"; + LOG_WARN("Invalid Authentication response"); + return NetworkBackend::NETWORK_ERROR; + } + picojson::value w = rsp_json.get("vc"); + if (!w.is()) { + error = "Invalid Authentication response"; + LOG_WARN("Invalid Authentication response"); + return NetworkBackend::NETWORK_ERROR; + } + std::string ver_code = w.get(); + LOG_DBG("Verification code: {}", ver_code); + SIDMIDFeedback fb = { + .code = (int) std::strtold(ver_code.c_str(), nullptr), + }; + result = showFeedback(fb); + if (result != OK) return result; - return NOT_IMPLEMENTED; + AuthResponse auth_rsp; + result = waitForAuthResult(auth_rsp, cli, path + "/auth/status/", location, 60); + if (result != OK) return result; + + cert = auth_rsp.cert; + + auto parts = split(auth_rsp.sessionToken, '~'); +#else + token = "eyJraWQiOiJEN3Y5aF8wb1RuQUxxNmx6aVJPYUNGRzBVbFRCYmdPQTVMSGpfYkoteVo4IiwidHlwIjoidm5kLmNkb2MyLnNlc3Npb24tdG9rZW4udjIrc2Qtand0IiwiYWxnIjoiRVMyNTYifQ.eyJycENoYWxsZW5nZSI6ImhSV1hodWhueDJ6RDgrQ0NvSk1sZERRMkNyOW8yRmFhNStmU1ZVSkVSTVQ5YW1mVlFPUEp0UGRsaW9yU01PbWNaY2tNdWt3OHZwR3FhQVdGY0JvcDhRPT0iLCJzdWIiOiJldHNpL1BOT0VFLTM3MTA0MDgyNzEwIiwic2lnbmF0dXJlIjp7InZhbHVlIjoiQXlUd3JhOW10VGt6SUxodi9iQ0NhaUI5QWtNcktTRnVKT0FkKzZTZVhjeFJTTTVvSVFxWDlyZlVHNTB6UFpqZlYrQ3RQUHlnc3paZVpBODVFNm1NMzU5SjFIOGJVZDZiVHBCdGNTcWtLTzVtWFNzbXJmT2NYdDdjZWJTdW1MZ0liQ1VJckxISDA2QjVXeldiWjVYcmMzVkhscVBqVjVGbkhOejVtMUdyb3lKNkoyWHpNTkNmMVZNU1JtcC9meU16dWd5VXcxS1pqUlhTcnh3Wk5TRGhyMzVRL1l2NmVFMVdLdnZudnBjTTBMQWJobGxOZDhBSmdXMUhFZThrNGZYWTlKemxYN0YyWk5mdXM5a1MzQmNLMzhZZ0FKRlk4d0JYQzc5ODZQL0xOcjYxK1pOSkxUVUlGWU0vZHRudTcxdzhkWEo4TXVGYm5tbEdGVERuVlBjQXlSZmw5ZHdnS1Yvdmg3SFBWTlJ6d21TVjBCU1cwL1p4RVlFcmdMQ0lXaVJaSEFzYzBZVHppN0UycjVTV1A1b05KVVNtbnRHM0RpRmpWNUMwUERRa2hFUHpQYlhaYWg4MHhMQ21yT1dUelp2dVdXYnFRcHUwalR6SDRLOTc1bStlMlpRMEZMZEpsU3RYa0FlSTAwcE5va25LQW1MTkRkREh4Q2xVYVlJbzN5aDlFVkc0RlhGaTlqTnBiOWdKMGNEWEJmR3hpbENXUmVUZkF2TkE3c0lST0lhNjA0YmhjWmcyaTRybWxxYjdJNS9jYVQ1Y3lDb1pHSlZxa0lyVmtGa3ZDL1hjYzBzTy9Cdmpaa0doTWNWNnZEMU01eFVjcXNKK1d3ZHlXMjhyOURQTUZjdXRoRTl6QVVLbHY4OE9aRlFkc2xBeFJ6UEJSNHhDTnkvUm5WWVRSL1FmS0wrV1l3MTcyMU0zZ1d2WURMbXoyZ3VrWlBqNHBncklDTXU2cFV3SW9kUDNROTRROW5IeGtLNE5aWFY1eStMVjJNckNXVWZTWWh6dHNuVlJXWlhCMmplNFd5Yyt0b1IvcjNoQlpZL3FRa3BxTEhQcnExcDBjY2hnMEhtM2RzQm5RYzJPL3FKYUUyTGdYQVBYVVZ3cmJiVkFzb2NZc2tHeEF6UjdpWmlRQ2FVSkE0Um5zTGNkaitLT20zc0lHMVJ5azE3czNoeG5CYkZocGRHQnp5WWxSR25ncm5heW1CU3J3bDRBeVhoQTE1M09YbEp5b01EQkU0SmkxYlJwSXpwT1ZpSm45T2M0MHhQK1hpbU5MM0tnMm9LcS9EZEk3Z1Evdmd0WkFOMFFsU0pNcHo1TlJlRWRuTGgyUUt3V2duSGtYdGJ3ZUFXNFJIdml2M1lrNmtRYiIsInNlcnZlclJhbmRvbSI6IkVsMFVSWW5vdUNWWEcrVFpDS0lTNko4SyIsInVzZXJDaGFsbGVuZ2UiOiJNcEFrOTN3YTJFUEhGMW9wSmFQRVRHbTNjZDNNX29VYlBnNHRuVFZwZVRJIiwic2lnbmF0dXJlQWxnb3JpdGhtIjoicnNhc3NhLXBzcyIsImZsb3dUeXBlIjoiTm90aWZpY2F0aW9uIiwic2lnbmF0dXJlQWxnb3JpdGhtUGFyYW1ldGVycyI6eyJoYXNoQWxnb3JpdGhtIjoiU0hBLTI1NiIsIm1hc2tHZW5BbGdvcml0aG0iOnsiYWxnb3JpdGhtIjoiaWQtbWdmMSIsInBhcmFtZXRlcnMiOnsiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYifX0sInNhbHRMZW5ndGgiOjMyLCJ0cmFpbGVyRmllbGQiOiIweGJjIn19LCJpc3MiOiJodHRwczovL2Nkb2MyLWF1dGgudGVzdC5yaWFpbnQuZWUiLCJzY2hlbWVOYW1lIjoic21hcnQtaWQtZGVtbyIsInNpZ25hdHVyZVByb3RvY29sIjoiUlNBU1NBLVBTUytBQ1NQX1YyIiwiX3NkIjpbIlZhb3ZCN0RYSm44Q21aOEFSd2dvODJNR2pPSkc2VTA4Zm9VVXl3UlJzaVkiXSwiaW50ZXJhY3Rpb25zRGlnZXN0IjoiWG9pN1F1eDB0NmxFaXN6MU9Gc1dIUGdhM1l6Q2QzQ2ROQ0t1VE91UHZBWT0iLCJfc2RfYWxnIjoic2hhLTI1NiIsImV4cCI6MTc4NDcwMjI2OCwiaWF0IjoxNzg0NjE1ODY4LCJpbnRlcmFjdGlvblR5cGVVc2VkIjoiY29uZmlybWF0aW9uTWVzc2FnZUFuZFZlcmlmaWNhdGlvbkNvZGVDaG9pY2UiLCJycE5hbWUiOiJERU1PIn0.ekJ-J--6wuLhvsmxwEOpOLqjYCV1QMiYaUjwbAsq6Nt6qNdvMy81ArUCyN5l3CENfUKcgcQdw3HtQxuPEk0_PQ~WyJMdlRKN3VaNF9ValBJU3JFc3h3Wmp3IiwiYXVkIixbeyIuLi4iOiJYTnR2amNRZEhTbkhNNWdPWDhwcXpHWHUzMzY4VE0xNExjS2h5Z0dzZWM0In0seyIuLi4iOiJzX1d1aDFqenVFbmo3ZVZVdldtcXIzaURVSmNMcGlkQ1BpZDVYOXFuWi0wIn0seyIuLi4iOiIyVmRsWGpaMU9DcndwbGpGYURMMFJ2N3VZNEtvZ1hoQWdobjBTM1VRZ2dnIn1dXQ~WyI3RmxpMXhPd3hhQXdBWVZkR1ZJVkVBIiwiaHR0cHM6Ly9jZG9jMi1ycC50ZXN0LnJpYWludC5lZS9zZXNzaW9uX25vbmNlL3EyaUxJS2VveXpEQ1RyeGRJbVk0aUEiXQ~WyJfVUpmQ1hDbXJyMml1N3N4NXo3QWZ3IiwiaHR0cHM6Ly9jZG9jMi1zaGFyZXMudGVzdC5yaWFpbnQuZWUvc2Vzc2lvbl9ub25jZS9GTGw2b3dramlJZVMwVzBqUHlLNll3Il0~WyJQMURtczZFbmk3LU4yS0Y0OXl2SUZBIiwiaHR0cHM6Ly9jZG9jMi1zaGFyZXNleHRlcm5hbC50ZXN0LnJpYWludC5lZS9zZXNzaW9uX25vbmNlL3hHdzVxR1g1alFLOUlFVi1CbVZKZWciXQ~"; + cert = "MIIGuzCCBkCgAwIBAgIQDV-hZUN9xS5yEzxpvHOPejAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjUxMDI5MDcwOTEwWhcNMjgxMDI4MDYwOTEwWjBpMQswCQYDVQQGEwJFRTEZMBcGA1UEAwwQS0FQTElOU0tJLExBVVJJUzESMBAGA1UEBAwJS0FQTElOU0tJMQ8wDQYDVQQqDAZMQVVSSVMxGjAYBgNVBAUTEVBOT0VFLTM3MTA0MDgyNzEwMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAkOP8-thy1C0eG_CuqA5stRrjUaD5T07Q7-JZcZcWPnRTBROZizhehHozd-Kqxs_PH4I2lFCmGx8QgoeIba4VO7NeZ-DacaQtfGHlX85pYpzJH3l31e-xs_oQsW9CIp07MpkfcbuB16T2X88S2_YCFC2pbgxJg3CpF4ejL-zjT18SeRfXmHwPEP9kuLYFSZ6yALDIRLf-_r0SucwARNDSG0MVu-riE0xDZjok0SqCfscaa027sco43T4l2OSd1G8yGHwoIhZuTepWpkfgwUR3RUhAFZdPUEBvtLbeLaQ_J5BG6gHWcZtHD_wEXjcf7HPygxVk8XPlmndUPNmt-gtWLh5LuGqjgL1o8P2M9SXkNHPy0mD9s3noMS2agluVRA3dJoitn3hG_nBS3ADOtwUwGtb11KMZJMJaEePATv5iNLEXEiFBDcST4F2QEjs96QBLGwJEpToOxHDbfVe1p9OpRWjsSrf9IJAXxjsm37uP1QgaqRMAUaF8LZaSyWQ9OIKVfMQZ8DR-kl4BYyAMyxTNewL7Ht4f4qYV2WUvrD3U0puhVWXH5dAzgDXVmzYHgsRj_gCTGNoQBCop3X2ZQ-lbQZvMto6xtL-tHl9oG8b_hcTvv-UCIIhj2Sokb7RljG0UAyn4rsEUya_pBreaFKBsufTIVt-oNbjAReqzdNXT_gtmHm8P920Yq7i0qpnEAouigpvNcB_04o11ZGpLf4sOoJb-IvOoWnf0f52PFOTDxroP9N9LM8umNFT7WPbL9JLev9huURRBobp2D6Rf47Ktgiq4KzuOMLUMRRDUTAYrKftwGwye6nlV2fONX7PLRqlqzhLDe5ljEwchJ7asiC0FPN5IG-j4wl1RBMjlNcaGfgnhalMsDIiBRlTg4qIkQwwz4y4GRJXjfgz2J4CymQDEdha-pMePRCKttVDPAsTS5y4ACQphyWjVxfEvGC7z4XkDbr7kPhOuAkkfhdhsdOM5oFGz2KeJQmeZtEi5gMZ6BHjbYoYJPJKXa5B_yYu1d6dbAgMBAAGjggH1MIIB8TAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS0zNzEwNDA4MjcxMC1OWEhTLVEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBAjAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5NzEwNDA4MTIwMDAwWjAWBgNVHSUEDzANBgsrBgEEAYPmYgUHADA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIwMjRlLmNybDAdBgNVHQ4EFgQUJRKanMsHUMXf5zhIfK51Qn_n6lAwDgYDVR0PAQH_BAQDAgeAMAoGCCqGSM49BAMDA2kAMGYCMQC8hVUFpywnFWTqUB6Rw-CADvHrQBft9H9pyM5IIUanrS2O-KFsc0TsMojORxcg_xkCMQDE71vvGtizs-sEk23jam_2pi76C1e5rDjfPDn6qFBuvvEtpcQlmX7e9JDCfGcKJtc="; + auto parts = split(token, '~'); +#endif + if (parts.size() < 3) { + error = "Invalid JWT-SD token"; + LOG_WARN("Invalid JWT-SD token"); + return NetworkBackend::NETWORK_ERROR; + } + std::string jwt = parts[0]; + std::string aud = parts[1]; + for (size_t i = 2; i < parts.size(); i++) { + auto v = parts[i]; + LOG_DBG("Session token part {} ({}) : {}", i, v.size(), v); + if (i > 0) { + std::vector decoded_part = fromBase64URL(v); + LOG_DBG("Decoded part {} ({}): {}", i, decoded_part.size(), std::string(decoded_part.begin(), decoded_part.end())); + } + } + + token = auth_rsp.sessionToken; + + auto decoded = decodeTicket(parts[0]); + //auto st_json = decoded.get_header_json(); + LOG_DBG("Session token: {}", decoded); + picojson::value dec_json; + auto p_err = picojson::parse(dec_json, decoded); + if (!p_err.empty()) { + error = FORMAT("JSON parse error: {}", p_err); + LOG_ERROR("{}", error); + return NETWORK_ERROR; + } + if (!dec_json.is()) { + error = "Invalid Authentication response"; + LOG_WARN("Invalid Authentication response"); + return NetworkBackend::NETWORK_ERROR; + } + for (auto a : dec_json.get()) { + LOG_DBG("Payload JSON {}: {}", a.first, a.second.to_str()); + } + + return OK; } libcdoc::result_t -libcdoc::NetworkBackend::fetchNonce(std::vector& dst, const std::string& url, const std::string& share_id) +libcdoc::NetworkBackend::fetchNonce(std::vector& dst, const std::string& url, const std::string& share_id, const std::string& auth_token, const std::string& auth_cert) { LOG_DBG("Get nonce from: {}", url); @@ -523,8 +702,13 @@ libcdoc::NetworkBackend::fetchNonce(std::vector& dst, const std::string if (result != OK) return result; if (result = setProxy(cli, this); result != OK) return result; + SessionToken stoken(auth_token); + std::string full = path + "/key-shares/" + share_id + "/nonce"; httplib::Headers hdrs; + hdrs.insert({"x-cdoc2-session-token", stoken.discloseForUrl(url)}); + hdrs.insert({"x-cdoc2-session-x5c", auth_cert}); + LOG_DBG("POST nonce request to: {}", full); httplib::Response rsp; result = post(cli, full, hdrs, "", rsp); if (result != libcdoc::OK) return result; @@ -596,60 +780,7 @@ libcdoc::NetworkBackend::fetchShare(ShareInfo& share, const std::string& url, co share = {std::move(shareval), std::move(recipient)}; return OK; } -#endif -ECDSA_SIG * -ecdsa_do_sign(const unsigned char *dgst, int dgst_len, const BIGNUM * /*inv*/, const BIGNUM * /*rp*/, EC_KEY *eckey) -{ - auto *backend = (libcdoc::NetworkBackend *) EC_KEY_get_ex_data(eckey, 0); - std::vector dst; - std::vector digest(dgst, dgst + dgst_len); - int result = backend->signTLS(dst, libcdoc::CryptoBackend::SHA_512, digest); - if (result != libcdoc::OK) { - return nullptr; - } - int size_2 = (int) dst.size() / 2; - ECDSA_SIG *sig = ECDSA_SIG_new(); - ECDSA_SIG_set0(sig, - BN_bin2bn(dst.data(), size_2, nullptr), - BN_bin2bn(dst.data() + size_2, size_2, nullptr)); - return sig; -} - -int -rsa_sign(int type, const unsigned char *m, unsigned int m_len, unsigned char *sigret, unsigned int *siglen, const RSA *rsa) -{ - auto *backend = (libcdoc::NetworkBackend *) RSA_get_ex_data(rsa, 0); - auto algo = libcdoc::CryptoBackend::SHA_512; - switch (type) { - case NID_sha224: - algo = libcdoc::CryptoBackend::SHA_224; - break; - case NID_sha256: - algo = libcdoc::CryptoBackend::SHA_256; - break; - case NID_sha384: - algo = libcdoc::CryptoBackend::SHA_384; - break; - case NID_sha512: - break; - default: - return 0; - } - std::vector dst; - std::vector digest(m, m + m_len); - int result = backend->signTLS(dst, algo, digest); - if (result != libcdoc::OK) { - return 0; - } - if (sigret && (*siglen >= dst.size())) { - memcpy(sigret, dst.data(), dst.size()); - } - *siglen = (unsigned int) dst.size(); - return 1; -} - -#ifdef HAS_KEYSHARES libcdoc::result_t libcdoc::NetworkBackend::showFeedback(SIDMIDFeedback& feedback) { @@ -787,7 +918,7 @@ waitForResult(SIDResponse& dst, httplib::SSLClient& cli, const std::string& path libcdoc::result_t libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector& cert, - const std::string& url, const std::string& rp_uuid, const std::string& rp_name, + const std::string& url, const std::string& auth_token, const std::string& auth_cert, const std::string& rcpt_id, const std::vector& digest, CryptoBackend::HashAlgorithm algo) { std::string certificateLevel = "QUALIFIED"; @@ -796,11 +927,29 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector return libcdoc::CRYPTO_ERROR; std::string nonce = libcdoc::toBase64(nonce_bytes); + picojson::object sap = { + {"hashAlgorithm", picojson::value("SHA-256")} + }; + picojson::object spp = { + {"rpChallenge", picojson::value(toBase64(digest))}, + {"signatureAlgorithm", picojson::value("rsassa-pss")}, + {"signatureAlgorithmParameters", picojson::value(sap)} + }; + picojson::object inter = { + {"type", picojson::value("confirmationMessageAndVerificationCodeChoice")}, + {"displayText200", picojson::value("Do you want to decrypt the document")} + }; + picojson::array inter_arr = { + picojson::value(inter) + }; + std::string inter_str = picojson::value(inter_arr).serialize(); picojson::object obj = { - {"relyingPartyUUID", picojson::value(rp_uuid)}, - {"relyingPartyName", picojson::value(rp_name)}, + {"semanticsIdentifier", picojson::value(rcpt_id)}, {"certificateLevel", picojson::value(certificateLevel)}, - {"nonce", picojson::value(nonce)} + {"signatureProtocol", picojson::value("ACSP_V2")}, + {"signatureProtocolParameters", picojson::value(spp)}, + {"interactions", picojson::value(toBase64((const uint8_t *) inter_str.data(), inter_str.size()))}, + {"vcType", picojson::value("numeric4")} }; picojson::value query(obj); LOG_DBG("JSON:{}", query.serialize()); @@ -814,6 +963,8 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector LOG_DBG("PORT:{}", port); LOG_DBG("PATH:{}", path); + SessionToken stoken(auth_token); + LOG_DBG("Starting client: {} {}", host, port); httplib::SSLClient cli(host, port); if (result = applySSLTimeout(cli, this); result != OK) return result; @@ -824,14 +975,17 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector // // Let user choose certificate (if multiple) // - std::string full = path + "/certificatechoice/" + rcpt_id; + std::string full = path + "/sid/authenticate"; LOG_DBG("SmartID path: {}", full); httplib::Headers hdrs; + hdrs.insert({"x-cdoc2-session-token", stoken.discloseForUrl(url)}); + hdrs.insert({"x-cdoc2-session-x5c", auth_cert}); httplib::Response rsp; result = post(cli, full, hdrs, query.serialize(), rsp); if (result != libcdoc::OK) return result; - + return NOT_IMPLEMENTED; +#if 0 LOG_DBG("Response: {}", rsp.body); picojson::value v; std::string parse_err = picojson::parse(v, rsp.body); @@ -877,9 +1031,9 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector } // Generate code - uint8_t b[32]; - SHA256(digest.data(), digest.size(), b); SIDMIDFeedback fb; + std::array b; + SHA256(digest.data(), digest.size(), b.data()); fb.code = ((b[30] << 8) | b[31]) % 10000; result = showFeedback(fb); if (result != OK) return result; @@ -940,6 +1094,7 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector cert = fromBase64(sidrsp.cert); return OK; +#endif } libcdoc::result_t @@ -1069,3 +1224,54 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector return OK; } #endif + +ECDSA_SIG * +ecdsa_do_sign(const unsigned char *dgst, int dgst_len, const BIGNUM * /*inv*/, const BIGNUM * /*rp*/, EC_KEY *eckey) +{ + auto *backend = (libcdoc::NetworkBackend *) EC_KEY_get_ex_data(eckey, 0); + std::vector dst; + std::vector digest(dgst, dgst + dgst_len); + int result = backend->signTLS(dst, libcdoc::CryptoBackend::SHA_512, digest); + if (result != libcdoc::OK) { + return nullptr; + } + int size_2 = (int) dst.size() / 2; + ECDSA_SIG *sig = ECDSA_SIG_new(); + ECDSA_SIG_set0(sig, + BN_bin2bn(dst.data(), size_2, nullptr), + BN_bin2bn(dst.data() + size_2, size_2, nullptr)); + return sig; +} + +int +rsa_sign(int type, const unsigned char *m, unsigned int m_len, unsigned char *sigret, unsigned int *siglen, const RSA *rsa) +{ + auto *backend = (libcdoc::NetworkBackend *) RSA_get_ex_data(rsa, 0); + auto algo = libcdoc::CryptoBackend::SHA_512; + switch (type) { + case NID_sha224: + algo = libcdoc::CryptoBackend::SHA_224; + break; + case NID_sha256: + algo = libcdoc::CryptoBackend::SHA_256; + break; + case NID_sha384: + algo = libcdoc::CryptoBackend::SHA_384; + break; + case NID_sha512: + break; + default: + return 0; + } + std::vector dst; + std::vector digest(m, m + m_len); + int result = backend->signTLS(dst, algo, digest); + if (result != libcdoc::OK) { + return 0; + } + if (sigret && (*siglen >= dst.size())) { + memcpy(sigret, dst.data(), dst.size()); + } + *siglen = (unsigned int) dst.size(); + return 1; +} diff --git a/cdoc/NetworkBackend.h b/cdoc/NetworkBackend.h index 390c06f9..d832c5c4 100644 --- a/cdoc/NetworkBackend.h +++ b/cdoc/NetworkBackend.h @@ -180,8 +180,10 @@ struct CDOC_EXPORT NetworkBackend { * @return error code or OK */ virtual result_t fetchKey (std::vector& dst, const std::string& url, const std::string& transaction_id); + #ifdef HAS_KEYSHARES - virtual result_t authenticateForShares(std::vector& dst); + virtual result_t authenticateForShares(std::string& token, std::string& cert); + /** * @brief fetch authentication nonce from share server * @param dst a destination container for nonce @@ -189,7 +191,7 @@ struct CDOC_EXPORT NetworkBackend { * @param share_id share id (transaction id) * @return error code or OK */ - virtual result_t fetchNonce(std::vector& dst, const std::string& url, const std::string& share_id); + virtual result_t fetchNonce(std::vector& dst, const std::string& url, const std::string& share_id, const std::string& auth_token, const std::string& auth_cert); /** * @brief fetch key share from share server * @param share a container for result @@ -274,7 +276,7 @@ struct CDOC_EXPORT NetworkBackend { * @return error code or OK */ result_t signSID(std::vector& dst, std::vector& cert, - const std::string& url, const std::string& rp_uuid, const std::string& rp_name, + const std::string& url, const std::string& auth_token, const std::string& auth_cert, const std::string& rcpt_id, const std::vector& digest, CryptoBackend::HashAlgorithm algo); /** diff --git a/cdoc/Utils.cpp b/cdoc/Utils.cpp index b422d596..62f9b6cd 100644 --- a/cdoc/Utils.cpp +++ b/cdoc/Utils.cpp @@ -45,6 +45,14 @@ fromBase64(std::string_view data) return std::vector(str.cbegin(), str.cend()); } +std::vector +fromBase64URL(std::string_view data) +{ + auto padded = jwt::base::pad(std::string(data)); + auto str = jwt::base::decode(padded); + return std::vector(str.cbegin(), str.cend()); +} + double getTime() { diff --git a/cdoc/Utils.h b/cdoc/Utils.h index 68dda1c6..c45026b3 100644 --- a/cdoc/Utils.h +++ b/cdoc/Utils.h @@ -55,6 +55,7 @@ static std::string toBase64(const std::vector &data) { } std::vector fromBase64(std::string_view data); +std::vector fromBase64URL(std::string_view data); template static std::string toHex(const F &data) @@ -86,12 +87,17 @@ fromHex(std::string_view hex) { } static std::vector -split(const std::string &s, char delim = ':') { +split(std::string_view s, char delim = ':') { std::vector result; - std::stringstream ss(s); - std::string item; - while (getline (ss, item, delim)) { - result.push_back (item); + auto start = s.cbegin(); + for (auto end = s.cbegin(); end != s.cend(); ++end) { + if (*end == delim) { + result.push_back(std::string(start, end)); + start = end + 1; + } + } + if (start != s.cend()) { + result.push_back(std::string(start, s.cend())); } return result; } @@ -309,15 +315,11 @@ static inline void LogFormat(LogLevel level, std::string_view file, int line, st #define LOG_INFO(...) LogFormat(libcdoc::LEVEL_INFO, __FILE__, __LINE__, __VA_ARGS__) #define LOG_DBG(...) LogFormat(libcdoc::LEVEL_DEBUG, __FILE__, __LINE__, __VA_ARGS__) -#ifdef NDEBUG -#define LOG_TRACE(...) -#else -#define LOG_TRACE(...) LogFormat(libcdoc::LEVEL_TRACE, __FILE__, __LINE__, __VA_ARGS__) -#endif - #ifdef LIBCDOC_CRYPTO_TRACE +#define LOG_TRACE(...) LogFormat(libcdoc::LEVEL_TRACE, __FILE__, __LINE__, __VA_ARGS__) #define LOG_TRACE_KEY(MSG, KEY) LogFormat(libcdoc::LEVEL_TRACE, __FILE__, __LINE__, MSG, toHex(KEY)) #else +#define LOG_TRACE(...) #define LOG_TRACE_KEY(MSG, KEY) #endif From dc23ffacc00964ca141982daee1c919d9b692fec Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 24 Jul 2026 15:34:34 +0300 Subject: [PATCH 48/65] Added SecureTarget class --- cdoc/CDoc1Reader.cpp | 7 +++-- cdoc/CDoc2Reader.cpp | 28 ++++++++----------- cdoc/utils/memory.h | 64 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 21 deletions(-) diff --git a/cdoc/CDoc1Reader.cpp b/cdoc/CDoc1Reader.cpp index cb724f0d..6f676f0d 100644 --- a/cdoc/CDoc1Reader.cpp +++ b/cdoc/CDoc1Reader.cpp @@ -164,8 +164,8 @@ CDoc1Reader::getFMK(std::vector& fmk, unsigned int lock_idx) // The downstream AES decrypt at the body level is what tells // success from failure. } else { - std::vector key; - int result = crypto->deriveConcatKDF(key, + SecureTarget key; + int result = crypto->deriveConcatKDF(key.getTarget(), lock.getBytes(Lock::Params::KEY_MATERIAL), lock.getString(Lock::Params::CONCAT_DIGEST), lock.getBytes(Lock::Params::ALGORITHM_ID), @@ -173,13 +173,12 @@ CDoc1Reader::getFMK(std::vector& fmk, unsigned int lock_idx) lock.getBytes(Lock::Params::PARTY_VINFO), lock_idx); if (result < 0) { - libcdoc::cleanse(key); setLastError(FAIL_MSG); LOG_ERROR("{}", last_error); return libcdoc::CRYPTO_ERROR; } fmk = libcdoc::Crypto::AESWrap(key, lock.encrypted_fmk, false); - libcdoc::cleanse(key); + key.cleanse(); // AESWrap returns {} on failure. Pad the candidate to expected // length so the failure shape matches the RSA path; the bytes // are arbitrary because the body decrypt is going to reject diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index caf6a75b..e5cf0295 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -147,17 +147,15 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) // exceptions). All early returns below previously had to remember to // call libcdoc::cleanse(kek) - which several of them did not. With the // guard the wipe is unconditional. - std::vector kek; - libcdoc::Cleanser kek_guard(kek); + SecureTarget kek; if (lock.type == Lock::Type::PASSWORD) { // Password LOG_DBG("password"); std::string info_str = libcdoc::CDoc2::getSaltForExpand(lock.label); LOG_DBG("info: {}", toHex(info_str)); - std::vector kek_pm; - libcdoc::Cleanser kek_pm_guard(kek_pm); - if (auto rv = crypto->extractHKDF(kek_pm, lock.getBytes(Lock::SALT), lock.getBytes(Lock::PW_SALT), lock.getInt(Lock::KDF_ITER), lock_idx); rv != libcdoc::OK) { + SecureTarget kek_pm; + if (auto rv = crypto->extractHKDF(kek_pm.getTarget(), lock.getBytes(Lock::SALT), lock.getBytes(Lock::PW_SALT), lock.getInt(Lock::KDF_ITER), lock_idx); rv != libcdoc::OK) { setLastError(crypto->getLastErrorStr(rv)); LOG_ERROR("{}", last_error); return rv; @@ -170,9 +168,8 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) LOG_DBG("symmetric"); std::string info_str = libcdoc::CDoc2::getSaltForExpand(lock.label); LOG_DBG("info: {}", toHex(info_str)); - std::vector kek_pm; - libcdoc::Cleanser kek_pm_guard(kek_pm); - if (auto rv = crypto->extractHKDF(kek_pm, lock.getBytes(Lock::SALT), {}, 0, lock_idx); rv != libcdoc::OK) { + SecureTarget kek_pm; + if (auto rv = crypto->extractHKDF(kek_pm.getTarget(), lock.getBytes(Lock::SALT), {}, 0, lock_idx); rv != libcdoc::OK) { setLastError(crypto->getLastErrorStr(rv)); LOG_ERROR("{}", last_error); return rv; @@ -182,11 +179,10 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) kek = libcdoc::Crypto::expand(kek_pm, info_str, 32); } else if ((lock.type == Lock::Type::PUBLIC_KEY) || (lock.type == Lock::Type::SERVER)) { // Public/private key - std::vector key_material; + SecureTarget key_material; // SERVER path fetches key_material over the network; PUBLIC_KEY // takes it from the lock. Either way it gets fed into ECDH or RSA // and is sensitive enough to wipe in-scope. - libcdoc::Cleanser key_material_guard(key_material); if(lock.type == Lock::Type::SERVER) { if(!conf) { setLastError("Configuration is missing"); @@ -206,7 +202,7 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) return libcdoc::CONFIGURATION_ERROR; } std::string transaction_id = lock.getString(Lock::Params::TRANSACTION_ID); - int result = network->fetchKey(key_material, fetch_url, transaction_id); + int result = network->fetchKey(key_material.getTarget(), fetch_url, transaction_id); if (result < 0) { setLastError(network->getLastErrorStr(result)); return result; @@ -219,16 +215,15 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) LOG_TRACE_KEY("Key material: {}", key_material); if (lock.isRSA()) { - int result = crypto->decryptRSA(kek, key_material, true, lock_idx); + int result = crypto->decryptRSA(kek.getTarget(), key_material, true, lock_idx); if (result < 0) { setLastError(crypto->getLastErrorStr(result)); LOG_ERROR("{}", last_error); return result; } } else { - std::vector kek_pm; - libcdoc::Cleanser kek_pm_guard(kek_pm); - int result = crypto->deriveHMACExtract(kek_pm, key_material, toUint8Vector(libcdoc::CDoc2::KEKPREMASTER), lock_idx); + SecureTarget kek_pm; + int result = crypto->deriveHMACExtract(kek_pm.getTarget(), key_material, toUint8Vector(libcdoc::CDoc2::KEKPREMASTER), lock_idx); if (result < 0) { setLastError(crypto->getLastErrorStr(result)); LOG_ERROR("{}", last_error); @@ -365,8 +360,7 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) fmk.clear(); return err; } - std::vector hhk = libcdoc::Crypto::expand(fmk, libcdoc::CDoc2::HMAC); - libcdoc::Cleanser hhk_guard(hhk); + SecureTarget hhk = libcdoc::Crypto::expand(fmk, libcdoc::CDoc2::HMAC); LOG_TRACE_KEY("xor: {}", lock.encrypted_fmk); LOG_TRACE_KEY("fmk: {}", fmk); diff --git a/cdoc/utils/memory.h b/cdoc/utils/memory.h index c441a5a2..74a97f73 100644 --- a/cdoc/utils/memory.h +++ b/cdoc/utils/memory.h @@ -191,6 +191,70 @@ class SecureBytes { } }; +// +// A self-cleaning writable container for temporary secrets. +// +// We allow getting a reference to the actual content vector to be used in library calls, but +// all existing contents will be cleansed first in that case. +// + +class SecureTarget { + std::vector data_; +public: + using iterator = std::vector::iterator; + using const_iterator = std::vector::const_iterator; + + SecureTarget() noexcept = default; + + ~SecureTarget() { + cleanse(); + } + + SecureTarget(const SecureTarget& other) = delete; + SecureTarget(SecureTarget&& other) = delete; + SecureTarget& operator=(const SecureTarget& other) = delete; + SecureTarget& operator=(SecureTarget&& other) = delete; + // Need a plain constructor for declaration-initialisation case + SecureTarget(std::vector v) noexcept : data_(std::move(v)) {} + // Assignment should first cleanse and then copy/move + SecureTarget& operator=(const std::vector& v) { + cleanse(); + data_ = v; + return *this; + } + SecureTarget& operator=(std::vector&& v) { + cleanse(); + data_ = std::move(v); + return *this; + } + + [[nodiscard]] bool empty() const noexcept { return data_.empty(); } + [[nodiscard]] size_t size() const noexcept { return data_.size(); } + [[nodiscard]] const uint8_t* data() const noexcept { return data_.data(); } + [[nodiscard]] const_iterator cbegin() const noexcept { return data_.cbegin(); } + [[nodiscard]] const_iterator cend() const noexcept { return data_.cend(); } + [[nodiscard]] const_iterator begin() const noexcept { return data_.begin(); } + [[nodiscard]] const_iterator end() const noexcept { return data_.end(); } + + [[nodiscard]] operator const std::vector&() const noexcept { return data_; } + + // Get writable vector + // Any secret, if present, is cleansed first to avoid leaking previous contents + std::vector& getTarget() { + cleanse(); + return data_; + } + std::vector& getTarget(size_t size) { + cleanse(size); + return data_; + } + + void cleanse(size_t size = 0) noexcept { + ::libcdoc::cleanse(data_); + data_.resize(size); + } +}; + /** * @brief Scope guard that wipes a contiguous secret on destruction. * From 139d1a7400573ac2cac41ce6b38193fc08e0b48f Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Mon, 27 Jul 2026 14:03:45 +0300 Subject: [PATCH 49/65] Fixed N1 (out-of-bounds read) from 2026-07 report --- .github/workflows/build.yml | 14 +++++ CMakePresets.json | 19 +++++++ cdoc/Crypto.cpp | 11 +++- test/libcdoc_boost.cpp | 106 +++++++++++++++++++++++++++++++++++- 4 files changed, 146 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9a0b2b0..3f6ee1ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -118,6 +118,20 @@ jobs: with: name: ${{ matrix.target }} path: ${{ env.DEST }} + sanitizers: + name: Run unit tests with ASan+UBSan + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Install dependencies + run: sudo apt update -qq && sudo apt install --no-install-recommends -y ninja-build libboost-test-dev ${UBUNTU_DEPS} + - name: Configure + run: cmake --preset sanitize + - name: Build + run: cmake --build --preset sanitize --target unittests + - name: Test + run: ctest --test-dir build/sanitize --output-on-failure windows: name: Build on Windows runs-on: ${{ matrix.image }} diff --git a/CMakePresets.json b/CMakePresets.json index 744a600e..51bd80dd 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -124,10 +124,29 @@ "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", "VCPKG_TARGET_TRIPLET": "$env{PLATFORM}-windows-static-md" } + }, + { + "name": "sanitize", + "displayName": "Sanitizers (ASan+UBSan)", + "description": "Debug build with AddressSanitizer and UndefinedBehaviorSanitizer for running the unit tests (system dependencies, no vcpkg; requires boost-test, openssl, libxml2, zlib and flatbuffers development packages)", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined", + "CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address,undefined", + "CMAKE_DISABLE_FIND_PACKAGE_SWIG": "YES", + "CMAKE_DISABLE_FIND_PACKAGE_Doxygen": "YES" + } } ], "buildPresets": [ + { + "name": "sanitize", + "configurePreset": "sanitize" + }, { "name": "macos", "configurePreset": "macos" diff --git a/cdoc/Crypto.cpp b/cdoc/Crypto.cpp index bcf6314e..984843cd 100644 --- a/cdoc/Crypto.cpp +++ b/cdoc/Crypto.cpp @@ -628,8 +628,15 @@ void unpadPKCS1v15CT(const std::vector &em, // range since em.size() >= 11+expected_len > 0). The clamped value // is replaced by synth[i] below when good == 0, so the actual // bytes read here never reach the caller. - size_t in_range = size_t(ge_size(em.size() - 1, src_idx)); // 0 or 0xFF - size_t mask = in_range & ~size_t(0); + // ge_size() returns a single-byte mask (0x00 or 0xFF). It must be + // widened to a full-width size_t mask before splicing indices; + // using the byte mask directly would mix the low byte of src_idx + // with the high bits of (em.size() - 1) and index past the end of + // em for modulus lengths that are not a multiple of 256 bytes + // (e.g. 384-byte EM of a 3072-bit RSA key). The widening is + // branch-free arithmetic: 0x00 -> 0, 0xFF -> ~size_t(0). + size_t in_range = size_t(ge_size(em.size() - 1, src_idx)); // 0x00 or 0xFF + size_t mask = size_t(0) - (in_range & size_t(0x01)); // 0 or ~size_t(0) size_t safe_idx = (src_idx & mask) | ((em.size() - 1) & ~mask); uint8_t real = em[safe_idx]; uint8_t synthetic = synth[i]; diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 544c00b9..4cdedcb0 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -1333,8 +1333,9 @@ BOOST_AUTO_TEST_CASE(RejectsNonDigitNationalId) { BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/PNOEE-30303039 14").valid()); BOOST_CHECK(!libcdoc::parseEtsiRecipientId("etsi/PNOEE-3030303991a").valid()); - // Embedded NUL. - BOOST_CHECK(!libcdoc::parseEtsiRecipientId(std::string("etsi/PNOEE-3030\0039914", 22)).valid()); + // Embedded NUL. (sizeof - 1: the literal is 20 chars; a hard-coded + // length of 22 read 2 bytes past it - caught by ASan.) + BOOST_CHECK(!libcdoc::parseEtsiRecipientId(std::string("etsi/PNOEE-3030\0039914", sizeof("etsi/PNOEE-3030\0039914") - 1)).valid()); } BOOST_AUTO_TEST_CASE(RejectsOversizedNationalId) @@ -1350,3 +1351,104 @@ BOOST_AUTO_TEST_CASE(RejectsOversizedNationalId) } BOOST_AUTO_TEST_SUITE_END() + +// Regression coverage for the constant-time PKCS#1 v1.5 unpadding used by +// the RSA implicit-rejection path (N1 in SecurityReview_Kilo_2026-07.md). +// The index-clamping mask in unpadPKCS1v15CT was a single byte (0x00/0xFF) +// instead of a full-width size_t mask, which spliced the low byte of the +// source index with the high bits of (em.size() - 1) and read past the end +// of the EM buffer for modulus lengths that are not a multiple of 256 +// bytes (e.g. the 384-byte EM of a 3072-bit RSA key, up to 128 bytes OOB). +BOOST_AUTO_TEST_SUITE(RsaImplicitRejectUnpad) + +// Sweep the zero separator across the whole EM block: output must be the +// real message exactly when the padding is valid (00 02 || PS>=8 || 00 || +// M of expected_len) and the synthetic plaintext in every other case. +// Under ASAN this also fails on any out-of-bounds EM access. +static void sweepSeparatorPositions(size_t em_len) +{ + constexpr size_t expected_len = 32; + std::vector synth(expected_len); + for (size_t i = 0; i < expected_len; i++) + synth[i] = uint8_t(0xA0 + i); + + for (size_t sep = 2; sep < em_len; sep++) { + std::vector em(em_len, 0x55); + em[0] = 0x00; + em[1] = 0x02; + em[sep] = 0x00; + + std::vector dst; + BOOST_REQUIRE_EQUAL(libcdoc::Crypto::rsaImplicitRejectFromEM(dst, em, {0x01}, synth, expected_len), libcdoc::OK); + BOOST_REQUIRE_EQUAL(dst.size(), expected_len); + + const size_t msg_len = em_len - sep - 1; + const bool expect_real = (sep >= 10) && (msg_len == expected_len); + for (size_t i = 0; i < expected_len; i++) { + const uint8_t want = expect_real ? em[sep + 1 + i] : synth[i]; + BOOST_CHECK_EQUAL(dst[i], want); + } + } +} + +BOOST_AUTO_TEST_CASE(SeparatorSweepAllModulusSizes) +{ + sweepSeparatorPositions(192); // 1536-bit RSA + sweepSeparatorPositions(256); // 2048-bit RSA + sweepSeparatorPositions(384); // 3072-bit RSA (read up to +128 bytes OOB before the fix) + sweepSeparatorPositions(512); // 4096-bit RSA +} + +BOOST_AUTO_TEST_CASE(ValidPaddingReturnsMessage3072) +{ + // Valid-padding 3072-bit case (message at the end of the EM block); + // the old byte-wide mask happened to compute these indices correctly. + // The actual OOB reproducer is the separator sweep above: for 384-byte + // EMs, separator positions 127..254 made the old mask splice read past + // the buffer (padding is invalid there, so only ASAN observes it). + constexpr size_t em_len = 384; + constexpr size_t expected_len = 32; + constexpr size_t sep = em_len - expected_len - 1; + std::vector em(em_len, 0x55); + em[0] = 0x00; + em[1] = 0x02; + em[sep] = 0x00; + std::vector synth(expected_len, 0xAA); + + std::vector dst; + BOOST_REQUIRE_EQUAL(libcdoc::Crypto::rsaImplicitRejectFromEM(dst, em, {0x01}, synth, expected_len), libcdoc::OK); + BOOST_REQUIRE_EQUAL(dst.size(), expected_len); + for (size_t i = 0; i < expected_len; i++) + BOOST_CHECK_EQUAL(dst[i], em[sep + 1 + i]); +} + +BOOST_AUTO_TEST_CASE(BadHeaderReturnsSynthetic) +{ + constexpr size_t em_len = 384; + constexpr size_t expected_len = 32; + std::vector em(em_len, 0x55); + em[0] = 0x01; // wrong leading byte + em[1] = 0x02; + em[em_len - expected_len - 1] = 0x00; + std::vector synth(expected_len, 0xAA); + + std::vector dst; + BOOST_REQUIRE_EQUAL(libcdoc::Crypto::rsaImplicitRejectFromEM(dst, em, {0x01}, synth, expected_len), libcdoc::OK); + BOOST_CHECK(dst == synth); +} + +BOOST_AUTO_TEST_CASE(NoSeparatorReturnsSynthetic) +{ + constexpr size_t em_len = 384; + constexpr size_t expected_len = 32; + std::vector em(em_len, 0x55); + em[0] = 0x00; + em[1] = 0x02; + std::vector synth(expected_len, 0xAA); + + std::vector dst; + BOOST_REQUIRE_EQUAL(libcdoc::Crypto::rsaImplicitRejectFromEM(dst, em, {0x01}, synth, expected_len), libcdoc::OK); + BOOST_CHECK(dst == synth); +} + +BOOST_AUTO_TEST_SUITE_END() From e37902c48b4ef672f351566ce2ffd62fa8c7099b Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Mon, 27 Jul 2026 14:09:35 +0300 Subject: [PATCH 50/65] Added Utils.cpp to test build --- test/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 56b1ae3e..a41635cc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,6 +3,7 @@ add_executable(unittests ../cdoc/Crypto.cpp ../cdoc/Tar.cpp ../cdoc/XmlReader.cpp + ../cdoc/Utils.cpp ) target_link_libraries(unittests From e7ad0997a1c4718042da5075c30ddb85a859e117 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Mon, 27 Jul 2026 14:54:23 +0300 Subject: [PATCH 51/65] Delete reader and writer in TestPasswordWithlabel --- test/libcdoc_boost.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 4cdedcb0..80e0ad31 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -720,6 +720,9 @@ BOOST_FIXTURE_TEST_CASE_WITH_DECOR(EncryptWithPasswordAndLabel, FixtureBase, * u } BOOST_TEST(reader->nextFile(fi) == libcdoc::END_OF_STREAM); BOOST_TEST(reader->finishDecryption() == libcdoc::OK); + + delete writer; + delete reader; } BOOST_AUTO_TEST_SUITE_END() From 5743645ed8b42a630dec60420803c37569de5eba Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Mon, 27 Jul 2026 15:11:37 +0300 Subject: [PATCH 52/65] Clean up z_stream on zlib errors --- cdoc/ZStream.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cdoc/ZStream.h b/cdoc/ZStream.h index eecfc5b5..615cf217 100644 --- a/cdoc/ZStream.h +++ b/cdoc/ZStream.h @@ -117,6 +117,7 @@ struct ZSource : public DataSource { if (n_read > 0) { buf.insert(buf.end(), in.begin(), in.begin() + n_read); } else if (n_read != 0) { + inflateEnd(&_s); _error = n_read; return _error; } @@ -132,6 +133,7 @@ struct ZSource : public DataSource { buf.clear(); break; default: + inflateEnd(&_s); _error = ZLIB_ERROR; return _error; } From 6ae5ab985ef68b25df5f045780c34887a9176c67 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Mon, 27 Jul 2026 15:43:15 +0300 Subject: [PATCH 53/65] Fixed N3 (base64 decode throws on errors) from 202607 report --- cdoc/Lock.cpp | 12 +++++++++-- cdoc/NetworkBackend.cpp | 4 ++++ cdoc/Utils.cpp | 14 +++++++++++-- cdoc/cdoc-tool.cpp | 17 +++++++++++++--- test/libcdoc_boost.cpp | 45 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 7 deletions(-) diff --git a/cdoc/Lock.cpp b/cdoc/Lock.cpp index 983e7c3c..8ca54604 100644 --- a/cdoc/Lock.cpp +++ b/cdoc/Lock.cpp @@ -83,8 +83,16 @@ Lock::parseLabel(const std::string& label) base64IndPos != std::string::npos) { std::string base64_label(label_wo_prefix.substr(base64IndPos + CDoc2::LABELBASE64IND.size())); - decodedBase64 = jwt::base::decode(base64_label); - label_to_prcss = decodedBase64; + // jwt::base::decode throws std::runtime_error on malformed base64. + // The label comes from the (untrusted) container, so a malformed + // label must not crash the process - treat it as unparseable. + try { + decodedBase64 = jwt::base::decode(base64_label); + label_to_prcss = decodedBase64; + } catch (const std::exception &e) { + LOG_WARN("The label '{}' contains invalid base64: {}", label, e.what()); + return parsed_label; + } } else if (label_wo_prefix.starts_with(",")) { label_to_prcss = label_wo_prefix.substr(1); } else { diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 27ccbb7c..8bbb9c2f 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -458,6 +458,10 @@ libcdoc::NetworkBackend::fetchKey (std::vector& dst, const std::string& error = {}; std::string ks = v.get(); dst = fromBase64(ks); + if (dst.empty()) { + error = FORMAT("Invalid base64 in 'ephemeral_key_material'"); + return NETWORK_ERROR; + } return libcdoc::OK; } diff --git a/cdoc/Utils.cpp b/cdoc/Utils.cpp index 9495e2d0..8d40c0f4 100644 --- a/cdoc/Utils.cpp +++ b/cdoc/Utils.cpp @@ -42,8 +42,18 @@ toBase64(const uint8_t *data, size_t len) std::vector fromBase64(std::string_view data) { - std::string str = jwt::base::details::decode(data, jwt::alphabet::base64::rdata(), "="); - return std::vector(str.cbegin(), str.cend()); + // jwt::base::details::decode throws std::runtime_error on malformed + // input (characters outside the alphabet, bad padding, bad length). + // The decoded data comes from remote servers and containers, i.e. it + // is untrusted, so a decode failure must not crash the process. An + // empty result signals a format error to the callers. + try { + std::string str = jwt::base::details::decode(data, jwt::alphabet::base64::rdata(), "="); + return std::vector(str.cbegin(), str.cend()); + } catch (const std::exception &e) { + LOG_WARN("fromBase64: invalid base64 input: {}", e.what()); + return {}; + } } double diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index 3e655b74..290ae835 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -101,8 +101,15 @@ print_usage(ostream& ofs) static std::vector fromB64(const std::string& data) { - std::string str = jwt::base::details::decode(data, jwt::alphabet::base64::rdata(), "="); - return std::vector(str.cbegin(), str.cend()); + // jwt::base::details::decode throws std::runtime_error on malformed + // base64; an invalid --accept certificate file must not crash the tool. + try { + std::string str = jwt::base::details::decode(data, jwt::alphabet::base64::rdata(), "="); + return std::vector(str.cbegin(), str.cend()); + } catch (const std::exception &e) { + LOG_WARN("Invalid base64: {}", e.what()); + return {}; + } } static void @@ -115,7 +122,11 @@ load_certs(ToolConf& conf, const std::string& filename) for (auto part : parts) { if (part.size() > 3) { std::vector v = fromB64(part); - conf.accept_certs.push_back(v); + if (v.empty()) { + LOG_WARN("Skipping invalid base64 line in {}", filename); + continue; + } + conf.accept_certs.push_back(std::move(v)); } } } else { diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 80e0ad31..468d0940 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -814,6 +814,51 @@ BOOST_AUTO_TEST_CASE(LabelParsingEmptyLabel) } } +// N3 regression: the base64 decoder (jwt::base::decode) throws +// std::runtime_error on malformed input. A crafted container label must +// not crash the process; the label is reported as unparseable instead. +BOOST_AUTO_TEST_CASE(Base64LabelParsingInvalidBase64) +{ + // Characters outside the base64 alphabet. + BOOST_CHECK(libcdoc::Lock::parseLabel("data:;base64,###").empty()); + // Valid alphabet but impossible length (single character). + BOOST_CHECK(libcdoc::Lock::parseLabel("data:;base64,A").empty()); + // Too much padding. + BOOST_CHECK(libcdoc::Lock::parseLabel("data:;base64,QQ===").empty()); + // Same, with a media type part in front. + BOOST_CHECK(libcdoc::Lock::parseLabel("data:application/x-www-form-urlencoded;base64,###").empty()); + // Trailing garbage after otherwise valid base64. + BOOST_CHECK(libcdoc::Lock::parseLabel("data:;base64,dj0x###").empty()); +} + +BOOST_AUTO_TEST_SUITE_END() + +// N3 regression: libcdoc::fromBase64 decodes untrusted data (key server +// and share server responses). Malformed input must yield an empty vector, +// not an exception. +BOOST_AUTO_TEST_SUITE(FromBase64) + +BOOST_AUTO_TEST_CASE(ValidInput) +{ + // "hello world" + std::vector expected {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}; + BOOST_CHECK(libcdoc::fromBase64("aGVsbG8gd29ybGQ=") == expected); + BOOST_CHECK(libcdoc::fromBase64("").empty()); +} + +BOOST_AUTO_TEST_CASE(InvalidInputReturnsEmpty) +{ + // Characters outside the alphabet. + BOOST_CHECK(libcdoc::fromBase64("###").empty()); + BOOST_CHECK(libcdoc::fromBase64("aGVsbG8###").empty()); + // Impossible lengths (not a multiple of 4 after padding rules). + BOOST_CHECK(libcdoc::fromBase64("A").empty()); + // Excess padding. + BOOST_CHECK(libcdoc::fromBase64("QQ===").empty()); + // Padding in the middle. + BOOST_CHECK(libcdoc::fromBase64("QQ==QQ==").empty()); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(TarPaxHeader) From 90c72e654d53cecc89f49876ffc8dde18d33224e Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Tue, 4 Aug 2026 14:05:25 +0300 Subject: [PATCH 54/65] Initial working SID decryption --- cdoc/CDoc2Reader.cpp | 59 ++++---- cdoc/KeyShares.cpp | 31 ++--- cdoc/KeyShares.h | 43 ++++-- cdoc/NetworkBackend.cpp | 289 +++++++++++++++++++--------------------- cdoc/NetworkBackend.h | 32 +++-- cdoc/Utils.cpp | 12 ++ cdoc/Utils.h | 7 +- 7 files changed, 241 insertions(+), 232 deletions(-) diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index ef39b378..8cc00162 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -254,17 +254,6 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) LOG_ERROR("{}", last_error); return libcdoc::DATA_FORMAT_ERROR; } - - // Get authentication token - std::string auth_token; - std::string auth_cert; - if (auto rv = network->authenticateForShares(auth_token, auth_cert); rv != OK) { - setLastError(network->getLastErrorStr(rv)); - LOG_ERROR("{}", last_error); - return rv; - } - - // Get nonces and initialize share array std::vector shares; for (auto& server : servers) { std::vector parts = split(server, ','); @@ -273,25 +262,34 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) LOG_ERROR("{}", last_error); return libcdoc::DATA_FORMAT_ERROR; } - std::string url = parts[0]; - std::string id = parts[1]; - LOG_DBG("Share {} url {}", id, url); + LOG_DBG("Share {} url {}", parts[1], parts[0]); + shares.emplace_back(parts[0], parts[1]); + } + // Get authentication token + SessionData session; + if (auto rv = network->authenticateForShares(session.token, session.cert); rv != OK) { + setLastError(network->getLastErrorStr(rv)); + LOG_ERROR("{}", last_error); + return rv; + } + + // Get nonces + for (auto& share : shares) { std::vector nonce; - result_t result = network->fetchNonce(nonce, url, id, auth_token, auth_cert); + result_t result = network->fetchNonce(nonce, share.base_url, share.share_id, session.token, session.cert); if (result != libcdoc::OK) { setLastError(network->getLastErrorStr(result)); - LOG_ERROR("Cannot fetch nonce from server {}", url); + LOG_ERROR("Cannot fetch nonce {} from server {}", share.share_id, share.base_url); return result; } LOG_DBG("Nonce: {}", std::string(nonce.cbegin(), nonce.cend())); - ShareData acc(url, id, std::string(nonce.cbegin(), nonce.cend())); - shares.push_back(std::move(acc)); + share.nonce = std::string(nonce.cbegin(), nonce.cend()); } /* Create tickets from shares */ - std::vector tickets; - std::vector cert; + std::vector auth_tokens; + AuthenticationData auth; result_t result = NOT_IMPLEMENTED; // fixme: std::string signer = "SMART_ID";// conf->getValue(Configuration::SHARE_SIGNER); @@ -299,17 +297,14 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) if (signer == "SMART_ID") { // "https://sid.demo.sk.ee/smart-id-rp/v2" // std::string url = conf->getValue(Configuration::SID_DOMAIN, Configuration::BASE_URL); - std::string url = "https://cdoc2-rp.test.riaint.ee"; - // "00000000-0000-0000-0000-000000000000" - //std::string relyingPartyUUID = conf->getValue(Configuration::SID_DOMAIN, Configuration::RP_UUID); - // "DEMO" - //std::string relyingPartyName = conf->getValue(Configuration::SID_DOMAIN, Configuration::RP_NAME); - SIDSigner signer(url, auth_token, auth_cert, rcpt_id, network); - result = signer.generateTickets(tickets, shares); + std::string url = "https://cdoc2-rp.dev.riaint.ee/"; + SIDSigner signer(url, session, rcpt_id, network); + result = signer.generateTickets(auth_tokens, shares); if (result != OK) { setLastError(signer.error); } else { - cert = std::move(signer.cert); + auth.cert = std::move(signer.cert); + auth.params = std::move(signer.params); } } else if (signer == "MOBILE_ID") { // "https://sid.demo.sk.ee/smart-id-rp/v2" @@ -321,11 +316,11 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) // "37200000566" std::string phone = conf->getValue(Configuration::MID_DOMAIN, Configuration::PHONE_NUMBER); MIDSigner signer(url, relyingPartyUUID, relyingPartyName, phone, rcpt_id, network); - result = signer.generateTickets(tickets, shares); + result = signer.generateTickets(auth_tokens, shares); if (result != OK) { setLastError(signer.error); } else { - cert = std::move(signer.cert); + auth.cert = std::move(signer.cert); } } else { setLastError(t_("Unknown or missing signer type")); @@ -338,9 +333,9 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) } kek.resize(32); std::fill(kek.begin(), kek.end(), 0); - for (unsigned int i = 0; i < tickets.size(); i++) { + for (unsigned int i = 0; i < auth_tokens.size(); i++) { NetworkBackend::ShareInfo share; - result = network->fetchShare(share, shares[i].base_url, shares[i].share_id, tickets[i], cert); + result = network->fetchShare(share, shares[i].base_url, shares[i].share_id, session.token, session.cert, auth_tokens[i], auth.cert, auth.params); if (result != libcdoc::OK) { setLastError(network->getLastErrorStr(result)); LOG_ERROR("Cannot fetch share {}", i); diff --git a/cdoc/KeyShares.cpp b/cdoc/KeyShares.cpp index 4ddae332..b0250be2 100644 --- a/cdoc/KeyShares.cpp +++ b/cdoc/KeyShares.cpp @@ -36,27 +36,16 @@ #include #include -static std::string -toBase64URL(const std::string& data) -{ - return jwt::base::details::encode(data, jwt::alphabet::base64url::data(), ""); -} - -static std::string -toBase64URL(const std::vector& data) -{ - return toBase64URL(std::string((const char *) data.data(), data.size())); -} - -libcdoc::ShareData::ShareData(const std::string& _base_url, const std::string& _share_id, const std::string& _nonce) -: base_url(_base_url), share_id(_share_id), nonce(_nonce) -{ -} - std::string libcdoc::ShareData::getURL() { - return base_url + "key-shares/" + share_id + "?nonce=" + nonce; + // fixme: Understand where the trailing '/' is dropped + std::string url = base_url; + if (!base_url.ends_with('/')) + url = url + "/"; + url = url + + "key-shares/" + share_id + "?nonce=" + nonce; + LOG_DBG("Share URL: {}", url); + return url; } namespace libcdoc { @@ -210,12 +199,12 @@ SIDSigner::signDigest(std::vector& dst, const std::vector& dig { LOG_TRACE_KEY("SID signing: {}", digest); - result_t result = network->signSID(dst, cert, url, session_token, auth_cert, rcpt_id, digest, libcdoc::CryptoBackend::SHA_256); + result_t result = network->signSID(dst, cert, params, url, session.token, session.cert, rcpt_id, digest, libcdoc::CryptoBackend::SHA_256); if (result != OK) { error = network->getLastErrorStr(result); } - LOG_DBG("SID dignature:{}", toHex(dst)); + LOG_DBG("SID signature:{}", toHex(dst)); LOG_DBG("SID signatureB64:{}", toBase64URL(dst)); LOG_DBG("SID certificateB64:{}", toBase64(cert)); @@ -223,7 +212,7 @@ SIDSigner::signDigest(std::vector& dst, const std::vector& dig } result_t -libcdoc::MIDSigner::signDigest(std::vector& dst, const std::vector& digest) +MIDSigner::signDigest(std::vector& dst, const std::vector& digest) { LOG_TRACE_KEY("MID signing: {}", digest); diff --git a/cdoc/KeyShares.h b/cdoc/KeyShares.h index 86885ba1..6f4711d7 100644 --- a/cdoc/KeyShares.h +++ b/cdoc/KeyShares.h @@ -37,11 +37,11 @@ struct ShareData { /** * @brief Construct a new Share Data object for authentication * - * @param base_url share server base url (e.g. https://cdoc2.my.domain/v1/) - * @param share_id share id from capsule - * @param nonce session nonce from server + * @param _base_url share server base url (e.g. https://cdoc2.my.domain/v1/) + * @param _share_id share id from capsule */ - ShareData(const std::string& base_url, const std::string& share_id, const std::string& nonce); + ShareData(const std::string& _base_url, const std::string& _share_id) : base_url(_base_url), share_id(_share_id) {} + /** * @brief Get share url @@ -53,6 +53,27 @@ struct ShareData { std::string getURL(); }; +/** + * @brief Session data + * + * The session token and certificate provided by AUTH server + * + */ +struct SessionData { + std::string token; + std::string cert; +}; + +/** + * @brief Authentication data for share tickets + * + * The certificate and signature parameters from RP server + */ +struct AuthenticationData { + std::vector cert; + std::string params; +}; + /** * @brief Abstract base class for MID/SID signing * @@ -94,6 +115,7 @@ struct Signer { * */ std::vector cert; + std::string params; /** * @brief The text of last error * @@ -124,20 +146,17 @@ struct SIDSigner : public Signer { * @brief Full session token * */ - // fixme: parsed? - const std::string session_token; - // fixme: - const std::string auth_cert; + const SessionData& session; + /** * @brief Construct a new SIDSigner object * * @param _url SmartID gateway url - * @param _rp_uuid Relying party UUID - * @param _rp_name Relying party name + * @param _session Full session data (token and certificate) * @param _rcpt_id Recipient full id in etsi format (ets/PNOEE-XYZXYZXYZXY) */ - SIDSigner(const std::string& _url, const std::string& _session_token, const std::string& _cert, const std::string& _rcpt_id, NetworkBackend *network) - : Signer(_rcpt_id, "RS256", network), url(_url), session_token(_session_token), auth_cert(_cert) {} + SIDSigner(const std::string& _url, const SessionData& _session, const std::string& _rcpt_id, NetworkBackend *network) + : Signer(_rcpt_id, "RSASSA-PSS+ACSP_V2", network), url(_url), session(_session) {} result_t signDigest(std::vector& dst, const std::vector& digest) final; }; diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index dc7002ce..2bd5941f 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -105,18 +105,25 @@ struct MIDSIDResultData { static constexpr auto midsid_results = std::to_array({ {libcdoc::NetworkBackend::MIDSID_USER_REFUSED, "USER_REFUSED", "User refused the session"}, {libcdoc::NetworkBackend::MIDSID_TIMEOUT, "TIMEOUT", "User did not confirm action within the timeframe"}, - {libcdoc::NetworkBackend::MIDSID_DOCUMENT_UNUSABLE, "DOCUMENT_UNUSABLE", "Smart document unusable, please contact Smart ID customer support"}, + {libcdoc::NetworkBackend::MIDSID_DOCUMENT_UNUSABLE, "DOCUMENT_UNUSABLE", "Document unusable, please contact Smart ID customer support"}, {libcdoc::NetworkBackend::MIDSID_WRONG_VC, "WRONG_VC", "User chose a wrong Smart ID verification code"}, {libcdoc::NetworkBackend::MIDSID_REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP, "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP", "Smart ID app does not support current protocol"}, {libcdoc::NetworkBackend::MIDSID_USER_REFUSED_CERT_CHOICE, "USER_REFUSED_CERT_CHOICE", "User refused certificate choice"}, + {libcdoc::NetworkBackend::MIDSID_USER_REFUSED_INTERACTION, "USER_REFUSED_INTERACTION", "User refused the interaction"}, + {libcdoc::NetworkBackend::MIDSID_PROTOCOL_FAILURE, "PROTOCOL_FAILURE", "There was a logical error in the signing protocol"}, + {libcdoc::NetworkBackend::MIDSID_EXPECTED_LINKED_SESSION, "EXPECTED_LINKED_SESSION", "The app received a different transaction while waiting for the linked session"}, + {libcdoc::NetworkBackend::MIDSID_SERVER_ERROR, "SERVER_ERROR", "The process was terminated due to server-side technical error"}, + {libcdoc::NetworkBackend::ACCOUNT_UNUSABLE, "ACCOUNT_UNUSABLE", "The account is currently unusable"}, + // Old + {libcdoc::NetworkBackend::MIDSID_NOT_MID_CLIENT, "NOT_MID_CLIENT", "user has no active Mobile-ID certificates"}, + {libcdoc::NetworkBackend::MIDSID_USER_CANCELLED, "USER_CANCELLED", "user rejected the operation on the device"}, + {libcdoc::NetworkBackend::MIDSID_SIGNATURE_HASH_MISMATCH, "SIGNATURE_HASH_MISMATCH", "mismatch between SIM and service provider configuration"}, + {libcdoc::NetworkBackend::MIDSID_PHONE_ABSENT, "PHONE_ABSENT", "SIM card is not available"}, + {libcdoc::NetworkBackend::MIDSID_USER_REFUSED_DISPLAYTEXTANDPIN, "USER_REFUSED_DISPLAYTEXTANDPIN", "User canceled the PIN choice"}, {libcdoc::NetworkBackend::MIDSID_USER_REFUSED_VC_CHOICE, "USER_REFUSED_VC_CHOICE", "User canceled the verification code choice"}, {libcdoc::NetworkBackend::MIDSID_USER_REFUSED_CONFIRMATIONMESSAGE, "USER_REFUSED_CONFIRMATIONMESSAGE", "User refused the confirmation message"}, {libcdoc::NetworkBackend::MIDSID_USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE, "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE", "User refused the confirmation message and verification code choice"}, - {libcdoc::NetworkBackend::MIDSID_NOT_MID_CLIENT, "NOT_MID_CLIENT", "User is not a Mobile ID client"}, - {libcdoc::NetworkBackend::MIDSID_USER_CANCELLED, "USER_CANCELLED", "User canceled the Mobile ID operation"}, - {libcdoc::NetworkBackend::MIDSID_SIGNATURE_HASH_MISMATCH, "SIGNATURE_HASH_MISMATCH", "SIM card signature mismatch, please contact the mobile provider"}, - {libcdoc::NetworkBackend::MIDSID_PHONE_ABSENT, "PHONE_ABSENT", "SIM card is not available"}, {libcdoc::NetworkBackend::MIDSID_DELIVERY_ERROR, "DELIVERY_ERROR", "SMS sending error"}, {libcdoc::NetworkBackend::MIDSID_SIM_ERROR, "SIM_ERROR", "Invalid response from SIM card"} }); @@ -307,17 +314,26 @@ static libcdoc::result_t get(httplib::SSLClient& cli, httplib::Headers& hdrs, const std::string& path, picojson::value& rsp_json) { // Capture TLS and HTTP errors + LOG_DBG("GET: {}", path); + for (auto h : hdrs) { + LOG_TRACE(" Header {}: {}", h.first, h.second); + } httplib::Result res = cli.Get(path, hdrs); if (!res) { error = FORMAT("Cannot connect to https://{}:{}{}", cli.host(), cli.port(), path); return libcdoc::NetworkBackend::NETWORK_ERROR; } - httplib::Response rsp = res.value(); - auto status = rsp.status; + int status = res->status; + LOG_DBG("Status: {}", status); + LOG_TRACE(" Body: {}", res->body); + for (auto h : res->headers) { + LOG_TRACE(" Header {}: {}", h.first, h.second); + } if ((status < 200) || (status >= 300)) { error = FORMAT("Http status {}", status); return libcdoc::NetworkBackend::NETWORK_ERROR; } + httplib::Response rsp = res.value(); picojson::parse(rsp_json, rsp.body); error = {}; return libcdoc::OK; @@ -563,8 +579,7 @@ waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& libcdoc::result_t libcdoc::NetworkBackend::authenticateForShares(std::string& token, std::string& cert) { -#if 1 - static const std::string url = "https://cdoc2-auth.test.riaint.ee"; + static const std::string url = "https://cdoc2-auth.dev.riaint.ee"; // Start authentication std::string host, path; int port; @@ -640,11 +655,6 @@ libcdoc::NetworkBackend::authenticateForShares(std::string& token, std::string& cert = auth_rsp.cert; auto parts = split(auth_rsp.sessionToken, '~'); -#else - token = "eyJraWQiOiJEN3Y5aF8wb1RuQUxxNmx6aVJPYUNGRzBVbFRCYmdPQTVMSGpfYkoteVo4IiwidHlwIjoidm5kLmNkb2MyLnNlc3Npb24tdG9rZW4udjIrc2Qtand0IiwiYWxnIjoiRVMyNTYifQ.eyJycENoYWxsZW5nZSI6ImhSV1hodWhueDJ6RDgrQ0NvSk1sZERRMkNyOW8yRmFhNStmU1ZVSkVSTVQ5YW1mVlFPUEp0UGRsaW9yU01PbWNaY2tNdWt3OHZwR3FhQVdGY0JvcDhRPT0iLCJzdWIiOiJldHNpL1BOT0VFLTM3MTA0MDgyNzEwIiwic2lnbmF0dXJlIjp7InZhbHVlIjoiQXlUd3JhOW10VGt6SUxodi9iQ0NhaUI5QWtNcktTRnVKT0FkKzZTZVhjeFJTTTVvSVFxWDlyZlVHNTB6UFpqZlYrQ3RQUHlnc3paZVpBODVFNm1NMzU5SjFIOGJVZDZiVHBCdGNTcWtLTzVtWFNzbXJmT2NYdDdjZWJTdW1MZ0liQ1VJckxISDA2QjVXeldiWjVYcmMzVkhscVBqVjVGbkhOejVtMUdyb3lKNkoyWHpNTkNmMVZNU1JtcC9meU16dWd5VXcxS1pqUlhTcnh3Wk5TRGhyMzVRL1l2NmVFMVdLdnZudnBjTTBMQWJobGxOZDhBSmdXMUhFZThrNGZYWTlKemxYN0YyWk5mdXM5a1MzQmNLMzhZZ0FKRlk4d0JYQzc5ODZQL0xOcjYxK1pOSkxUVUlGWU0vZHRudTcxdzhkWEo4TXVGYm5tbEdGVERuVlBjQXlSZmw5ZHdnS1Yvdmg3SFBWTlJ6d21TVjBCU1cwL1p4RVlFcmdMQ0lXaVJaSEFzYzBZVHppN0UycjVTV1A1b05KVVNtbnRHM0RpRmpWNUMwUERRa2hFUHpQYlhaYWg4MHhMQ21yT1dUelp2dVdXYnFRcHUwalR6SDRLOTc1bStlMlpRMEZMZEpsU3RYa0FlSTAwcE5va25LQW1MTkRkREh4Q2xVYVlJbzN5aDlFVkc0RlhGaTlqTnBiOWdKMGNEWEJmR3hpbENXUmVUZkF2TkE3c0lST0lhNjA0YmhjWmcyaTRybWxxYjdJNS9jYVQ1Y3lDb1pHSlZxa0lyVmtGa3ZDL1hjYzBzTy9Cdmpaa0doTWNWNnZEMU01eFVjcXNKK1d3ZHlXMjhyOURQTUZjdXRoRTl6QVVLbHY4OE9aRlFkc2xBeFJ6UEJSNHhDTnkvUm5WWVRSL1FmS0wrV1l3MTcyMU0zZ1d2WURMbXoyZ3VrWlBqNHBncklDTXU2cFV3SW9kUDNROTRROW5IeGtLNE5aWFY1eStMVjJNckNXVWZTWWh6dHNuVlJXWlhCMmplNFd5Yyt0b1IvcjNoQlpZL3FRa3BxTEhQcnExcDBjY2hnMEhtM2RzQm5RYzJPL3FKYUUyTGdYQVBYVVZ3cmJiVkFzb2NZc2tHeEF6UjdpWmlRQ2FVSkE0Um5zTGNkaitLT20zc0lHMVJ5azE3czNoeG5CYkZocGRHQnp5WWxSR25ncm5heW1CU3J3bDRBeVhoQTE1M09YbEp5b01EQkU0SmkxYlJwSXpwT1ZpSm45T2M0MHhQK1hpbU5MM0tnMm9LcS9EZEk3Z1Evdmd0WkFOMFFsU0pNcHo1TlJlRWRuTGgyUUt3V2duSGtYdGJ3ZUFXNFJIdml2M1lrNmtRYiIsInNlcnZlclJhbmRvbSI6IkVsMFVSWW5vdUNWWEcrVFpDS0lTNko4SyIsInVzZXJDaGFsbGVuZ2UiOiJNcEFrOTN3YTJFUEhGMW9wSmFQRVRHbTNjZDNNX29VYlBnNHRuVFZwZVRJIiwic2lnbmF0dXJlQWxnb3JpdGhtIjoicnNhc3NhLXBzcyIsImZsb3dUeXBlIjoiTm90aWZpY2F0aW9uIiwic2lnbmF0dXJlQWxnb3JpdGhtUGFyYW1ldGVycyI6eyJoYXNoQWxnb3JpdGhtIjoiU0hBLTI1NiIsIm1hc2tHZW5BbGdvcml0aG0iOnsiYWxnb3JpdGhtIjoiaWQtbWdmMSIsInBhcmFtZXRlcnMiOnsiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYifX0sInNhbHRMZW5ndGgiOjMyLCJ0cmFpbGVyRmllbGQiOiIweGJjIn19LCJpc3MiOiJodHRwczovL2Nkb2MyLWF1dGgudGVzdC5yaWFpbnQuZWUiLCJzY2hlbWVOYW1lIjoic21hcnQtaWQtZGVtbyIsInNpZ25hdHVyZVByb3RvY29sIjoiUlNBU1NBLVBTUytBQ1NQX1YyIiwiX3NkIjpbIlZhb3ZCN0RYSm44Q21aOEFSd2dvODJNR2pPSkc2VTA4Zm9VVXl3UlJzaVkiXSwiaW50ZXJhY3Rpb25zRGlnZXN0IjoiWG9pN1F1eDB0NmxFaXN6MU9Gc1dIUGdhM1l6Q2QzQ2ROQ0t1VE91UHZBWT0iLCJfc2RfYWxnIjoic2hhLTI1NiIsImV4cCI6MTc4NDcwMjI2OCwiaWF0IjoxNzg0NjE1ODY4LCJpbnRlcmFjdGlvblR5cGVVc2VkIjoiY29uZmlybWF0aW9uTWVzc2FnZUFuZFZlcmlmaWNhdGlvbkNvZGVDaG9pY2UiLCJycE5hbWUiOiJERU1PIn0.ekJ-J--6wuLhvsmxwEOpOLqjYCV1QMiYaUjwbAsq6Nt6qNdvMy81ArUCyN5l3CENfUKcgcQdw3HtQxuPEk0_PQ~WyJMdlRKN3VaNF9ValBJU3JFc3h3Wmp3IiwiYXVkIixbeyIuLi4iOiJYTnR2amNRZEhTbkhNNWdPWDhwcXpHWHUzMzY4VE0xNExjS2h5Z0dzZWM0In0seyIuLi4iOiJzX1d1aDFqenVFbmo3ZVZVdldtcXIzaURVSmNMcGlkQ1BpZDVYOXFuWi0wIn0seyIuLi4iOiIyVmRsWGpaMU9DcndwbGpGYURMMFJ2N3VZNEtvZ1hoQWdobjBTM1VRZ2dnIn1dXQ~WyI3RmxpMXhPd3hhQXdBWVZkR1ZJVkVBIiwiaHR0cHM6Ly9jZG9jMi1ycC50ZXN0LnJpYWludC5lZS9zZXNzaW9uX25vbmNlL3EyaUxJS2VveXpEQ1RyeGRJbVk0aUEiXQ~WyJfVUpmQ1hDbXJyMml1N3N4NXo3QWZ3IiwiaHR0cHM6Ly9jZG9jMi1zaGFyZXMudGVzdC5yaWFpbnQuZWUvc2Vzc2lvbl9ub25jZS9GTGw2b3dramlJZVMwVzBqUHlLNll3Il0~WyJQMURtczZFbmk3LU4yS0Y0OXl2SUZBIiwiaHR0cHM6Ly9jZG9jMi1zaGFyZXNleHRlcm5hbC50ZXN0LnJpYWludC5lZS9zZXNzaW9uX25vbmNlL3hHdzVxR1g1alFLOUlFVi1CbVZKZWciXQ~"; - cert = "MIIGuzCCBkCgAwIBAgIQDV-hZUN9xS5yEzxpvHOPejAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjUxMDI5MDcwOTEwWhcNMjgxMDI4MDYwOTEwWjBpMQswCQYDVQQGEwJFRTEZMBcGA1UEAwwQS0FQTElOU0tJLExBVVJJUzESMBAGA1UEBAwJS0FQTElOU0tJMQ8wDQYDVQQqDAZMQVVSSVMxGjAYBgNVBAUTEVBOT0VFLTM3MTA0MDgyNzEwMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAkOP8-thy1C0eG_CuqA5stRrjUaD5T07Q7-JZcZcWPnRTBROZizhehHozd-Kqxs_PH4I2lFCmGx8QgoeIba4VO7NeZ-DacaQtfGHlX85pYpzJH3l31e-xs_oQsW9CIp07MpkfcbuB16T2X88S2_YCFC2pbgxJg3CpF4ejL-zjT18SeRfXmHwPEP9kuLYFSZ6yALDIRLf-_r0SucwARNDSG0MVu-riE0xDZjok0SqCfscaa027sco43T4l2OSd1G8yGHwoIhZuTepWpkfgwUR3RUhAFZdPUEBvtLbeLaQ_J5BG6gHWcZtHD_wEXjcf7HPygxVk8XPlmndUPNmt-gtWLh5LuGqjgL1o8P2M9SXkNHPy0mD9s3noMS2agluVRA3dJoitn3hG_nBS3ADOtwUwGtb11KMZJMJaEePATv5iNLEXEiFBDcST4F2QEjs96QBLGwJEpToOxHDbfVe1p9OpRWjsSrf9IJAXxjsm37uP1QgaqRMAUaF8LZaSyWQ9OIKVfMQZ8DR-kl4BYyAMyxTNewL7Ht4f4qYV2WUvrD3U0puhVWXH5dAzgDXVmzYHgsRj_gCTGNoQBCop3X2ZQ-lbQZvMto6xtL-tHl9oG8b_hcTvv-UCIIhj2Sokb7RljG0UAyn4rsEUya_pBreaFKBsufTIVt-oNbjAReqzdNXT_gtmHm8P920Yq7i0qpnEAouigpvNcB_04o11ZGpLf4sOoJb-IvOoWnf0f52PFOTDxroP9N9LM8umNFT7WPbL9JLev9huURRBobp2D6Rf47Ktgiq4KzuOMLUMRRDUTAYrKftwGwye6nlV2fONX7PLRqlqzhLDe5ljEwchJ7asiC0FPN5IG-j4wl1RBMjlNcaGfgnhalMsDIiBRlTg4qIkQwwz4y4GRJXjfgz2J4CymQDEdha-pMePRCKttVDPAsTS5y4ACQphyWjVxfEvGC7z4XkDbr7kPhOuAkkfhdhsdOM5oFGz2KeJQmeZtEi5gMZ6BHjbYoYJPJKXa5B_yYu1d6dbAgMBAAGjggH1MIIB8TAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS0zNzEwNDA4MjcxMC1OWEhTLVEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBAjAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5NzEwNDA4MTIwMDAwWjAWBgNVHSUEDzANBgsrBgEEAYPmYgUHADA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIwMjRlLmNybDAdBgNVHQ4EFgQUJRKanMsHUMXf5zhIfK51Qn_n6lAwDgYDVR0PAQH_BAQDAgeAMAoGCCqGSM49BAMDA2kAMGYCMQC8hVUFpywnFWTqUB6Rw-CADvHrQBft9H9pyM5IIUanrS2O-KFsc0TsMojORxcg_xkCMQDE71vvGtizs-sEk23jam_2pi76C1e5rDjfPDn6qFBuvvEtpcQlmX7e9JDCfGcKJtc="; - auto parts = split(token, '~'); -#endif if (parts.size() < 3) { error = "Invalid JWT-SD token"; LOG_WARN("Invalid JWT-SD token"); @@ -703,10 +713,11 @@ libcdoc::NetworkBackend::fetchNonce(std::vector& dst, const std::string if (result = setProxy(cli, this); result != OK) return result; SessionToken stoken(auth_token); + std::string session_token_disclosed = stoken.discloseForUrl(url); std::string full = path + "/key-shares/" + share_id + "/nonce"; httplib::Headers hdrs; - hdrs.insert({"x-cdoc2-session-token", stoken.discloseForUrl(url)}); + hdrs.insert({"x-cdoc2-session-token", session_token_disclosed}); hdrs.insert({"x-cdoc2-session-x5c", auth_cert}); LOG_DBG("POST nonce request to: {}", full); httplib::Response rsp; @@ -732,7 +743,7 @@ libcdoc::NetworkBackend::fetchNonce(std::vector& dst, const std::string } libcdoc::result_t -libcdoc::NetworkBackend::fetchShare(ShareInfo& share, const std::string& url, const std::string& share_id, const std::string& ticket, const std::vector& cert) +libcdoc::NetworkBackend::fetchShare(ShareInfo& share, const std::string& url, const std::string& share_id, const std::string& session_token, const std::string& session_cert, const std::string& auth_token, const std::vector& auth_cert, const std::string& auth_params) { LOG_DBG("Get share from: {}", url); @@ -749,11 +760,17 @@ libcdoc::NetworkBackend::fetchShare(ShareInfo& share, const std::string& url, co if (result != OK) return result; if (result = setProxy(cli, this); result != OK) return result; + SessionToken stoken(session_token); + std::string session_token_disclosed = stoken.discloseForUrl(url); + std::string full = path + "/key-shares/" + share_id; LOG_DBG("Share url: {}", full); httplib::Headers hdrs; - hdrs.insert({"x-cdoc2-auth-ticket", ticket}); - hdrs.insert({"x-cdoc2-auth-x5c", std::string("-----BEGIN CERTIFICATE-----") + toBase64(cert) + "-----END CERTIFICATE-----"}); + hdrs.insert({"x-cdoc2-session-token", session_token_disclosed}); + hdrs.insert({"x-cdoc2-session-x5c", session_cert}); + hdrs.insert({"x-cdoc2-auth-token", auth_token}); + hdrs.insert({"x-cdoc2-auth-x5c", toBase64URL(auth_cert)}); + hdrs.insert({"x-cdoc2-sid-rpv3-signature-parameters", toBase64URL(auth_params)}); picojson::value rsp_json; result = get(cli, hdrs, full, rsp_json); if (result != libcdoc::OK) return result; @@ -795,24 +812,26 @@ libcdoc::NetworkBackend::showFeedback(SIDMIDFeedback& feedback) struct SIDResponse { // Signature value, base64 encoded std::string signature; - // Signature algorithm, in the form of sha256WithRSAEncryption - std::string algorithm; // Signer certificate, base64 encoded std::string cert; + // Full signature json to create verification info + picojson::object signature_json; + std::string inter_type_used; }; namespace libcdoc { static result_t -waitForResult(SIDResponse& dst, httplib::SSLClient& cli, const std::string& path, const std::string& session_id, double seconds, bool is_sid) +waitForResult(SIDResponse& dst, httplib::SSLClient& cli, const std::string& path, const std::string& auth_token_disclosed, const std::string& auth_cert, const std::string& session_id, double seconds) { - httplib::Headers hdrs; - double end = libcdoc::getTime() + seconds; - std::string full = path + session_id + "?timeoutMs=" + std::to_string((int) (seconds * 1000)); + std::string full = path + session_id; LOG_DBG("SID/MID session query path: {}", full); while (libcdoc::getTime() < end) { picojson::value rsp; + httplib::Headers hdrs; + hdrs.insert({"x-cdoc2-session-token", auth_token_disclosed}); + hdrs.insert({"x-cdoc2-session-x5c", auth_cert}); result_t result = get(cli, hdrs, full, rsp); if (result != OK) return result; if (!rsp.is()) { @@ -834,25 +853,21 @@ waitForResult(SIDResponse& dst, httplib::SSLClient& cli, const std::string& path std::this_thread::sleep_for(duration); continue; } else if (str != "COMPLETE") { - error = FORMAT("Invalid SmartID state: {}", str); + error = FORMAT("Invalid state value: {}", str); LOG_WARN("{}", error); return NetworkBackend::NETWORK_ERROR; } + // State is complete, check for end result v = rsp.get("result"); - picojson::value w; - if (is_sid) { - if (!v.is()) { - error = "Result is not a JSON object"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - w = v.get("endResult"); - } else { - w = v; + if (!v.is()) { + error = "Result is not an object"; + LOG_WARN("{}", error); + return NetworkBackend::NETWORK_ERROR; } + picojson::value w = v.get("endResult"); if (!w.is()) { - error = "EndResult is not a string"; + error = "Result is not a string"; LOG_WARN("{}", error); return NetworkBackend::NETWORK_ERROR; } @@ -867,7 +882,10 @@ waitForResult(SIDResponse& dst, httplib::SSLClient& cli, const std::string& path LOG_WARN("EndResult is not OK: {}", str); return result; } + // documentNumber + // details + // signatureProtocol // Signature v = rsp.get("signature"); if (v.is()) { @@ -878,26 +896,25 @@ waitForResult(SIDResponse& dst, httplib::SSLClient& cli, const std::string& path return NetworkBackend::NETWORK_ERROR; } dst.signature = w.get(); - w = v.get("algorithm"); - if (!w.is()) { - error = "Algorithm is not a string"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - dst.algorithm = w.get(); + dst.signature_json = v.get(); + } + // Interaction type + v = rsp.get("interactionTypeUsed"); + if (!v.is()) { + error = "InteractionTypeUsed is not a string"; + LOG_WARN("{}", error); + return NetworkBackend::NETWORK_ERROR; } + dst.inter_type_used = v.get(); + // Certificate v = rsp.get("cert"); - if (is_sid) { - if (!v.is()) { - error = "Certificate is not a JSON object"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - w = v.get("value"); - } else { - w = rsp.get("cert"); + if (!v.is()) { + error = "Certificate is not a JSON object"; + LOG_WARN("{}", error); + return NetworkBackend::NETWORK_ERROR; } + w = v.get("value"); if (!w.is()) { error = "Certificate value is not a string"; LOG_WARN("{}", error); @@ -917,18 +934,38 @@ waitForResult(SIDResponse& dst, httplib::SSLClient& cli, const std::string& path } libcdoc::result_t -libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector& cert, - const std::string& url, const std::string& auth_token, const std::string& auth_cert, +libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector& cert, std::string& params, + const std::string& url, const std::string& session_token, const std::string& session_cert, const std::string& rcpt_id, const std::vector& digest, CryptoBackend::HashAlgorithm algo) { + // Start authentication: + // + // semanticsIdentifier: PNOEE-XYZ... + // certificateLevel: QUALIFIED + // signatureProtocol: ACSP_V2 + // signatureProtocolParameters: + // rpChallenge: S480uRoCX4pAb1tWqAy8WGl/AWE1RnqaP2y5iamCDhlCyQrMTVa5d8Dh34sZ+UePHXRNKTwz7QTvsIL1ls05AQ== + // signatureAlgorithm: rsassa-pss + // signatureAlgorithmParameters: + // hashAlgorithm: SHA-512 + // interactions: W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IkRlY3J5cHRpbmcgY29udGFpbmVyIGZpbGUgXCJ0ZXN0LnR4dFwiIn0seyJ0eXBlIjoiZGlzcGxheVRleHRBbmRQSU4iLCJkaXNwbGF5VGV4dDYwIjoiRGVjcnlwdGluZyBjb250YWluZXIgZmlsZSBcInRlc3QudHh0XCIifV0= + // vcType: numeric4 + // std::string certificateLevel = "QUALIFIED"; + std::string hashAlgorithm = "SHA-256"; + if (!rcpt_id.starts_with("etsi/")) return libcdoc::INTERNAL_ERROR; + std::string semanticIdentifier = rcpt_id.substr(5); + auto nonce_bytes = Crypto::random(16); if (nonce_bytes.empty()) return libcdoc::CRYPTO_ERROR; std::string nonce = libcdoc::toBase64(nonce_bytes); + SessionToken stoken(session_token); + std::string session_token_disclosed = stoken.discloseForUrl(url); + picojson::object sap = { - {"hashAlgorithm", picojson::value("SHA-256")} + {"hashAlgorithm", picojson::value(hashAlgorithm)} }; picojson::object spp = { {"rpChallenge", picojson::value(toBase64(digest))}, @@ -942,13 +979,17 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector picojson::array inter_arr = { picojson::value(inter) }; - std::string inter_str = picojson::value(inter_arr).serialize(); + //std::string inter_str = picojson::value(inter_arr).serialize(); + std::string inter_str = "[{\"type\":\"confirmationMessageAndVerificationCodeChoice\",\"displayText200\":\"Do you want to decrypt the document\"}]"; + LOG_DBG("Interactions: {}", inter_str); + inter_str = toBase64((const uint8_t *) inter_str.data(), inter_str.size()); + std::string inter_str_64 = toBase64((const uint8_t *) inter_str.data(), inter_str.size()); picojson::object obj = { - {"semanticsIdentifier", picojson::value(rcpt_id)}, + {"semanticsIdentifier", picojson::value(semanticIdentifier)}, {"certificateLevel", picojson::value(certificateLevel)}, {"signatureProtocol", picojson::value("ACSP_V2")}, {"signatureProtocolParameters", picojson::value(spp)}, - {"interactions", picojson::value(toBase64((const uint8_t *) inter_str.data(), inter_str.size()))}, + {"interactions", picojson::value(inter_str_64)}, {"vcType", picojson::value("numeric4")} }; picojson::value query(obj); @@ -963,8 +1004,6 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector LOG_DBG("PORT:{}", port); LOG_DBG("PATH:{}", path); - SessionToken stoken(auth_token); - LOG_DBG("Starting client: {} {}", host, port); httplib::SSLClient cli(host, port); if (result = applySSLTimeout(cli, this); result != OK) return result; @@ -972,64 +1011,6 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector if (result != OK) return result; if (result = setProxy(cli, this); result != OK) return result; - // - // Let user choose certificate (if multiple) - // - std::string full = path + "/sid/authenticate"; - LOG_DBG("SmartID path: {}", full); - httplib::Headers hdrs; - hdrs.insert({"x-cdoc2-session-token", stoken.discloseForUrl(url)}); - hdrs.insert({"x-cdoc2-session-x5c", auth_cert}); - httplib::Response rsp; - result = post(cli, full, hdrs, query.serialize(), rsp); - if (result != libcdoc::OK) return result; - - return NOT_IMPLEMENTED; -#if 0 - LOG_DBG("Response: {}", rsp.body); - picojson::value v; - std::string parse_err = picojson::parse(v, rsp.body); - if (!parse_err.empty()) { - error = FORMAT("JSON parse error: {}", parse_err); - LOG_ERROR("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - if (!v.is()) { - error = "Invalid SmartID response"; - LOG_WARN("Invalid SmartID response"); - return NetworkBackend::NETWORK_ERROR; - } - picojson::value w = v.get("sessionID"); - if (!w.is()) { - error = "Invalid SmartID response"; - LOG_WARN("Invalid SmartID response"); - return NetworkBackend::NETWORK_ERROR; - } - std::string sessionID = w.get(); - LOG_DBG("SessionID: {}", sessionID); - - SIDResponse sidrsp; - result = waitForResult(sidrsp, cli, path + "/session/", sessionID, 60, true); - if (result != OK) return result; - LOG_DBG("Certificate: {}", sidrsp.cert); - - // - // Sign - // - std::string_view algo_name = hashAlgorithmToSidMidName(algo); - if (algo_name.empty()) { - error = "Unsupported hash algorithm for Smart-ID"; - LOG_ERROR("Unsupported hash algorithm for Smart-ID: {}", - static_cast(algo)); - return libcdoc::WRONG_ARGUMENTS; - } - - if (digest.empty()) { - error = "Empty digest"; - LOG_ERROR("Empty digest passed to signSID"); - return libcdoc::WRONG_ARGUMENTS; - } - // Generate code SIDMIDFeedback fb; std::array b; @@ -1038,63 +1019,65 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector result = showFeedback(fb); if (result != OK) return result; - picojson::object aio1 = { - {"type", picojson::value("confirmationMessageAndVerificationCodeChoice")}, - {"displayText200", picojson::value("Do you want to decrypt the document")} - }; - picojson::array aio = { - picojson::value(aio1) - }; - picojson::object qobj = { - {"relyingPartyUUID", picojson::value(rp_uuid)}, - {"relyingPartyName", picojson::value(rp_name)}, - {"hash", picojson::value(toBase64(digest))}, - {"hashType", picojson::value(std::string(algo_name))}, - {"allowedInteractionsOrder", - picojson::value(aio) - } - }; - query = picojson::value(qobj); - LOG_DBG("JSON:{}", query.serialize()); // - // Sign digest + // Begin authentication session // - full = path + "/authentication/" + rcpt_id; + std::string full = path + "/sid/authenticate"; LOG_DBG("SmartID path: {}", full); + httplib::Headers hdrs; + hdrs.insert({"x-cdoc2-session-token", session_token_disclosed}); + hdrs.insert({"x-cdoc2-session-x5c", session_cert}); + httplib::Response rsp; result = post(cli, full, hdrs, query.serialize(), rsp); if (result != libcdoc::OK) return result; - LOG_DBG("Response: {}", rsp.body); - parse_err = picojson::parse(v, rsp.body); + + // Reply: + // + // {"sessionID":"xyz..."} + // + picojson::value rsp_json; + std::string parse_err = picojson::parse(rsp_json, rsp.body); if (!parse_err.empty()) { error = FORMAT("JSON parse error: {}", parse_err); LOG_ERROR("{}", error); - return NetworkBackend::NETWORK_ERROR; + return NETWORK_ERROR; } - if (!v.is()) { - error = "Invalid SmartID response"; - LOG_WARN("Invalid SmartID response"); + if (!rsp_json.is()) { + error = "Invalid Authentication response"; + LOG_WARN("Invalid Authentication response"); return NetworkBackend::NETWORK_ERROR; } - w = v.get("sessionID"); + picojson::value w = rsp_json.get("sessionID"); if (!w.is()) { - error = "Invalid SmartID response"; - LOG_WARN("Invalid SmartID response"); + error = "Invalid Authentication response"; + LOG_WARN("Invalid Authentication response"); return NetworkBackend::NETWORK_ERROR; } - sessionID = w.get(); - LOG_DBG("SessionID: {}", sessionID); + std::string sessionId = w.get(); + LOG_DBG("SessionID: {}", sessionId); - sidrsp = {}; - result = waitForResult(sidrsp, cli, path + "/session/", sessionID, 60, true); + SIDResponse sidrsp; + result = waitForResult(sidrsp, cli, path + "/sid/session/", session_token_disclosed, session_cert, sessionId, 60); if (result != OK) return result; + LOG_DBG("Certificate: {}", sidrsp.cert); LOG_DBG("Signature: {}", sidrsp.signature); + sidrsp.signature_json.erase("value"); + SHA256((uint8_t *) inter_str.c_str(), inter_str.size(), b.data()); + std::string inter_hash_64 = toBase64(b.data(), b.size()); + + picojson::object sig_parms = { + {"interactionsDigest", picojson::value(inter_hash_64)}, + {"interactionTypeUsed", picojson::value(sidrsp.inter_type_used)}, + {"signature", picojson::value(sidrsp.signature_json)}, + }; + dst = fromBase64(sidrsp.signature); cert = fromBase64(sidrsp.cert); + params = picojson::value(sig_parms).serialize(); return OK; -#endif } libcdoc::result_t @@ -1212,7 +1195,7 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector LOG_DBG("SessionID: {}", sessionID); SIDResponse sidrsp; - result = waitForResult(sidrsp, cli, path + "/authentication/session/", sessionID, 60, false); + result = waitForResult(sidrsp, cli, path + "/authentication/session/", {}, {}, sessionID, 60); if (result != OK) return result; LOG_DBG("Certificate: {}", sidrsp.cert); diff --git a/cdoc/NetworkBackend.h b/cdoc/NetworkBackend.h index d832c5c4..b0101ef2 100644 --- a/cdoc/NetworkBackend.h +++ b/cdoc/NetworkBackend.h @@ -43,26 +43,32 @@ struct CDOC_EXPORT NetworkBackend { static constexpr int MIDSID_REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP = -354; // User has multiple accounts and pressed Cancel on device choice screen on any device static constexpr int MIDSID_USER_REFUSED_CERT_CHOICE = -355; + static constexpr int MIDSID_USER_REFUSED_INTERACTION = -356; + static constexpr int MIDSID_PROTOCOL_FAILURE = -357; + static constexpr int MIDSID_EXPECTED_LINKED_SESSION = -358; + static constexpr int MIDSID_SERVER_ERROR = -359; + static constexpr int ACCOUNT_UNUSABLE = -360; + // User pressed Cancel on PIN screen. Can be from the most common displayTextAndPIN flow or from verificationCodeChoice flow when user chosen the right code and then pressed cancel on PIN screen - static constexpr int MIDSID_USER_REFUSED_DISPLAYTEXTANDPIN = -356; + static constexpr int MIDSID_USER_REFUSED_DISPLAYTEXTANDPIN = -361; // User cancelled verificationCodeChoice screen - static constexpr int MIDSID_USER_REFUSED_VC_CHOICE = -357; + static constexpr int MIDSID_USER_REFUSED_VC_CHOICE = -362; // User cancelled on confirmationMessage screen - static constexpr int MIDSID_USER_REFUSED_CONFIRMATIONMESSAGE = -358; + static constexpr int MIDSID_USER_REFUSED_CONFIRMATIONMESSAGE = -363; // User cancelled on confirmationMessageAndVerificationCodeChoice screen - static constexpr int MIDSID_USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE = -359; + static constexpr int MIDSID_USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE = -364; // Given user has no active certificates and is not MID client. - static constexpr int MIDSID_NOT_MID_CLIENT = -360; + static constexpr int MIDSID_NOT_MID_CLIENT = -365; // User cancelled the operation - static constexpr int MIDSID_USER_CANCELLED = -361; + static constexpr int MIDSID_USER_CANCELLED = -366; // Mobile-ID configuration on user's SIM card differs from what is configured on service provider's side. User needs to contact his/her mobile operator. - static constexpr int MIDSID_SIGNATURE_HASH_MISMATCH = -362; + static constexpr int MIDSID_SIGNATURE_HASH_MISMATCH = -367; // Sim not available - static constexpr int MIDSID_PHONE_ABSENT = -363; + static constexpr int MIDSID_PHONE_ABSENT = -368; // SMS sending error - static constexpr int MIDSID_DELIVERY_ERROR = -364; + static constexpr int MIDSID_DELIVERY_ERROR = -369; // Invalid response from card - static constexpr int MIDSID_SIM_ERROR = -365; + static constexpr int MIDSID_SIM_ERROR = -370; #endif /** @@ -191,7 +197,7 @@ struct CDOC_EXPORT NetworkBackend { * @param share_id share id (transaction id) * @return error code or OK */ - virtual result_t fetchNonce(std::vector& dst, const std::string& url, const std::string& share_id, const std::string& auth_token, const std::string& auth_cert); + virtual result_t fetchNonce(std::vector& dst, const std::string& url, const std::string& share_id, const std::string& session_token, const std::string& session_cert); /** * @brief fetch key share from share server * @param share a container for result @@ -201,7 +207,7 @@ struct CDOC_EXPORT NetworkBackend { * @param cert a certificate of signing key (PEM without newlines) * @return error code or OK */ - virtual result_t fetchShare(ShareInfo& share, const std::string& url, const std::string& share_id, const std::string& ticket, const std::vector& cert); + virtual result_t fetchShare(ShareInfo& share, const std::string& url, const std::string& share_id, const std::string& session_token, const std::string& session_cert, const std::string& auth_token, const std::vector& auth_cert, const std::string& auth_params); #endif /** @@ -275,7 +281,7 @@ struct CDOC_EXPORT NetworkBackend { * @param algo algorithm type (SHA256, SHA385, SHA512) * @return error code or OK */ - result_t signSID(std::vector& dst, std::vector& cert, + result_t signSID(std::vector& dst, std::vector& cert, std::string& params, const std::string& url, const std::string& auth_token, const std::string& auth_cert, const std::string& rcpt_id, const std::vector& digest, CryptoBackend::HashAlgorithm algo); diff --git a/cdoc/Utils.cpp b/cdoc/Utils.cpp index 62f9b6cd..41156f32 100644 --- a/cdoc/Utils.cpp +++ b/cdoc/Utils.cpp @@ -38,6 +38,18 @@ toBase64(const uint8_t *data, size_t len) return result; } +std::string +toBase64URL(const std::string& data) +{ + return jwt::base::details::encode(data, jwt::alphabet::base64url::data(), ""); +} + +std::string +toBase64URL(const uint8_t *data, size_t len) +{ + return toBase64URL(std::string(reinterpret_cast(data), len)); +} + std::vector fromBase64(std::string_view data) { diff --git a/cdoc/Utils.h b/cdoc/Utils.h index c45026b3..5588f1d4 100644 --- a/cdoc/Utils.h +++ b/cdoc/Utils.h @@ -49,11 +49,16 @@ static auto encodeName(std::string_view path) } std::string toBase64(const uint8_t *data, size_t len); - static std::string toBase64(const std::vector &data) { return toBase64(data.data(), data.size()); } +std::string toBase64URL(const std::string& data); +std::string toBase64URL(const uint8_t *data, size_t len); +static std::string toBase64URL(const std::vector &data) { + return toBase64URL(data.data(), data.size()); +} + std::vector fromBase64(std::string_view data); std::vector fromBase64URL(std::string_view data); From 31fb917a434fd5e5c3c2c4c87675301b004a6132 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Tue, 4 Aug 2026 15:40:58 +0300 Subject: [PATCH 55/65] Preliminary configuration settings for MID --- cdoc/CDoc2Reader.cpp | 27 +++++++++++++++++---------- cdoc/Configuration.h | 16 ++++++++-------- cdoc/NetworkBackend.cpp | 17 ++++++++++++++--- cdoc/NetworkBackend.h | 15 ++++++++++++++- cdoc/ToolConf.h | 9 +++++++++ cdoc/cdoc-tool.cpp | 10 ++++++++++ 6 files changed, 72 insertions(+), 22 deletions(-) diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index 8cc00162..4c76060b 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -267,8 +267,15 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) } // Get authentication token + std::string auth_url = conf->getValue({}, Configuration::AUTH_SERVER); + if (auth_url.empty()) { + setLastError(FORMAT("No AUTH_SERVER found")); + LOG_ERROR("{}", last_error); + return libcdoc::CONFIGURATION_ERROR; + } + // auth_url = "https://cdoc2-auth.dev.riaint.ee"; SessionData session; - if (auto rv = network->authenticateForShares(session.token, session.cert); rv != OK) { + if (auto rv = network->authenticateForShares(auth_url, rcpt_id, session.token, session.cert); rv != OK) { setLastError(network->getLastErrorStr(rv)); LOG_ERROR("{}", last_error); return rv; @@ -287,6 +294,13 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) share.nonce = std::string(nonce.cbegin(), nonce.cend()); } + std::string rp_url = conf->getValue({}, Configuration::RP_SERVER); + if (rp_url.empty()) { + setLastError(FORMAT("No RP_SERVER found")); + LOG_ERROR("{}", last_error); + return libcdoc::CONFIGURATION_ERROR; + } + // rp_url = "https://cdoc2-rp.dev.riaint.ee/" /* Create tickets from shares */ std::vector auth_tokens; AuthenticationData auth; @@ -295,10 +309,7 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) std::string signer = "SMART_ID";// conf->getValue(Configuration::SHARE_SIGNER); LOG_DBG("Signer: {}", signer); if (signer == "SMART_ID") { - // "https://sid.demo.sk.ee/smart-id-rp/v2" - // std::string url = conf->getValue(Configuration::SID_DOMAIN, Configuration::BASE_URL); - std::string url = "https://cdoc2-rp.dev.riaint.ee/"; - SIDSigner signer(url, session, rcpt_id, network); + SIDSigner signer(rp_url, session, rcpt_id, network); result = signer.generateTickets(auth_tokens, shares); if (result != OK) { setLastError(signer.error); @@ -309,13 +320,9 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) } else if (signer == "MOBILE_ID") { // "https://sid.demo.sk.ee/smart-id-rp/v2" std::string url = conf->getValue(Configuration::MID_DOMAIN, Configuration::BASE_URL); - // "00000000-0000-0000-0000-000000000000" - std::string relyingPartyUUID = conf->getValue(Configuration::MID_DOMAIN, Configuration::RP_UUID); - // "DEMO" - std::string relyingPartyName = conf->getValue(Configuration::MID_DOMAIN, Configuration::RP_NAME); // "37200000566" std::string phone = conf->getValue(Configuration::MID_DOMAIN, Configuration::PHONE_NUMBER); - MIDSigner signer(url, relyingPartyUUID, relyingPartyName, phone, rcpt_id, network); + MIDSigner signer(url, {}, {}, phone, rcpt_id, network); result = signer.generateTickets(auth_tokens, shares); if (result != OK) { setLastError(signer.error); diff --git a/cdoc/Configuration.h b/cdoc/Configuration.h index 4ca72e60..e1c0451d 100644 --- a/cdoc/Configuration.h +++ b/cdoc/Configuration.h @@ -42,6 +42,14 @@ struct CDOC_EXPORT Configuration { * @brief Fetch URL of keyserver (Domain is server id) */ static constexpr char const *KEYSERVER_FETCH_URL = "KEYSERVER_FETCH_URL"; + /** + * @brief Authentication session server for SID/MID + */ + static constexpr char const *AUTH_SERVER = "AUTH_SERVER"; + /** + * @brief RP server for SID/MID + */ + static constexpr char const *RP_SERVER = "RP_SERVER"; #ifdef HAS_KEYSHARES /** * @brief JSON array of share server base urls (Domain is server id) @@ -63,14 +71,6 @@ struct CDOC_EXPORT Configuration { * @brief MID/SID base url (domain is SMART_ID or MOBILE_ID) */ static constexpr char const *BASE_URL = "BASE_URL"; - /** - * @brief MID/SID relying party UUID (domain is SMART_ID or MOBILE_ID) - */ - static constexpr char const *RP_UUID = "RP_UUID"; - /** - * @brief MID/SID relying party name (domain is SMART_ID or MOBILE_ID) - */ - static constexpr char const *RP_NAME = "RP_NAME"; /** * @brief Mobile ID phone number (domain is MOBILE_ID) */ diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 2bd5941f..a65eef47 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -577,15 +577,23 @@ waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& } libcdoc::result_t -libcdoc::NetworkBackend::authenticateForShares(std::string& token, std::string& cert) +libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std::string& rcpt_id, std::string& token, std::string& cert) { - static const std::string url = "https://cdoc2-auth.dev.riaint.ee"; // Start authentication std::string host, path; int port; int result = libcdoc::parseURL(url, host, port, path); if (result != libcdoc::OK) return result; + // The session is bound to the actual recipient identity from the lock. + // A hardcoded or malformed id would break the identity chain + // (session identity == signing identity == lock recipient). + if (!parseEtsiRecipientId(rcpt_id).valid()) { + error = FORMAT("Invalid recipient id: {}", rcpt_id); + LOG_WARN("{}", error); + return DATA_FORMAT_ERROR; + } + LOG_DBG("Starting client: {} {}", host, port); httplib::SSLClient cli(host, port); if (result = applySSLTimeout(cli, this); result != OK) return result; @@ -594,7 +602,7 @@ libcdoc::NetworkBackend::authenticateForShares(std::string& token, std::string& if (result = setProxy(cli, this); result != OK) return result; picojson::object obj = { - {"identifier", picojson::value("etsi/PNOEE-37104082710")}, + {"identifier", picojson::value(rcpt_id)}, }; picojson::value req_json(obj); std::string req_str = req_json.serialize(); @@ -802,6 +810,9 @@ libcdoc::result_t libcdoc::NetworkBackend::showFeedback(SIDMIDFeedback& feedback) { LOG_INFO("Verification code: {:04d} url: {}", feedback.code, feedback.url); + std::cout << "###########################" << "\n"; + std::cout << "# Verification code: " << feedback.code << " #" << "\n"; + std::cout << "###########################" << "\n"; return OK; } diff --git a/cdoc/NetworkBackend.h b/cdoc/NetworkBackend.h index b0101ef2..36c71bd1 100644 --- a/cdoc/NetworkBackend.h +++ b/cdoc/NetworkBackend.h @@ -188,7 +188,20 @@ struct CDOC_EXPORT NetworkBackend { virtual result_t fetchKey (std::vector& dst, const std::string& url, const std::string& transaction_id); #ifdef HAS_KEYSHARES - virtual result_t authenticateForShares(std::string& token, std::string& cert); + /** + * @brief Get a session token and certificate for share authentication + * + * Implementation may cache the session token and certificate if appropriate + * + * @param url The server URL + * @param rcpt_id The recipient id (etsi/PNOEE-...) the session is authenticated for. + * Must match the identity that will sign the share tickets and the lock's + * recipient id, so that session identity == signing identity == recipient. + * @param token Output parameter for session token + * @param cert Output parameter for session certificate + * @return Error code or OK + */ + virtual result_t authenticateForShares(const std::string& url, const std::string& rcpt_id, std::string& token, std::string& cert); /** * @brief fetch authentication nonce from share server diff --git a/cdoc/ToolConf.h b/cdoc/ToolConf.h index 73212acf..2db4c1a5 100644 --- a/cdoc/ToolConf.h +++ b/cdoc/ToolConf.h @@ -56,6 +56,8 @@ struct ToolConf : public JSONConfiguration { std::string library; std::vector servers; + std::string auth_server; + std::string rp_server; /** * @brief Files to be encrypted, or file to be decrypted. @@ -78,6 +80,13 @@ struct ToolConf : public JSONConfiguration { std::vector> accept_certs; std::string getValue(std::string_view domain, std::string_view param) const final { + if (domain.empty()) { + if (param == Configuration::AUTH_SERVER) { + return auth_server; + } else if (param == Configuration::RP_SERVER) { + return rp_server; + } + } for (auto& sdata : servers) { if (sdata.ID == domain) { if (param == Configuration::KEYSERVER_SEND_URL) { diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index 3e655b74..e937e8f8 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -84,6 +84,8 @@ print_usage(ostream& ofs) ofs << " --pin PIN - PKCS11 pin" << endl; ofs << " --key-id - PKCS11 key ID" << endl; ofs << " --key-label - PKCS11 key label" << endl; + ofs << " --rp-server RP_SERVER - RP server URL" << endl; + ofs << " --auth-server AUTH_SERVER - Authentication server URL" << endl; ofs << endl; ofs << "cdoc-tool locks FILE" << endl; ofs << endl; @@ -167,6 +169,14 @@ parse_common(ToolConf& conf, int arg_idx, int argc, char *argv[]) sdata.url = argv[arg_idx + 2]; conf.servers.push_back(sdata); return 3; + } else if (arg == "--auth-server") { + if ((arg_idx + 1) >= argc) return RESULT_USAGE; + conf.auth_server = argv[arg_idx + 1]; + return 2; + } else if (arg == "--rp-server") { + if ((arg_idx + 1) >= argc) return RESULT_USAGE; + conf.rp_server = argv[arg_idx + 1]; + return 2; } else if (arg == "--accept") { if ((arg_idx + 1) >= argc) return RESULT_USAGE; load_certs(conf, argv[arg_idx + 1]); From eaf6bff90ef106687320443c1672a0b34067b89f Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 5 Aug 2026 09:51:19 +0300 Subject: [PATCH 56/65] Check that we have session disclosures for all share servers before contacting them --- cdoc/KeyShares.cpp | 78 +++++++++++++++++++++++++++++------- cdoc/KeyShares.h | 11 ++++++ cdoc/NetworkBackend.cpp | 87 ++++++++++++++++++++--------------------- test/CMakeLists.txt | 4 ++ test/libcdoc_boost.cpp | 74 +++++++++++++++++++++++++++++++++++ 5 files changed, 195 insertions(+), 59 deletions(-) diff --git a/cdoc/KeyShares.cpp b/cdoc/KeyShares.cpp index b0250be2..90689d7f 100644 --- a/cdoc/KeyShares.cpp +++ b/cdoc/KeyShares.cpp @@ -32,6 +32,8 @@ #define CPPHTTPLIB_OPENSSL_SUPPORT #include "httplib.h" +#include +#include #include #include #include @@ -241,26 +243,38 @@ SessionToken::SessionToken(std::string_view str) } } +// Extract the target URL from a base64url-encoded SD-JWT disclosure. +// Returns an empty string if the disclosure is malformed. Never throws: +// fromBase64URL throws on malformed base64 until the S2 fix lands, and a +// malformed (server-issued) disclosure must not crash the process. +static std::string +disclosureTargetUrl(const std::string& disclosure) +{ + std::vector decoded_part; + try { + decoded_part = fromBase64URL(disclosure); + } catch (const std::exception &) { + return {}; + } + std::string json_str(decoded_part.begin(), decoded_part.end()); + picojson::value json; + if (!picojson::parse(json, json_str).empty()) + return {}; + if (!json.is()) + return {}; + picojson::array arr = json.get(); + if (arr.size() < 2 || !arr[1].is()) + return {}; + return arr[1].get(); +} + std::string SessionToken::discloseForUrl(std::string_view url) { LOG_DBG("Building token for: {}", url); for (auto& d : disclosures) { - std::vector decoded_part = fromBase64URL(d); - std::string json_str(decoded_part.begin(), decoded_part.end()); - picojson::value json; - if (!picojson::parse(json, json_str).empty()) { - return {}; - } - if (!json.is()) { - return {}; - } - picojson::array arr = json.get(); - if (arr.size() < 2) continue; - if (!arr[1].is()) { - return {}; - } - std::string target_url = arr[1].get(); + std::string target_url = disclosureTargetUrl(d); + if (target_url.empty()) continue; if (target_url.find(url) != std::string::npos) { std::string token = jwt + "~" + aud + "~" + d + "~"; LOG_DBG("Disclosed token: {}", token); @@ -270,6 +284,40 @@ SessionToken::discloseForUrl(std::string_view url) return {}; } +bool +SessionToken::hasDisclosureForUrl(std::string_view url) +{ + // Compare origins (scheme, host, port): the disclosure authorizes a + // share server, and the credential-theft risk (S1) is about the session + // token and user certificate being sent to a different host. Origin + // comparison is robust against trailing-slash and path variations. + // parseURL also rejects non-https URLs, so a disclosure or share URL + // with a plain-http scheme never matches. + std::string host, path; + int port = 0; + if (parseURL(std::string(url), host, port, path) != OK) + return false; + std::transform(host.begin(), host.end(), host.begin(), + [](unsigned char c) { return std::tolower(c); }); + for (const auto& d : disclosures) { + std::string target = disclosureTargetUrl(d); + if (target.empty()) + continue; + std::string dhost, dpath; + int dport = 0; + if (parseURL(target, dhost, dport, dpath) != OK) + continue; + std::transform(dhost.begin(), dhost.end(), dhost.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (host == dhost && port == dport) { + LOG_DBG("Server {} is authorized by a session disclosure", url); + return true; + } + } + LOG_WARN("No session disclosure authorizes server {}", url); + return false; +} + std::string decodeTicket(const std::string& ticket) { diff --git a/cdoc/KeyShares.h b/cdoc/KeyShares.h index 6f4711d7..c0b892b2 100644 --- a/cdoc/KeyShares.h +++ b/cdoc/KeyShares.h @@ -208,6 +208,17 @@ struct SessionToken { SessionToken(std::string_view str); std::string discloseForUrl(std::string_view url); + /** + * @brief Check whether the session token authorizes a share server + * + * Returns true if any disclosure in the session token refers to the same + * origin (scheme, host, port) as the given URL. The disclosures are issued + * by the authentication server, so they enumerate the share servers that + * are authorized for this session. Used to reject container-supplied share + * servers that the authentication server has not authorized - the session + * token and user credentials must never be sent to such servers. + */ + bool hasDisclosureForUrl(std::string_view url); }; std::string decodeTicket(const std::string& ticket); diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index a65eef47..62f79f06 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -128,7 +128,7 @@ static constexpr auto midsid_results = std::to_array({ {libcdoc::NetworkBackend::MIDSID_SIM_ERROR, "SIM_ERROR", "Invalid response from SIM card"} }); -static int +static libcdoc::result_t parseMIDSIDResult(std::string_view str) { if (str == "OK") return libcdoc::OK; @@ -497,6 +497,19 @@ struct AuthResponse { std::string cert; }; +static std::string +getJsonString(const picojson::value& json, const std::string& key) +{ + error = {}; + picojson::value v = json.get(key); + if (!v.is()) { + error = FORMAT("{} is not a string", key); + LOG_WARN("{}", error); + return {}; + } + return v.get(); +} + static result_t waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& path, const std::string& auth_proc_uuid, double seconds) { @@ -521,8 +534,10 @@ waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& LOG_WARN("{}", error); return NetworkBackend::NETWORK_ERROR; } - dst.status = v.get(); + dst.status = getJsonString(rsp, "status"); + if (!error.empty()) return NetworkBackend::NETWORK_ERROR; LOG_DBG("Status: {}", dst.status); + if (dst.status == "RUNNING") { // Pause for 0.5 seconds and repeat std::chrono::milliseconds duration(500); @@ -533,38 +548,23 @@ waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& LOG_WARN("{}", error); return NetworkBackend::NETWORK_ERROR; } + // State is complete, check for end result - v = rsp.get("endResult"); - if (!v.is()) { - error = "endResult is not a JSON object"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - dst.endResult = v.get(); + dst.endResult = getJsonString(rsp, "endResult"); + if (!error.empty()) return NetworkBackend::NETWORK_ERROR; LOG_DBG("EndResult: {}", dst.endResult); if (dst.endResult != "OK") { - LOG_WARN("EndResult is not OK: {}", dst.endResult); - return NetworkBackend::NETWORK_ERROR; + LOG_WARN("Authentication endResult is {}", dst.endResult); + return parseMIDSIDResult(dst.endResult); } - // Signature - v = rsp.get("sessionToken"); - if (!v.is()) { - error = "sessionToken is not a string"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - dst.sessionToken = v.get(); - LOG_DBG("Session token: {}", dst.sessionToken); - // Certificate - v = rsp.get("signingCertificate"); - if (!v.is()) { - error = "signingCertificate is not a string"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - dst.cert = v.get(); - LOG_DBG("Certificate: {}", dst.cert); + // Fetch session token and certificate + dst.sessionToken = getJsonString(rsp, "sessionToken"); + if (!error.empty()) return NetworkBackend::NETWORK_ERROR; + LOG_TRACE("Session token: {}", dst.sessionToken); + dst.cert = getJsonString(rsp, "signingCertificate"); + if (!error.empty()) return NetworkBackend::NETWORK_ERROR; + LOG_TRACE("Certificate: {}", dst.cert); error = {}; return OK; } @@ -642,20 +642,22 @@ libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std LOG_WARN("Invalid Authentication response"); return NetworkBackend::NETWORK_ERROR; } - picojson::value w = rsp_json.get("vc"); - if (!w.is()) { - error = "Invalid Authentication response"; - LOG_WARN("Invalid Authentication response"); - return NetworkBackend::NETWORK_ERROR; - } - std::string ver_code = w.get(); + // Verification code + std::string ver_code = getJsonString(rsp_json, "vc"); + if (!error.empty()) return NetworkBackend::NETWORK_ERROR; LOG_DBG("Verification code: {}", ver_code); + SIDMIDFeedback fb = { .code = (int) std::strtold(ver_code.c_str(), nullptr), }; result = showFeedback(fb); - if (result != OK) return result; + if (result != OK) { + error = FORMAT("Failed to show verification code: {}", result); + LOG_ERROR("{}", error); + return result; + } + // Fetch authentication response AuthResponse auth_rsp; result = waitForAuthResult(auth_rsp, cli, path + "/auth/status/", location, 60); if (result != OK) return result; @@ -663,7 +665,8 @@ libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std cert = auth_rsp.cert; auto parts = split(auth_rsp.sessionToken, '~'); - if (parts.size() < 3) { + // In minimum we need JWT, AUD, RP disclosure and 2 share disclosures + if (parts.size() < 5) { error = "Invalid JWT-SD token"; LOG_WARN("Invalid JWT-SD token"); return NetworkBackend::NETWORK_ERROR; @@ -681,9 +684,8 @@ libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std token = auth_rsp.sessionToken; - auto decoded = decodeTicket(parts[0]); - //auto st_json = decoded.get_header_json(); - LOG_DBG("Session token: {}", decoded); + auto decoded = decodeTicket(jwt); + LOG_TRACE("Session token: {}", decoded); picojson::value dec_json; auto p_err = picojson::parse(dec_json, decoded); if (!p_err.empty()) { @@ -696,9 +698,6 @@ libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std LOG_WARN("Invalid Authentication response"); return NetworkBackend::NETWORK_ERROR; } - for (auto a : dec_json.get()) { - LOG_DBG("Payload JSON {}: {}", a.first, a.second.to_str()); - } return OK; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 31f758d0..b7869044 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,9 +1,13 @@ add_executable(unittests libcdoc_boost.cpp ../cdoc/Crypto.cpp + ../cdoc/KeyShares.cpp ../cdoc/Tar.cpp + ../cdoc/Utils.cpp ) +target_compile_definitions(unittests PRIVATE HAS_KEYSHARES) + target_link_libraries(unittests OpenSSL::SSL cdoc diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 3ed35c89..29959fcd 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include "pipe.h" @@ -1283,3 +1284,76 @@ BOOST_AUTO_TEST_CASE(RejectsOversizedNationalId) } BOOST_AUTO_TEST_SUITE_END() + +// S1 regression: the session token's disclosures are the allowlist of share +// servers authorized by the authentication server. The reader must refuse to +// contact (and send credentials to) any container-supplied share server that +// has no disclosure. Matching is by origin (scheme, host, port). +BOOST_AUTO_TEST_SUITE(SessionTokenAuthorization) + +// ["salt","https://share1.example.com"] +static const char *DISC1 = "WyJzYWx0IiwiaHR0cHM6Ly9zaGFyZTEuZXhhbXBsZS5jb20iXQ"; +// ["salt","https://share2.example.com:8443/v1"] +static const char *DISC2 = "WyJzYWx0IiwiaHR0cHM6Ly9zaGFyZTIuZXhhbXBsZS5jb206ODQ0My92MSJd"; + +static libcdoc::SessionToken makeToken() +{ + std::string str = std::string("jwt~aud~") + DISC1 + "~" + DISC2; + return libcdoc::SessionToken(str); +} + +BOOST_AUTO_TEST_CASE(AuthorizedServers) +{ + auto st = makeToken(); + // Exact origin. + BOOST_CHECK(st.hasDisclosureForUrl("https://share1.example.com")); + // Trailing slash and sub-paths of the same origin. + BOOST_CHECK(st.hasDisclosureForUrl("https://share1.example.com/")); + BOOST_CHECK(st.hasDisclosureForUrl("https://share1.example.com/key-shares")); + // Host names are case-insensitive. + BOOST_CHECK(st.hasDisclosureForUrl("https://SHARE1.EXAMPLE.COM")); + // Explicit default port matches the implicit one. + BOOST_CHECK(st.hasDisclosureForUrl("https://share1.example.com:443")); + // Disclosure with a non-default port and a path. + BOOST_CHECK(st.hasDisclosureForUrl("https://share2.example.com:8443")); + BOOST_CHECK(st.hasDisclosureForUrl("https://share2.example.com:8443/other")); +} + +BOOST_AUTO_TEST_CASE(UnauthorizedServers) +{ + auto st = makeToken(); + // Unknown host. + BOOST_CHECK(!st.hasDisclosureForUrl("https://evil.com")); + // Domain-suffix confusion. + BOOST_CHECK(!st.hasDisclosureForUrl("https://share1.example.com.evil.com")); + // Subdomain is a different origin. + BOOST_CHECK(!st.hasDisclosureForUrl("https://sub.share1.example.com")); + // Wrong port. + BOOST_CHECK(!st.hasDisclosureForUrl("https://share2.example.com")); + BOOST_CHECK(!st.hasDisclosureForUrl("https://share2.example.com:8444")); + // Plain http is never authorized (parseURL enforces https). + BOOST_CHECK(!st.hasDisclosureForUrl("http://share1.example.com")); + // Not a URL at all. + BOOST_CHECK(!st.hasDisclosureForUrl("share1.example.com")); + BOOST_CHECK(!st.hasDisclosureForUrl("")); +} + +BOOST_AUTO_TEST_CASE(MalformedDisclosuresAreSkipped) +{ + // Bad base64url and non-JSON disclosures must not throw or match. + std::string str = std::string("jwt~aud~###~bm90LWpzb24~") + DISC1; + libcdoc::SessionToken st(str); + BOOST_CHECK(st.hasDisclosureForUrl("https://share1.example.com")); + BOOST_CHECK(!st.hasDisclosureForUrl("https://evil.com")); +} + +BOOST_AUTO_TEST_CASE(EmptyOrDisclosurelessTokenFailsClosed) +{ + libcdoc::SessionToken empty(""); + BOOST_CHECK(!empty.hasDisclosureForUrl("https://share1.example.com")); + // jwt~aud with no disclosures. + libcdoc::SessionToken twopart("jwt~aud"); + BOOST_CHECK(!twopart.hasDisclosureForUrl("https://share1.example.com")); +} + +BOOST_AUTO_TEST_SUITE_END() From a63652eb477de54c11d9202da906e6094dcb37c2 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 5 Aug 2026 09:52:44 +0300 Subject: [PATCH 57/65] Added CDoc2Reader.cpp changes --- cdoc/CDoc2Reader.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index 4c76060b..aed784fe 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -281,6 +281,23 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) return rv; } + // S1: only contact share servers that the authentication server has + // authorized for this session. The session token carries one + // disclosure per authorized server; a container pointing to any other + // server would otherwise receive the session token and the user's + // credentials (SSRF / credential exfiltration). N-of-N reconstruction + // needs every share, so an unauthorized server rejects the container. + { + SessionToken stoken(session.token); + for (const auto& share : shares) { + if (!stoken.hasDisclosureForUrl(share.base_url)) { + setLastError(FORMAT("Share server {} is not authorized by the authentication session", share.base_url)); + LOG_ERROR("{}", last_error); + return libcdoc::DATA_FORMAT_ERROR; + } + } + } + // Get nonces for (auto& share : shares) { std::vector nonce; From 9b6542369781c499ce3f0a16359acc1796fe8822 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 5 Aug 2026 13:32:19 +0300 Subject: [PATCH 58/65] More fixes to SID --- cdoc/CDoc2Writer.cpp | 7 ++- cdoc/CDocCipher.cpp | 6 ++ cdoc/KeyShares.cpp | 92 ++++++++++++++++-------------- cdoc/Utils.cpp | 15 ++++- test/libcdoc_boost.cpp | 124 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 197 insertions(+), 47 deletions(-) diff --git a/cdoc/CDoc2Writer.cpp b/cdoc/CDoc2Writer.cpp index 087c6636..894824e0 100644 --- a/cdoc/CDoc2Writer.cpp +++ b/cdoc/CDoc2Writer.cpp @@ -346,8 +346,11 @@ CDoc2Writer::buildHeader(std::vector& header, const std::vector urls = libcdoc::JsonToStringArray(url_list); - if (urls.size() < 1) - FAIL("No server URLs in " + rcpt.server_id, libcdoc::CONFIGURATION_ERROR); + // S5: with fewer than 2 servers the XOR "split" would hand the + // complete KEK to a single server, defeating the threshold + // protection - refuse to produce such a container. + if (urls.size() < 2) + FAIL("At least 2 share server URLs are required for ID " + rcpt.server_id, libcdoc::CONFIGURATION_ERROR); int N_SHARES = urls.size(); LOG_DBG("Number of shares: {}", N_SHARES); diff --git a/cdoc/CDocCipher.cpp b/cdoc/CDocCipher.cpp index b98b938e..9b44e0b8 100644 --- a/cdoc/CDocCipher.cpp +++ b/cdoc/CDocCipher.cpp @@ -394,6 +394,12 @@ fill_recipients_from_rcpt_info(ToolConf& conf, ToolCrypto& crypto, std::vector decoded_part; - try { - decoded_part = fromBase64URL(disclosure); - } catch (const std::exception &) { - return {}; - } + std::vector decoded_part = fromBase64URL(disclosure); std::string json_str(decoded_part.begin(), decoded_part.end()); picojson::value json; if (!picojson::parse(json, json_str).empty()) @@ -268,6 +263,31 @@ disclosureTargetUrl(const std::string& disclosure) return arr[1].get(); } +// Compare two URLs by origin (scheme, host, port). Used for SD-JWT +// disclosure binding (S7): a disclosure authorizes exactly one server, so +// substring matching is not acceptable - a disclosure for +// share.example.com.evil.ee must not match share.example.com, and a short +// query URL must not over-match many disclosures. Origin comparison is +// robust against trailing-slash and path variations (session-token +// disclosures carry the nonce on the path). parseURL enforces the https +// scheme on both sides, so plain-http never matches. Host comparison is +// case-insensitive. +static bool +urlsMatchByOrigin(std::string_view a, std::string_view b) +{ + std::string ahost, apath, bhost, bpath; + int aport = 0, bport = 0; + if (parseURL(std::string(a), ahost, aport, apath) != OK) + return false; + if (parseURL(std::string(b), bhost, bport, bpath) != OK) + return false; + std::transform(ahost.begin(), ahost.end(), ahost.begin(), + [](unsigned char c) { return std::tolower(c); }); + std::transform(bhost.begin(), bhost.end(), bhost.begin(), + [](unsigned char c) { return std::tolower(c); }); + return ahost == bhost && aport == bport; +} + std::string SessionToken::discloseForUrl(std::string_view url) { @@ -275,7 +295,7 @@ SessionToken::discloseForUrl(std::string_view url) for (auto& d : disclosures) { std::string target_url = disclosureTargetUrl(d); if (target_url.empty()) continue; - if (target_url.find(url) != std::string::npos) { + if (urlsMatchByOrigin(target_url, url)) { std::string token = jwt + "~" + aud + "~" + d + "~"; LOG_DBG("Disclosed token: {}", token); return token; @@ -287,29 +307,9 @@ SessionToken::discloseForUrl(std::string_view url) bool SessionToken::hasDisclosureForUrl(std::string_view url) { - // Compare origins (scheme, host, port): the disclosure authorizes a - // share server, and the credential-theft risk (S1) is about the session - // token and user certificate being sent to a different host. Origin - // comparison is robust against trailing-slash and path variations. - // parseURL also rejects non-https URLs, so a disclosure or share URL - // with a plain-http scheme never matches. - std::string host, path; - int port = 0; - if (parseURL(std::string(url), host, port, path) != OK) - return false; - std::transform(host.begin(), host.end(), host.begin(), - [](unsigned char c) { return std::tolower(c); }); for (const auto& d : disclosures) { std::string target = disclosureTargetUrl(d); - if (target.empty()) - continue; - std::string dhost, dpath; - int dport = 0; - if (parseURL(target, dhost, dport, dpath) != OK) - continue; - std::transform(dhost.begin(), dhost.end(), dhost.begin(), - [](unsigned char c) { return std::tolower(c); }); - if (host == dhost && port == dport) { + if (!target.empty() && urlsMatchByOrigin(target, url)) { LOG_DBG("Server {} is authorized by a session disclosure", url); return true; } @@ -321,18 +321,26 @@ SessionToken::hasDisclosureForUrl(std::string_view url) std::string decodeTicket(const std::string& ticket) { - auto decoded = jwt::decode(ticket); - auto a = decoded.get_header_json(); - for (auto t : a) { - LOG_DBG("Header {}: {}", t.first, t.second.to_str()); - } - a = decoded.get_payload_json(); - for (auto t : a) { - LOG_DBG("Payload {}: {}", t.first, t.second.to_str()); + // jwt::decode throws on malformed input; the ticket comes from a remote + // server, so a decode failure must not crash the process. An empty result + // makes the caller's JSON parse step report the format error. + try { + auto decoded = jwt::decode(ticket); + auto a = decoded.get_header_json(); + for (auto t : a) { + LOG_DBG("Header {}: {}", t.first, t.second.to_str()); + } + a = decoded.get_payload_json(); + for (auto t : a) { + LOG_DBG("Payload {}: {}", t.first, t.second.to_str()); + } + auto b = decoded.get_signature(); + LOG_DBG("Signature: {}", b); + return picojson::value(decoded.get_payload_json()).serialize(); + } catch (const std::exception &e) { + LOG_WARN("decodeTicket: invalid JWT: {}", e.what()); + return {}; } - auto b = decoded.get_signature(); - LOG_DBG("Signature: {}", b); - return picojson::value(decoded.get_payload_json()).serialize(); } } // namespace libcdoc diff --git a/cdoc/Utils.cpp b/cdoc/Utils.cpp index 28891e85..036aa727 100644 --- a/cdoc/Utils.cpp +++ b/cdoc/Utils.cpp @@ -71,9 +71,18 @@ fromBase64(std::string_view data) std::vector fromBase64URL(std::string_view data) { - auto padded = jwt::base::pad(std::string(data)); - auto str = jwt::base::decode(padded); - return std::vector(str.cbegin(), str.cend()); + // Same contract as fromBase64: the input is untrusted (server-issued + // tokens and disclosures) and jwt::base::decode throws std::runtime_error + // on malformed input, so failures are signalled with an empty result + // instead of an exception escaping into the caller. + try { + auto padded = jwt::base::pad(std::string(data)); + auto str = jwt::base::decode(padded); + return std::vector(str.cbegin(), str.cend()); + } catch (const std::exception &e) { + LOG_WARN("fromBase64URL: invalid base64url input: {}", e.what()); + return {}; + } } double diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index f2363774..c4c9040f 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -29,7 +29,11 @@ #include #include #include +#include #include +#include +#include +#include #include @@ -860,6 +864,39 @@ BOOST_AUTO_TEST_CASE(InvalidInputReturnsEmpty) BOOST_CHECK(libcdoc::fromBase64("QQ==QQ==").empty()); } +// S2 regression: same non-throwing contract for fromBase64URL (session +// token parts, SD-JWT disclosures - all server-controlled). +BOOST_AUTO_TEST_CASE(UrlValidInput) +{ + // "hello world" in unpadded base64url (fromBase64URL pads it). + std::vector expected {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}; + BOOST_CHECK(libcdoc::fromBase64URL("aGVsbG8gd29ybGQ") == expected); + BOOST_CHECK(libcdoc::fromBase64URL("").empty()); +} + +BOOST_AUTO_TEST_CASE(UrlInvalidInputReturnsEmpty) +{ + // Characters outside the base64url alphabet. + BOOST_CHECK(libcdoc::fromBase64URL("###").empty()); + BOOST_CHECK(libcdoc::fromBase64URL("aGVsbG8+//").empty()); + // Impossible length. + BOOST_CHECK(libcdoc::fromBase64URL("A").empty()); + // Excess/embedded padding. + BOOST_CHECK(libcdoc::fromBase64URL("QQ===").empty()); +} + +// S2 regression: decodeTicket parses server-issued JWTs; malformed input +// must yield an empty string, not an exception. +BOOST_AUTO_TEST_CASE(DecodeTicketInvalidReturnsEmpty) +{ + BOOST_CHECK(libcdoc::decodeTicket("").empty()); + BOOST_CHECK(libcdoc::decodeTicket("not-a-jwt").empty()); + // Three parts but payload is not valid base64url JSON. + BOOST_CHECK(libcdoc::decodeTicket("AAA.###.BBB").empty()); + // Valid base64url parts but the payload is not JSON. + BOOST_CHECK(libcdoc::decodeTicket("dHlw.bm90LWpzb24.c2ln").empty()); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(TarPaxHeader) @@ -1454,6 +1491,30 @@ BOOST_AUTO_TEST_CASE(UnauthorizedServers) BOOST_CHECK(!st.hasDisclosureForUrl("")); } +// S7 regression: discloseForUrl binds a disclosure to its server by origin +// (scheme, host, port) - not by substring. A disclosure for +// share1.example.com must not be disclosed to share1.example.com.evil.com. +BOOST_AUTO_TEST_CASE(DiscloseForUrlBindsByOrigin) +{ + auto st = makeToken(); + // Exact origin -> the matching disclosure is appended. + BOOST_CHECK_EQUAL(st.discloseForUrl("https://share1.example.com"), + std::string("jwt~aud~") + DISC1 + "~"); + // Sub-path of the same origin still matches (nonces live on the path). + BOOST_CHECK_EQUAL(st.discloseForUrl("https://share1.example.com/key-shares"), + std::string("jwt~aud~") + DISC1 + "~"); + // Non-default port matches only with the same port. + BOOST_CHECK_EQUAL(st.discloseForUrl("https://share2.example.com:8443"), + std::string("jwt~aud~") + DISC2 + "~"); + // Domain-suffix confusion -> no disclosure. + BOOST_CHECK(st.discloseForUrl("https://share1.example.com.evil.com").empty()); + // Unknown host / subdomain / wrong port / plain http -> no disclosure. + BOOST_CHECK(st.discloseForUrl("https://evil.com").empty()); + BOOST_CHECK(st.discloseForUrl("https://sub.share1.example.com").empty()); + BOOST_CHECK(st.discloseForUrl("https://share2.example.com").empty()); + BOOST_CHECK(st.discloseForUrl("http://share1.example.com").empty()); +} + BOOST_AUTO_TEST_CASE(MalformedDisclosuresAreSkipped) { // Bad base64url and non-JSON disclosures must not throw or match. @@ -1574,3 +1635,66 @@ BOOST_AUTO_TEST_CASE(NoSeparatorReturnsSynthetic) } BOOST_AUTO_TEST_SUITE_END() + +// S5 regression: a keyshare recipient with fewer than 2 share servers would +// hand the complete KEK to a single server (the XOR split degenerates). +// CDoc2Writer must refuse with CONFIGURATION_ERROR. +BOOST_AUTO_TEST_SUITE(KeyShareWriter) + +namespace { + +struct ShareServerConf : public libcdoc::Configuration { + std::string urls; + explicit ShareServerConf(std::string u) : urls(std::move(u)) {} + std::string getValue(std::string_view domain, std::string_view param) const override { + if (param == libcdoc::Configuration::SHARE_SERVER_URLS) + return urls; + return {}; + } +}; + +// Avoids real network connections for the two-server control case. +struct StubNetworkBackend : public libcdoc::NetworkBackend { + libcdoc::result_t sendShare(std::vector&, const std::string&, const std::string&, const std::vector&) override { + return libcdoc::NOT_IMPLEMENTED; + } +}; + +libcdoc::result_t encryptWithShareServers(libcdoc::Configuration& conf, libcdoc::NetworkBackend& network) +{ + std::vector out; + libcdoc::VectorConsumer consumer(out); + libcdoc::CryptoBackend crypto; + std::unique_ptr writer(libcdoc::CDocWriter::createWriter(2, &consumer, false, &conf, &crypto, &network)); + libcdoc::Recipient rcpt = libcdoc::Recipient::makeShare("label", "server1", "PNOEE-30303039914"); + if (auto rv = writer->addRecipient(rcpt); rv != libcdoc::OK) + return rv; + return writer->beginEncryption(); +} + +} // namespace + +BOOST_AUTO_TEST_CASE(MissingServerListIsConfigurationError) +{ + ShareServerConf conf({}); + StubNetworkBackend network; + BOOST_CHECK_EQUAL(encryptWithShareServers(conf, network), libcdoc::CONFIGURATION_ERROR); +} + +BOOST_AUTO_TEST_CASE(SingleServerIsConfigurationError) +{ + ShareServerConf conf(R"(["https://share1.example.com"])"); + StubNetworkBackend network; + BOOST_CHECK_EQUAL(encryptWithShareServers(conf, network), libcdoc::CONFIGURATION_ERROR); +} + +BOOST_AUTO_TEST_CASE(TwoServersPassTheCountCheck) +{ + ShareServerConf conf(R"(["https://share1.example.com", "https://share2.example.com"])"); + StubNetworkBackend network; + // Gets past the URL-count check and fails later in the (stubbed) share + // upload - i.e. NOT with CONFIGURATION_ERROR. + BOOST_CHECK_EQUAL(encryptWithShareServers(conf, network), libcdoc::NOT_IMPLEMENTED); +} + +BOOST_AUTO_TEST_SUITE_END() From adc94f46d481032e7cd90dad3dca9a438d12db81 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Thu, 6 Aug 2026 20:40:00 +0300 Subject: [PATCH 59/65] Validate auth and rp server signatures --- cdoc/CDoc2Reader.cpp | 23 ++ cdoc/Crypto.cpp | 37 +++ cdoc/Crypto.h | 23 ++ cdoc/CryptoBackend.cpp | 27 ++ cdoc/CryptoBackend.h | 16 + cdoc/KeyShares.cpp | 134 ++++++++ cdoc/KeyShares.h | 57 ++++ cdoc/NetworkBackend.cpp | 218 ++++++------ cdoc/Utils.cpp | 13 +- cdoc/json/base.h | 717 +++++++++++++++++++++++++++++++++++++++- test/libcdoc_boost.cpp | 209 +++++++++++- 11 files changed, 1346 insertions(+), 128 deletions(-) diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index 361b2a89..0ad4d961 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -293,6 +293,17 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) } } + // S8: validate the authentication session client-side - the session + // certificate must belong to the lock recipient and the session token + // must not be expired. Also learns the schemeName/rpName claims needed + // to verify the signed ticket later. + std::string scheme_name, rp_name, v_err; + if (auto rv = validateSessionData(crypto, rcpt_id, session.token, session.cert, scheme_name, rp_name, v_err); rv != OK) { + setLastError(v_err); + LOG_ERROR("{}", last_error); + return rv; + } + // Get nonces for (auto& share : shares) { std::vector nonce; @@ -350,6 +361,18 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) LOG_ERROR("Cannot generate share tickets"); return result; } + // S8: verify the signed auth ticket client-side before spending it - + // the signing certificate must belong to rcpt_id and the ACSP_V2 + // signature must verify (binds identity, the consent text shown to + // the user, and freshness). All tickets share the same signed JWT, + // so validating the first one covers them all. + if (!auth_tokens.empty()) { + if (auto rv = validateAuthTicket(crypto, rcpt_id, auth_tokens[0], auth.cert, auth.params, scheme_name, rp_name, v_err); rv != OK) { + setLastError(v_err); + LOG_ERROR("{}", last_error); + return rv; + } + } std::vector& kek_build = kek.getTarget(32); std::fill(kek_build.begin(), kek_build.end(), 0); for (unsigned int i = 0; i < auth_tokens.size(); i++) { diff --git a/cdoc/Crypto.cpp b/cdoc/Crypto.cpp index 984843cd..53014e1b 100644 --- a/cdoc/Crypto.cpp +++ b/cdoc/Crypto.cpp @@ -59,6 +59,43 @@ const std::string Crypto::RSA_MTH = "http://www.w3.org/2001/04/xmlenc#rsa-1_5"; const std::string Crypto::CONCATKDF_MTH = "http://www.w3.org/2009/xmlenc11#ConcatKDF"; const std::string Crypto::AGREEMENT_MTH = "http://www.w3.org/2009/xmlenc11#ECDH-ES"; +bool +Crypto::validateSignature(const std::vector &cert_der, + const std::vector &data, + const std::vector &signature, + SignatureAlgorithm algo) +{ + const unsigned char *ptr = cert_der.data(); + auto x509 = make_unique_ptr(d2i_X509(nullptr, &ptr, long(cert_der.size()))); + if (!x509) + return false; + auto pkey = make_unique_ptr(X509_get_pubkey(x509.get())); + if (!pkey) + return false; + auto ctx = make_unique_ptr(EVP_PKEY_CTX_new(pkey.get(), nullptr)); + if (!ctx) + return false; + switch (algo) { + case SignatureAlgorithm::RSASSA_PSS_SHA256: { + // The provider's one-shot EVP_PKEY_verify for RSA requires the + // input to be the message digest already, so hash `data` first. + uint8_t md_value[EVP_MAX_MD_SIZE]; + unsigned int md_len = 0; + if (EVP_Digest(data.data(), data.size(), md_value, &md_len, EVP_sha256(), nullptr) != 1) + return false; + if (EVP_PKEY_verify_init(ctx.get()) != 1) + return false; + if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_PSS_PADDING) <= 0 || + EVP_PKEY_CTX_set_signature_md(ctx.get(), EVP_sha256()) <= 0 || + EVP_PKEY_CTX_set_rsa_mgf1_md(ctx.get(), EVP_sha256()) <= 0 || + EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx.get(), RSA_PSS_SALTLEN_DIGEST) <= 0) + return false; + return EVP_PKEY_verify(ctx.get(), signature.data(), signature.size(), md_value, md_len) == 1; + } + } + return false; +} + std::vector Crypto::AESWrap(const std::vector &key, const std::vector &data, bool encrypt) { // Note: AES_set_{encrypt,decrypt}_key return 0 on success and a negative diff --git a/cdoc/Crypto.h b/cdoc/Crypto.h index 58a7e346..4a2c771e 100644 --- a/cdoc/Crypto.h +++ b/cdoc/Crypto.h @@ -192,6 +192,29 @@ class Crypto const std::vector& synth_seed, size_t expected_len); + /** + * @brief Signature algorithms supported by validateSignature + */ + enum class SignatureAlgorithm { + RSASSA_PSS_SHA256, /**< RSASSA-PSS, SHA-256, MGF1/SHA-256, salt length = digest length */ + }; + + /** + * @brief Validate a signature over a message with a certificate's public key + * + * Used by the Smart-ID (ACSP_V2) client-side ticket validation (S8). + * + * @param cert_der X.509 certificate in DER encoding + * @param data the signed message (hashed internally as the algorithm requires) + * @param signature the signature value + * @param algo signature algorithm and parameters + * @return true if the signature verifies + */ + static bool validateSignature(const std::vector &cert_der, + const std::vector &data, + const std::vector &signature, + SignatureAlgorithm algo); + static bool isError(int retval, const char* funcName, const char* file, int line) { if (retval < 1) { diff --git a/cdoc/CryptoBackend.cpp b/cdoc/CryptoBackend.cpp index 17694cfe..a2cc5826 100644 --- a/cdoc/CryptoBackend.cpp +++ b/cdoc/CryptoBackend.cpp @@ -18,6 +18,7 @@ #include "Crypto.h" #include "CryptoBackend.h" +#include "Certificate.h" #include "Utils.h" #define OPENSSL_SUPPRESS_DEPRECATED @@ -111,6 +112,32 @@ CryptoBackend::getKeyMaterial(std::vector& key_material, const std::vec return OK; } +libcdoc::result_t +CryptoBackend::validateCertificate(const std::string& user_id, const std::vector& cert_der) +{ + // Identity part of etsi/PNOEE-... (or used as-is if there is no prefix) + std::string id = user_id.starts_with("etsi/") ? user_id.substr(5) : user_id; + if (id.empty()) { + LOG_WARN("validateCertificate: empty user id"); + return INVALID_PARAMS; + } + Certificate cert(cert_der); + if (!cert) { + LOG_WARN("validateCertificate: cannot parse certificate"); + return CRYPTO_ERROR; + } + std::string serial = cert.getName(NID_serialNumber); + if (serial.empty()) { + LOG_WARN("validateCertificate: certificate subject has no serialNumber"); + return CRYPTO_ERROR; + } + if (serial != id) { + LOG_WARN("validateCertificate: certificate identity '{}' does not match '{}'", serial, id); + return CRYPTO_ERROR; + } + return OK; +} + libcdoc::result_t CryptoBackend::extractHKDF(std::vector& kek_pm, const std::vector& salt, const std::vector& pw_salt, int32_t kdf_iter, unsigned int idx) diff --git a/cdoc/CryptoBackend.h b/cdoc/CryptoBackend.h index d3ce3c62..77c6f5ce 100644 --- a/cdoc/CryptoBackend.h +++ b/cdoc/CryptoBackend.h @@ -172,6 +172,22 @@ struct CDOC_EXPORT CryptoBackend { return NOT_IMPLEMENTED; } + /** + * @brief Validate that a certificate belongs to the given user (S8) + * + * The default implementation checks only that the certificate subject + * serialNumber matches the identity part of user_id (etsi/PNOEE-...). + * It deliberately does NOT check expiry, revocation status or chain + * trust: users must be able to decrypt their documents even after the + * signing certificate has expired. Implementations may override this to + * enforce expiry dates, OCSP lookups, trust lists etc. + * + * @param user_id recipient id (etsi/PNOEE-...) + * @param cert_der certificate in DER encoding + * @return error code or OK + */ + virtual result_t validateCertificate(const std::string& user_id, const std::vector& cert_der); + virtual int test(libcdoc::Lock& lock) { return NOT_IMPLEMENTED; } }; diff --git a/cdoc/KeyShares.cpp b/cdoc/KeyShares.cpp index 78581954..8a4591c6 100644 --- a/cdoc/KeyShares.cpp +++ b/cdoc/KeyShares.cpp @@ -343,6 +343,140 @@ decodeTicket(const std::string& ticket) } } + +std::string +libcdoc::buildAcspV2Payload(const std::string& scheme_name, const std::string& server_random, + const std::string& rp_challenge, const std::string& user_challenge, + const std::string& rp_name, const std::string& interactions_digest, + const std::string& interaction_type_used, const std::string& flow_type) +{ + // schemeName|ACSP_V2|serverRandom|rpChallenge|userChallenge|base64(rpName)|| + // interactionsDigest|interactionTypeUsed||flowType + // (brokeredRpNameBase64 and initialCallbackUrl are always empty here) + std::string rp_name64 = toBase64((const uint8_t *) rp_name.data(), rp_name.size()); + return scheme_name + "|ACSP_V2|" + server_random + "|" + rp_challenge + "|" + user_challenge + + "|" + rp_name64 + "||" + interactions_digest + "|" + interaction_type_used + "||" + flow_type; +} + +libcdoc::result_t +libcdoc::validateSessionData(CryptoBackend *crypto, const std::string& rcpt_id, + const std::string& session_token, const std::string& session_cert_b64, + std::string& scheme_name, std::string& rp_name, std::string& error) +{ + if (!crypto) { + error = "No crypto backend"; + return CryptoBackend::INVALID_PARAMS; + } + // The session certificate belongs to the person the session authenticated; + // it must match the container recipient (base64url per the auth server spec). + std::vector cert_der = fromBase64URL(session_cert_b64); + if (cert_der.empty()) { + error = "Invalid session certificate"; + return DATA_FORMAT_ERROR; + } + if (auto rv = crypto->validateCertificate(rcpt_id, cert_der); rv != OK) { + error = FORMAT("Session certificate does not match recipient {}", rcpt_id); + return rv; + } + // Session token claims: expiry (fail fast; servers are authoritative) and + // the schemeName/rpName needed to reconstruct the ACSP_V2 payload. + SessionToken stoken(session_token); + std::string payload = decodeTicket(stoken.jwt); + picojson::value json; + if (!picojson::parse(json, payload).empty() || !json.is()) { + error = "Invalid session token"; + return DATA_FORMAT_ERROR; + } + if (json.get("exp").is() && json.get("exp").get() < libcdoc::getTime()) { + error = "Session token is expired"; + return NetworkBackend::NETWORK_ERROR; + } + scheme_name = json.get("schemeName").is() ? json.get("schemeName").get() : std::string(); + rp_name = json.get("rpName").is() ? json.get("rpName").get() : std::string(); + if (scheme_name.empty() || rp_name.empty()) { + error = "Session token misses schemeName/rpName claims"; + return DATA_FORMAT_ERROR; + } + return OK; +} + +libcdoc::result_t +libcdoc::validateAuthTicket(CryptoBackend *crypto, const std::string& rcpt_id, + const std::string& ticket, const std::vector& cert_der, + const std::string& signature_params_json, + const std::string& scheme_name, const std::string& rp_name, + std::string& error) +{ + if (!crypto) { + error = "No crypto backend"; + return CryptoBackend::INVALID_PARAMS; + } + // Signing certificate identity must match the container recipient. + if (auto rv = crypto->validateCertificate(rcpt_id, cert_der); rv != OK) { + error = FORMAT("Signing certificate does not match recipient {}", rcpt_id); + return rv; + } + + // The signed part of the ticket JWT is header64.payload64.sig64 + auto parts = split(ticket, '~'); + if (parts.empty()) { + error = "Invalid ticket"; + return DATA_FORMAT_ERROR; + } + auto jwt_parts = split(parts[0], '.'); + if (jwt_parts.size() != 3) { + error = "Invalid ticket JWT"; + return DATA_FORMAT_ERROR; + } + std::string signing_input = jwt_parts[0] + "." + jwt_parts[1]; + std::vector signature = fromBase64URL(jwt_parts[2]); + if (signature.empty()) { + error = "Invalid ticket signature"; + return DATA_FORMAT_ERROR; + } + + // The rpChallenge sent to the RP server is base64(SHA256(signing input)) + std::vector digest(32); + SHA256(reinterpret_cast(signing_input.data()), signing_input.size(), digest.data()); + std::string rp_challenge = toBase64(digest); + + // ACSP_V2 parameters returned by the RP server + picojson::value json; + if (!picojson::parse(json, signature_params_json).empty() || !json.is()) { + error = "Invalid signature parameters"; + return DATA_FORMAT_ERROR; + } + auto getStr = [](const picojson::value& obj, const char *key) -> std::string { + picojson::value v = obj.get(key); + return v.is() ? v.get() : std::string(); + }; + picojson::value sig = json.get("signature"); + if (!sig.is()) { + error = "Missing ACSP_V2 signature parameters"; + return DATA_FORMAT_ERROR; + } + std::string server_random = getStr(sig, "serverRandom"); + std::string user_challenge = getStr(sig, "userChallenge"); + std::string flow_type = getStr(sig, "flowType"); + std::string interactions_digest = getStr(json, "interactionsDigest"); + std::string interaction_type = getStr(json, "interactionTypeUsed"); + if (server_random.empty() || user_challenge.empty() || flow_type.empty() + || interactions_digest.empty() || interaction_type.empty()) { + error = "Missing ACSP_V2 signature parameters"; + return DATA_FORMAT_ERROR; + } + + std::string payload = buildAcspV2Payload(scheme_name, server_random, rp_challenge, user_challenge, + rp_name, interactions_digest, interaction_type, flow_type); + if (!Crypto::validateSignature(cert_der, {payload.cbegin(), payload.cend()}, signature, + Crypto::SignatureAlgorithm::RSASSA_PSS_SHA256)) { + error = "Auth ticket signature verification failed"; + return CRYPTO_ERROR; + } + return OK; +} + } // namespace libcdoc + diff --git a/cdoc/KeyShares.h b/cdoc/KeyShares.h index c0b892b2..f61b4092 100644 --- a/cdoc/KeyShares.h +++ b/cdoc/KeyShares.h @@ -223,6 +223,63 @@ struct SessionToken { std::string decodeTicket(const std::string& ticket); +/** + * @brief Build the ACSP_V2 signed payload (Smart-ID RP v3) + * + * The payload is the |-joined string: + * schemeName|ACSP_V2|serverRandom|rpChallenge|userChallenge|base64(rpName)|| + * interactionsDigest|interactionTypeUsed||flowType + * (construction verified against the SK reference verifier). + */ +std::string buildAcspV2Payload(const std::string& scheme_name, const std::string& server_random, + const std::string& rp_challenge, const std::string& user_challenge, + const std::string& rp_name, const std::string& interactions_digest, + const std::string& interaction_type_used, const std::string& flow_type); + +/** + * @brief Validate the authentication session client-side (S8) + * + * Checks that the session signing certificate belongs to rcpt_id (via + * CryptoBackend::validateCertificate), that the session token is not expired, + * and extracts the schemeName/rpName claims needed for ticket validation. + * + * @param crypto crypto backend + * @param rcpt_id recipient id from the lock (etsi/PNOEE-...) + * @param session_token the SD-JWT session token from the auth server + * @param session_cert_b64 session signing certificate (base64url DER) + * @param scheme_name output: session token schemeName claim + * @param rp_name output: session token rpName claim + * @param error output: error description on failure + * @return error code or OK + */ +result_t validateSessionData(CryptoBackend *crypto, const std::string& rcpt_id, + const std::string& session_token, const std::string& session_cert_b64, + std::string& scheme_name, std::string& rp_name, std::string& error); + +/** + * @brief Validate a signed SID/MID auth ticket client-side (S8) + * + * Checks that the signing certificate belongs to rcpt_id and that the + * ACSP_V2 signature verifies. This binds the signer's identity, the consent + * text shown to the user (interactionsDigest) and the freshness + * (serverRandom) of the signature before it is presented to share servers. + * + * @param crypto crypto backend + * @param rcpt_id recipient id from the lock (etsi/PNOEE-...) + * @param ticket the auth ticket (jwt~disclosures...) + * @param cert_der signing certificate in DER encoding + * @param signature_params_json the x-cdoc2-sid-rpv3-signature-parameters JSON + * @param scheme_name schemeName (from the session token claims) + * @param rp_name rpName (from the session token claims) + * @param error output: error description on failure + * @return error code or OK + */ +result_t validateAuthTicket(CryptoBackend *crypto, const std::string& rcpt_id, + const std::string& ticket, const std::vector& cert_der, + const std::string& signature_params_json, + const std::string& scheme_name, const std::string& rp_name, + std::string& error); + } // namespace libcdoc #endif // KEYSHARES_H diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 16a17e31..7e37980c 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -183,6 +183,52 @@ static_assert(hashAlgorithmToSidMidName(static_cast()) { + error = FORMAT("{} is missing (the response is not a JSON object)", key); + LOG_WARN("{}", error); + result = libcdoc::DATA_FORMAT_ERROR; + return {}; + } + picojson::value v = json.get(key); + if (!v.is()) { + error = FORMAT("{} is missing or is not a string", key); + LOG_WARN("{}", error); + result = libcdoc::DATA_FORMAT_ERROR; + return {}; + } + result = libcdoc::OK; + return v.get(); +} + +static picojson::object +getJsonObject(const picojson::value& json, const std::string& key, libcdoc::result_t& result) +{ + error = {}; + // picojson::value::get(key) throws std::runtime_error if json is not an + // object - check first, the input comes from a remote server. + if (!json.is()) { + error = FORMAT("{} is missing (the response is not a JSON object)", key); + LOG_WARN("{}", error); + result = libcdoc::DATA_FORMAT_ERROR; + return {}; + } + picojson::value v = json.get(key); + if (!v.is()) { + error = FORMAT("{} is missing or is not an object", key); + LOG_WARN("{}", error); + result = libcdoc::DATA_FORMAT_ERROR; + return {}; + } + result = libcdoc::OK; + return v.get(); +} + std::string libcdoc::NetworkBackend::getLastErrorStr(result_t code) const { @@ -405,7 +451,7 @@ libcdoc::NetworkBackend::fetchKey (std::vector& dst, const std::string& { std::string host, path; int port; - int result = libcdoc::parseURL(url, host, port, path); + result_t result = libcdoc::parseURL(url, host, port, path); if (result != libcdoc::OK) return result; std::vector cert; @@ -426,13 +472,8 @@ libcdoc::NetworkBackend::fetchKey (std::vector& dst, const std::string& result = get(cli, hdrs, full, rsp_json); if (result != libcdoc::OK) return result; - picojson::value v = rsp_json.get("ephemeral_key_material"); - if (!v.is()) { - error = FORMAT("No 'ephemeral_key_material' in response"); - return NETWORK_ERROR; - } - error = {}; - std::string ks = v.get(); + std::string ks = getJsonString(rsp_json, "ephemeral_key_material", result); + if (result != libcdoc::OK) return NETWORK_ERROR; dst = fromBase64(ks); if (dst.empty()) { error = FORMAT("Invalid base64 in 'ephemeral_key_material'"); @@ -501,19 +542,6 @@ struct AuthResponse { std::string cert; }; -static std::string -getJsonString(const picojson::value& json, const std::string& key) -{ - error = {}; - picojson::value v = json.get(key); - if (!v.is()) { - error = FORMAT("{} is not a string", key); - LOG_WARN("{}", error); - return {}; - } - return v.get(); -} - static result_t waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& path, const std::string& auth_proc_uuid, double seconds) { @@ -531,15 +559,9 @@ waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& LOG_WARN("{}", error); return NetworkBackend::NETWORK_ERROR; } - // State - picojson::value v = rsp.get("status"); - if (!v.is()) { - error = "Status is not a string"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - dst.status = getJsonString(rsp, "status"); - if (!error.empty()) return NetworkBackend::NETWORK_ERROR; + // Status + dst.status = getJsonString(rsp, "status", result); + if (result != OK) return NetworkBackend::NETWORK_ERROR; LOG_DBG("Status: {}", dst.status); if (dst.status == "RUNNING") { @@ -554,8 +576,8 @@ waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& } // State is complete, check for end result - dst.endResult = getJsonString(rsp, "endResult"); - if (!error.empty()) return NetworkBackend::NETWORK_ERROR; + dst.endResult = getJsonString(rsp, "endResult", result); + if (result != OK) return NetworkBackend::NETWORK_ERROR; LOG_DBG("EndResult: {}", dst.endResult); if (dst.endResult != "OK") { LOG_WARN("Authentication endResult is {}", dst.endResult); @@ -563,11 +585,11 @@ waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& } // Fetch session token and certificate - dst.sessionToken = getJsonString(rsp, "sessionToken"); - if (!error.empty()) return NetworkBackend::NETWORK_ERROR; + dst.sessionToken = getJsonString(rsp, "sessionToken", result); + if (result != OK) return NetworkBackend::NETWORK_ERROR; LOG_TRACE("Session token: {}", dst.sessionToken); - dst.cert = getJsonString(rsp, "signingCertificate"); - if (!error.empty()) return NetworkBackend::NETWORK_ERROR; + dst.cert = getJsonString(rsp, "signingCertificate", result); + if (result != OK) return NetworkBackend::NETWORK_ERROR; LOG_TRACE("Certificate: {}", dst.cert); error = {}; return OK; @@ -586,7 +608,7 @@ libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std // Start authentication std::string host, path; int port; - int result = libcdoc::parseURL(url, host, port, path); + result_t result = parseURL(url, host, port, path); if (result != libcdoc::OK) return result; // The session is bound to the actual recipient identity from the lock. @@ -647,8 +669,8 @@ libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std return NetworkBackend::NETWORK_ERROR; } // Verification code - std::string ver_code = getJsonString(rsp_json, "vc"); - if (!error.empty()) return NetworkBackend::NETWORK_ERROR; + std::string ver_code = getJsonString(rsp_json, "vc", result); + if (result != libcdoc::OK) return NETWORK_ERROR; LOG_DBG("Verification code: {}", ver_code); SIDMIDFeedback fb = { @@ -743,12 +765,9 @@ libcdoc::NetworkBackend::fetchNonce(std::vector& dst, const std::string LOG_ERROR("{}", error); return NETWORK_ERROR; } - picojson::value v = rsp_json.get("nonce"); - if (!v.is()) { - error = FORMAT("No 'nonce' in response"); - return NETWORK_ERROR; - } - std::string nonce_str = v.get(); + libcdoc::result_t rv = libcdoc::OK; + std::string nonce_str = getJsonString(rsp_json, "nonce", rv); + if (rv != libcdoc::OK) return rv; dst = toUint8Vector(nonce_str); return OK; } @@ -786,19 +805,12 @@ libcdoc::NetworkBackend::fetchShare(ShareInfo& share, const std::string& url, co result = get(cli, hdrs, full, rsp_json); if (result != libcdoc::OK) return result; - picojson::value v = rsp_json.get("share"); - if (!v.is()) { - error = FORMAT("No 'share' in response"); - return NETWORK_ERROR; - } - std::string share64 = v.get(); + libcdoc::result_t rv = libcdoc::OK; + std::string share64 = getJsonString(rsp_json, "share", rv); + if (rv != libcdoc::OK) return rv; LOG_DBG("Share64: {}", share64); - v = rsp_json.get("recipient"); - if (!v.is()) { - error = FORMAT("No 'recipient' in response"); - return NETWORK_ERROR; - } - std::string recipient = v.get(); + std::string recipient = getJsonString(rsp_json, "recipient", rv); + if (rv != libcdoc::OK) return rv; std::vector shareval = fromBase64(share64); if (shareval.size() != 32) { error = FORMAT("Invalid share size: expected 32, got {}", shareval.size()); @@ -854,13 +866,8 @@ waitForResult(SIDResponse& dst, httplib::SSLClient& cli, const std::string& path return NetworkBackend::NETWORK_ERROR; } // State - picojson::value v = rsp.get("state"); - if (!v.is()) { - error = "State is not a string"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - std::string str = v.get(); + std::string str = getJsonString(rsp, "state", result); + if (result != OK) return result; if (str == "RUNNING") { // Pause for 0.5 seconds and repeat std::chrono::milliseconds duration(500); @@ -873,19 +880,10 @@ waitForResult(SIDResponse& dst, httplib::SSLClient& cli, const std::string& path } // State is complete, check for end result - v = rsp.get("result"); - if (!v.is()) { - error = "Result is not an object"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - picojson::value w = v.get("endResult"); - if (!w.is()) { - error = "Result is not a string"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - str = w.get(); + picojson::object result_obj = getJsonObject(rsp, "result", result); + if (result != OK) return result; + str = getJsonString(picojson::value(result_obj), "endResult", result); + if (result != OK) return result; result = parseMIDSIDResult(str); if (result == UNSPECIFIED_ERROR) { // Unknown result @@ -900,41 +898,21 @@ waitForResult(SIDResponse& dst, httplib::SSLClient& cli, const std::string& path // details // signatureProtocol - // Signature - v = rsp.get("signature"); - if (v.is()) { - w = v.get("value"); - if (!w.is()) { - error = "Value is not a string"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - dst.signature = w.get(); - dst.signature_json = v.get(); + // Signature (optional field; rsp is a verified object here) + if (picojson::value sig = rsp.get("signature"); sig.is()) { + dst.signature = getJsonString(sig, "value", result); + if (result != OK) return result; + dst.signature_json = sig.get(); } // Interaction type - v = rsp.get("interactionTypeUsed"); - if (!v.is()) { - error = "InteractionTypeUsed is not a string"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - dst.inter_type_used = v.get(); + dst.inter_type_used = getJsonString(rsp, "interactionTypeUsed", result); + if (result != OK) return result; // Certificate - v = rsp.get("cert"); - if (!v.is()) { - error = "Certificate is not a JSON object"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - w = v.get("value"); - if (!w.is()) { - error = "Certificate value is not a string"; - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } - dst.cert = w.get(); + picojson::object cert_obj = getJsonObject(rsp, "cert", result); + if (result != OK) return result; + dst.cert = getJsonString(picojson::value(cert_obj), "value", result); + if (result != OK) return result; error = {}; return OK; @@ -994,7 +972,7 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector picojson::value(inter) }; //std::string inter_str = picojson::value(inter_arr).serialize(); - std::string inter_str = "[{\"type\":\"confirmationMessageAndVerificationCodeChoice\",\"displayText200\":\"Do you want to decrypt the document\"}]"; + std::string inter_str = "[{\"type\":\"confirmationnMessageAndVerificationCodeChoice\",\"displayText200\":\"Do you want to decrypt the document\"}]"; LOG_DBG("Interactions: {}", inter_str); inter_str = toBase64((const uint8_t *) inter_str.data(), inter_str.size()); std::string inter_str_64 = toBase64((const uint8_t *) inter_str.data(), inter_str.size()); @@ -1003,7 +981,7 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector {"certificateLevel", picojson::value(certificateLevel)}, {"signatureProtocol", picojson::value("ACSP_V2")}, {"signatureProtocolParameters", picojson::value(spp)}, - {"interactions", picojson::value(inter_str_64)}, + {"interactions", picojson::value(inter_str)}, {"vcType", picojson::value("numeric4")} }; picojson::value query(obj); @@ -1061,13 +1039,9 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector LOG_WARN("Invalid Authentication response"); return NetworkBackend::NETWORK_ERROR; } - picojson::value w = rsp_json.get("sessionID"); - if (!w.is()) { - error = "Invalid Authentication response"; - LOG_WARN("Invalid Authentication response"); - return NetworkBackend::NETWORK_ERROR; - } - std::string sessionId = w.get(); + libcdoc::result_t rv = libcdoc::OK; + std::string sessionId = getJsonString(rsp_json, "sessionID", rv); + if (rv != libcdoc::OK) return rv; LOG_DBG("SessionID: {}", sessionId); SIDResponse sidrsp; @@ -1199,13 +1173,9 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector LOG_WARN("Invalid Mobile ID response"); return NetworkBackend::NETWORK_ERROR; } - picojson::value w = v.get("sessionID"); - if (!w.is()) { - error = "Invalid Mobile ID response"; - LOG_WARN("Invalid Mobile ID response"); - return NetworkBackend::NETWORK_ERROR; - } - std::string sessionID = w.get(); + libcdoc::result_t rv = libcdoc::OK; + std::string sessionID = getJsonString(v, "sessionID", rv); + if (rv != libcdoc::OK) return rv; LOG_DBG("SessionID: {}", sessionID); SIDResponse sidrsp; diff --git a/cdoc/Utils.cpp b/cdoc/Utils.cpp index 036aa727..bebd69ee 100644 --- a/cdoc/Utils.cpp +++ b/cdoc/Utils.cpp @@ -68,6 +68,16 @@ fromBase64(std::string_view data) } } +static std::string +strip(std::string input) +{ + // Remove trailing '=' padding characters (used in Base64URL encoding) + while (!input.empty() && input.back() == '=') { + input.pop_back(); + } + return input; +} + std::vector fromBase64URL(std::string_view data) { @@ -76,7 +86,8 @@ fromBase64URL(std::string_view data) // on malformed input, so failures are signalled with an empty result // instead of an exception escaping into the caller. try { - auto padded = jwt::base::pad(std::string(data)); + auto stripped = strip(std::string(data)); + auto padded = jwt::base::pad(stripped); auto str = jwt::base::decode(padded); return std::vector(str.cbegin(), str.cend()); } catch (const std::exception &e) { diff --git a/cdoc/json/base.h b/cdoc/json/base.h index 3682abac..6904dc57 100644 --- a/cdoc/json/base.h +++ b/cdoc/json/base.h @@ -139,7 +139,722 @@ namespace jwt { inline uint32_t index(const std::array& rdata, char symbol) { auto index = rdata[static_cast(symbol)]; - if (index <= -1) { throw std::runtime_error("Invalid input: not within alphabet"); } + if (index <= -1) { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + throw std::runtime_error("Invalid input: not within alphabet"); } return static_cast(index); } } // namespace alphabet diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index c4c9040f..2989b791 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -879,10 +879,15 @@ BOOST_AUTO_TEST_CASE(UrlInvalidInputReturnsEmpty) // Characters outside the base64url alphabet. BOOST_CHECK(libcdoc::fromBase64URL("###").empty()); BOOST_CHECK(libcdoc::fromBase64URL("aGVsbG8+//").empty()); + // Padding character in the middle hits the alphabet check. + BOOST_CHECK(libcdoc::fromBase64URL("QQ==QQ").empty()); // Impossible length. BOOST_CHECK(libcdoc::fromBase64URL("A").empty()); - // Excess/embedded padding. - BOOST_CHECK(libcdoc::fromBase64URL("QQ===").empty()); + // Trailing padding is tolerated (RFC 4648 allows '=' in base64url): + // fromBase64URL strips it before decoding, so "QQ===" == "QQ" == {0x41}. + std::vector expected {0x41}; + BOOST_CHECK(libcdoc::fromBase64URL("QQ===") == expected); + BOOST_CHECK(libcdoc::fromBase64URL("QQ") == expected); } // S2 regression: decodeTicket parses server-issued JWTs; malformed input @@ -1698,3 +1703,203 @@ BOOST_AUTO_TEST_CASE(TwoServersPassTheCountCheck) } BOOST_AUTO_TEST_SUITE_END() + +// S8 regression: client-side validation of Smart-ID (ACSP_V2) auth tickets. +// All vectors come from a cdoc-tool Smart-ID session log (2026-08-06) using a +// Smart-ID TEST identity (serialNumber PNOEE-30303039903) - no real PII. +// The signature was independently verified with OpenSSL. +BOOST_AUTO_TEST_SUITE(SidTicketValidation) + +namespace { + +static const char *SID_CERT_B64 = + "MIIGqDCCBi6gAwIBAgIQfCl8dqrXBKOVGG0OTfMqTzAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0" + "aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkG" + "A1UEBhMCRUUwHhcNMjYwNTE4MTI0NTEzWhcNMjkwNTE3MTI0NTEyWjBXMQswCQYDVQQGEwJFRTEQMA4GA1UEAwwHVEVTVCxPSzEN" + "MAsGA1UEBAwEVEVTVDELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTMwMzAzMDM5OTAzMIIDIjANBgkqhkiG9w0BAQEFAAOC" + "Aw8AMIIDCgKCAwEAsbP7GwkiyLnVk4Xneq76DuDklgie/LurancUp5Mw13Pn7Sp/XTnie1PtWHIgZFsvKKHRWwHFB4H/XQisgZS7" + "yfRYVe2u3cfSuoH/W5oRpnAnojaltBQZRE6LM5WRhqI6+sdcoGM938AWEkr/gThU3DPSGglZ0mNEOR7SvVHtaKz4KAc1XQZtyHmo" + "iZ/eqNW7Nlj1s3A66jmEBTq4aiqlx0JXhfgmNV+1yw81vEwB0LHQLadp3Ca2G60bDMQItzWpe8pzd2gUv6smxjKq3MnVVsgYEAFw" + "kbeuDR3OLUbWbnSTAn9Y5DfDW30xRg4if1I+ruDWLicv5vJXsHCgjgUqLlk9/v+gIFuieJhczyZh9+FOSmCOREqrWOyGUNzCFruV" + "yg+Z6o8NRZkz9cNqFCUU7O3FnpIHC/1Vz08hJJZLzaF3Ao9qg7WkdNEz28wCuFeVSq/yp1gEEpvnMdYM6FUUUM+y+/b7N61pgk1M" + "P7ljTREJ0bHY5O8y0/YynP4NI36nKyNbGhsgtqhquHfLWCaCm/kLdgymQUNI1VCl1XcmYtCZkFFB9Ru7EtmTfuk2Lc7mdQYEHutH" + "qWIUywSJvm1P878PYXRtqNY2hu6MuyarN5uQIO887R1ho+IeN2BUGsEArUeN8RCuqr2J5DZj4GloReZ7GYFWFBXTNKaDRu+deLOj" + "/41Hztg4ITjOSUnh6/Z2kkRPkH7rhNT4+irtRfiXG2MMsV+kEVO4p/j+l7lofbL0NbkUlskd4Od6iox3YCXIicxY+5JSj63QLU1r" + "Gv6EbFHkPAseZ+MYys2J99KGToAtz+XOMyKr3VJp7vc4RWBhNcRygm/Oj60DgXQS/ph2y1ZMfl7NL3m2jAJQzADTqBahOuTuJj57" + "BObdI7xV8bwOI8sFSFG3xVfKpoPkvi4C+G+rxErims2CC5rezJBwwVJtLCQ2CA1e/+4kv1DBja8Z1jzBfFXypXQfT1fXN+jL54+0" + "85JNiBeCfXUnUPUW+gjl+ea+17m9Y0b5XTu4QyF5AgMBAAGjggH1MIIB8TAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4" + "zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5k" + "ZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQD" + "DBhQTk9FRS0zMDMwMzAzOTkwMy1ERU0xLVEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93" + "d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBAjAo" + "BgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5MDMwMzAzMTIwMDAwWjAWBgNVHSUEDzANBgsrBgEEAYPmYgUHADA0BgNVHR8ELTAr" + "MCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIwMjRlLmNybDAdBgNVHQ4EFgQUFxWovRQENDMS4BItkheOSBR1dPcw" + "DgYDVR0PAQH/BAQDAgeAMAoGCCqGSM49BAMDA2gAMGUCMQDcI/ZV6SPo13ZPwsjhLMS9n6ZN1czKd02I/eKj67RBOOD1HWkW0DJ6" + "QxDoUoeaTcACMFdNOAY2BotlUO6uZWlWdUFjoqVZOGEgZVHGJkIxPZ04+SrO4jMOukWuZQqJYM4WZQ=="; + +static const char *SID_CERT_B64URL = + "MIIGqDCCBi6gAwIBAgIQfCl8dqrXBKOVGG0OTfMqTzAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0" + "aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkG" + "A1UEBhMCRUUwHhcNMjYwNTE4MTI0NTEzWhcNMjkwNTE3MTI0NTEyWjBXMQswCQYDVQQGEwJFRTEQMA4GA1UEAwwHVEVTVCxPSzEN" + "MAsGA1UEBAwEVEVTVDELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTMwMzAzMDM5OTAzMIIDIjANBgkqhkiG9w0BAQEFAAOC" + "Aw8AMIIDCgKCAwEAsbP7GwkiyLnVk4Xneq76DuDklgie_LurancUp5Mw13Pn7Sp_XTnie1PtWHIgZFsvKKHRWwHFB4H_XQisgZS7" + "yfRYVe2u3cfSuoH_W5oRpnAnojaltBQZRE6LM5WRhqI6-sdcoGM938AWEkr_gThU3DPSGglZ0mNEOR7SvVHtaKz4KAc1XQZtyHmo" + "iZ_eqNW7Nlj1s3A66jmEBTq4aiqlx0JXhfgmNV-1yw81vEwB0LHQLadp3Ca2G60bDMQItzWpe8pzd2gUv6smxjKq3MnVVsgYEAFw" + "kbeuDR3OLUbWbnSTAn9Y5DfDW30xRg4if1I-ruDWLicv5vJXsHCgjgUqLlk9_v-gIFuieJhczyZh9-FOSmCOREqrWOyGUNzCFruV" + "yg-Z6o8NRZkz9cNqFCUU7O3FnpIHC_1Vz08hJJZLzaF3Ao9qg7WkdNEz28wCuFeVSq_yp1gEEpvnMdYM6FUUUM-y-_b7N61pgk1M" + "P7ljTREJ0bHY5O8y0_YynP4NI36nKyNbGhsgtqhquHfLWCaCm_kLdgymQUNI1VCl1XcmYtCZkFFB9Ru7EtmTfuk2Lc7mdQYEHutH" + "qWIUywSJvm1P878PYXRtqNY2hu6MuyarN5uQIO887R1ho-IeN2BUGsEArUeN8RCuqr2J5DZj4GloReZ7GYFWFBXTNKaDRu-deLOj" + "_41Hztg4ITjOSUnh6_Z2kkRPkH7rhNT4-irtRfiXG2MMsV-kEVO4p_j-l7lofbL0NbkUlskd4Od6iox3YCXIicxY-5JSj63QLU1r" + "Gv6EbFHkPAseZ-MYys2J99KGToAtz-XOMyKr3VJp7vc4RWBhNcRygm_Oj60DgXQS_ph2y1ZMfl7NL3m2jAJQzADTqBahOuTuJj57" + "BObdI7xV8bwOI8sFSFG3xVfKpoPkvi4C-G-rxErims2CC5rezJBwwVJtLCQ2CA1e_-4kv1DBja8Z1jzBfFXypXQfT1fXN-jL54-0" + "85JNiBeCfXUnUPUW-gjl-ea-17m9Y0b5XTu4QyF5AgMBAAGjggH1MIIB8TAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4" + "zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5k" + "ZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQD" + "DBhQTk9FRS0zMDMwMzAzOTkwMy1ERU0xLVEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93" + "d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBAjAo" + "BgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5MDMwMzAzMTIwMDAwWjAWBgNVHSUEDzANBgsrBgEEAYPmYgUHADA0BgNVHR8ELTAr" + "MCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIwMjRlLmNybDAdBgNVHQ4EFgQUFxWovRQENDMS4BItkheOSBR1dPcw" + "DgYDVR0PAQH_BAQDAgeAMAoGCCqGSM49BAMDA2gAMGUCMQDcI_ZV6SPo13ZPwsjhLMS9n6ZN1czKd02I_eKj67RBOOD1HWkW0DJ6" + "QxDoUoeaTcACMFdNOAY2BotlUO6uZWlWdUFjoqVZOGEgZVHGJkIxPZ04-SrO4jMOukWuZQqJYM4WZQ"; + +static const char *SID_SIG_B64 = + "XN6OijUhZvTQDMME7I2OLzYu84lNhWl9FjKG8sfHBNvpVhsmaz2LR/WzTrJJ+QyVpz9A+o0i+Dl4Zf0v1MR79cjAHIhpsbrQVhC3" + "vlWmoE1s3tKoqWNLxyr2ub46J8H3Aac8x/62RiELsxhBO0JrEA8Vf4N0gXTqoSXxFBK/vbH7ANxbCP+Nx/DsnK1dUPLUQO+44Srs" + "qTv6ZVCO5QFV0cnQIS7wITbx41qCukKwL4nglNV52dGfzoLQh9LP+OlSbFkj3+gYWyoVKniBXXPb4G5wBVVSnjsfWaT5RyELNpV5" + "9A6Ucp7Kj9MUVi/pZY3iDl79AZM71QMfx9VMiR/nBVcwxINDsqW56WQiVzKqLqeys6eI6J4udqSctdNybYUuYhQIq7qYc1Up8sLQ" + "RZcpVHvD13648aMgCheyf7TnamA2fmtOFj/0Lnu3TPMX2Nyg+TWpRLYVlIsYO1fyQTkquST0QlONn5ayhL9nPzbEOwuEea6kWEuM" + "aakt0jLOSvs2dwwFIvypmisv/ywjJhC7pbinpE8M3RK7u9AS915SKNnBAgJXURKCO2HQ91fnNuz0KlpUODTKKpU1DN8pjskRodos" + "SBdRjsUuzsegw1QJAO9o/OL7qV1p1mDf48GlrPwz9VNVxqjh0Y2OjcWkdzQBgnPZwzBNErWVPgm0YOx75D3Wpp4dkgWeZkEItlxW" + "im8DNyBsOwOahsn/EMrqm3MYpv1SPMAFZH0aIAdvIbzEfapacqIHgFX4U4HrXSHzpLGsjs8dNlIrJtHZoN8d5HfxCS1wHZnv8whP" + "h5+Us4jAbAGz+Z/mUBg45AQPB8cTSnqgOlpOHqDri7/XoK0S8wYwlNjwrVXSuZjj++vAIudh5Q4QBrxKuT7ASMeZwjVhw384kWhK" + "RDck7Y0wTHOBSG+pVY7VCiWNsbd8Kc1Jy8DGtvtUiIdCJv/KZNjjckvoBVKN/NDthwZMxj8iIfVtsf+GWx+D6CvytpxwXFR+RFtz" + "5efHmlxhyK2fnE3DLJ6J1NKK"; + +static const char *SID_TICKET_JWT = + "eyJhbGciOiJSU0FTU0EtUFNTK0FDU1BfVjIiLCJ0eXAiOiJ2bmQuY2RvYzIuYXV0aC10b2tlbi52MStzZC1qd3QifQ.eyJfc2QiO" + "lsiX1NvQmRrZlVoeHJSbGpFRHhuazVrbkdkOWs4QVlKdUxya2s2NkZkVVI4byJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6I" + "mV0c2lcL1BOT0VFLTMwMzAzMDM5OTAzIn0.XN6OijUhZvTQDMME7I2OLzYu84lNhWl9FjKG8sfHBNvpVhsmaz2LR_WzTrJJ-QyVp" + "z9A-o0i-Dl4Zf0v1MR79cjAHIhpsbrQVhC3vlWmoE1s3tKoqWNLxyr2ub46J8H3Aac8x_62RiELsxhBO0JrEA8Vf4N0gXTqoSXxF" + "BK_vbH7ANxbCP-Nx_DsnK1dUPLUQO-44SrsqTv6ZVCO5QFV0cnQIS7wITbx41qCukKwL4nglNV52dGfzoLQh9LP-OlSbFkj3-gYW" + "yoVKniBXXPb4G5wBVVSnjsfWaT5RyELNpV59A6Ucp7Kj9MUVi_pZY3iDl79AZM71QMfx9VMiR_nBVcwxINDsqW56WQiVzKqLqeys" + "6eI6J4udqSctdNybYUuYhQIq7qYc1Up8sLQRZcpVHvD13648aMgCheyf7TnamA2fmtOFj_0Lnu3TPMX2Nyg-TWpRLYVlIsYO1fyQ" + "TkquST0QlONn5ayhL9nPzbEOwuEea6kWEuMaakt0jLOSvs2dwwFIvypmisv_ywjJhC7pbinpE8M3RK7u9AS915SKNnBAgJXURKCO" + "2HQ91fnNuz0KlpUODTKKpU1DN8pjskRodosSBdRjsUuzsegw1QJAO9o_OL7qV1p1mDf48GlrPwz9VNVxqjh0Y2OjcWkdzQBgnPZw" + "zBNErWVPgm0YOx75D3Wpp4dkgWeZkEItlxWim8DNyBsOwOahsn_EMrqm3MYpv1SPMAFZH0aIAdvIbzEfapacqIHgFX4U4HrXSHzp" + "LGsjs8dNlIrJtHZoN8d5HfxCS1wHZnv8whPh5-Us4jAbAGz-Z_mUBg45AQPB8cTSnqgOlpOHqDri7_XoK0S8wYwlNjwrVXSuZjj-" + "-vAIudh5Q4QBrxKuT7ASMeZwjVhw384kWhKRDck7Y0wTHOBSG-pVY7VCiWNsbd8Kc1Jy8DGtvtUiIdCJv_KZNjjckvoBVKN_NDth" + "wZMxj8iIfVtsf-GWx-D6CvytpxwXFR-RFtz5efHmlxhyK2fnE3DLJ6J1NKK"; + +static const char *SID_PARAMS_JSON = R"({"interactionTypeUsed":"confirmationMessageAndVerificationCodeChoice","interactionsDigest":"l3Fawq7fsklfb+ZkDsZcJICehrtVrMmhidQ4Ha+gTM0=","signature":{"flowType":"Notification","serverRandom":"tjB5sLBWR8OVEJDOgUPRKhk4","signatureAlgorithm":"rsassa-pss","signatureAlgorithmParameters":{"hashAlgorithm":"SHA-256","maskGenAlgorithm":{"algorithm":"id-mgf1","parameters":{"hashAlgorithm":"SHA-256"}},"saltLength":32,"trailerField":"0xbc"},"userChallenge":"_eegCn9XBOQSqQRUQPRflc7r1CDJcz0k4jBL1AqpPmk"}})"; + +std::vector sidCert() { return libcdoc::fromBase64(SID_CERT_B64); } +std::vector sidSig() { return libcdoc::fromBase64(SID_SIG_B64); } + +std::vector sidPayload() +{ + std::string p = libcdoc::buildAcspV2Payload("smart-id-demo", "tjB5sLBWR8OVEJDOgUPRKhk4", + "p6jwyzPizfS8+DozeW4fytXf0OVDp4hCHqvJWIlKLfg=", "_eegCn9XBOQSqQRUQPRflc7r1CDJcz0k4jBL1AqpPmk", + "DEMO", "l3Fawq7fsklfb+ZkDsZcJICehrtVrMmhidQ4Ha+gTM0=", + "confirmationMessageAndVerificationCodeChoice", "Notification"); + return {p.begin(), p.end()}; +} + +std::string sidTicket() +{ + // The disclosures are irrelevant for validation; any suffix works. + return std::string(SID_TICKET_JWT) + "~aud~ZGlzY2xvc3VyZQ"; +} + +std::string makeSessionToken(const std::string& payload_json) +{ + std::string h = libcdoc::toBase64URL(R"({"typ":"vnd.cdoc2.session-token.v2+sd-jwt","alg":"ES256"})"); + std::string p = libcdoc::toBase64URL(payload_json); + // jwt~aud~disclosure (SessionToken needs >= 3 parts) + return h + "." + p + ".c2ln~aud~ZGlzYw"; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(ValidateSignatureReferenceVector) +{ + const auto algo = libcdoc::Crypto::SignatureAlgorithm::RSASSA_PSS_SHA256; + // The ACSP_V2 signature from the log verifies (verified independently with OpenSSL). + BOOST_CHECK(libcdoc::Crypto::validateSignature(sidCert(), sidPayload(), sidSig(), algo)); + // Tampered payload must not verify. + auto bad = sidPayload(); + bad[10] ^= 0x01; + BOOST_CHECK(!libcdoc::Crypto::validateSignature(sidCert(), bad, sidSig(), algo)); + // Garbage certificate must not verify (and must not crash). + BOOST_CHECK(!libcdoc::Crypto::validateSignature({1, 2, 3}, sidPayload(), sidSig(), algo)); +} + +BOOST_AUTO_TEST_CASE(ValidateCertificateIdentity) +{ + libcdoc::CryptoBackend crypto; + // The test certificate's subject serialNumber is PNOEE-30303039903. + BOOST_CHECK_EQUAL(crypto.validateCertificate("etsi/PNOEE-30303039903", sidCert()), libcdoc::OK); + // Also accepted without the etsi/ prefix. + BOOST_CHECK_EQUAL(crypto.validateCertificate("PNOEE-30303039903", sidCert()), libcdoc::OK); + // Different person (another Smart-ID test number). + BOOST_CHECK_EQUAL(crypto.validateCertificate("etsi/PNOEE-30303039914", sidCert()), libcdoc::CRYPTO_ERROR); + // Garbage DER. + BOOST_CHECK_EQUAL(crypto.validateCertificate("etsi/PNOEE-30303039903", {1, 2, 3}), libcdoc::CRYPTO_ERROR); + // Empty id. + BOOST_CHECK_EQUAL(crypto.validateCertificate("etsi/", sidCert()), libcdoc::CryptoBackend::INVALID_PARAMS); +} + +BOOST_AUTO_TEST_CASE(ValidateAuthTicketReferenceVector) +{ + libcdoc::CryptoBackend crypto; + std::string err; + // Full ticket from the log validates. + BOOST_CHECK_EQUAL(libcdoc::validateAuthTicket(&crypto, "etsi/PNOEE-30303039903", sidTicket(), + sidCert(), SID_PARAMS_JSON, "smart-id-demo", "DEMO", err), + libcdoc::OK); + // Wrong recipient: identity mismatch. + BOOST_CHECK_EQUAL(libcdoc::validateAuthTicket(&crypto, "etsi/PNOEE-30303039914", sidTicket(), + sidCert(), SID_PARAMS_JSON, "smart-id-demo", "DEMO", err), + libcdoc::CRYPTO_ERROR); + // Wrong rpName: ACSP_V2 payload mismatch -> signature failure. + BOOST_CHECK_EQUAL(libcdoc::validateAuthTicket(&crypto, "etsi/PNOEE-30303039903", sidTicket(), + sidCert(), SID_PARAMS_JSON, "smart-id-demo", "EVIL", err), + libcdoc::CRYPTO_ERROR); + // Tampered serverRandom: signature failure. + std::string badParams(SID_PARAMS_JSON); + badParams.replace(badParams.find("tjB5sLBWR8OVEJDOgUPRKhk4"), 24, "AAAAAAAAAAAAAAAAAAAAAA"); + BOOST_CHECK_EQUAL(libcdoc::validateAuthTicket(&crypto, "etsi/PNOEE-30303039903", sidTicket(), + sidCert(), badParams, "smart-id-demo", "DEMO", err), + libcdoc::CRYPTO_ERROR); + // Missing params entirely. + BOOST_CHECK_EQUAL(libcdoc::validateAuthTicket(&crypto, "etsi/PNOEE-30303039903", sidTicket(), + sidCert(), "{}", "smart-id-demo", "DEMO", err), + libcdoc::DATA_FORMAT_ERROR); +} + +BOOST_AUTO_TEST_CASE(ValidateSessionDataChecks) +{ + libcdoc::CryptoBackend crypto; + std::string err, scheme, rp; + std::string good = makeSessionToken(R"({"schemeName":"smart-id-demo","rpName":"DEMO","exp":2000000000})"); + BOOST_CHECK_EQUAL(libcdoc::validateSessionData(&crypto, "etsi/PNOEE-30303039903", good, SID_CERT_B64URL, + scheme, rp, err), + libcdoc::OK); + BOOST_CHECK_EQUAL(scheme, "smart-id-demo"); + BOOST_CHECK_EQUAL(rp, "DEMO"); + // Expired session token. + std::string expired = makeSessionToken(R"({"schemeName":"smart-id-demo","rpName":"DEMO","exp":1000000000})"); + BOOST_CHECK_EQUAL(libcdoc::validateSessionData(&crypto, "etsi/PNOEE-30303039903", expired, SID_CERT_B64URL, + scheme, rp, err), + libcdoc::NetworkBackend::NETWORK_ERROR); + // Identity mismatch. + BOOST_CHECK_EQUAL(libcdoc::validateSessionData(&crypto, "etsi/PNOEE-30303039914", good, SID_CERT_B64URL, + scheme, rp, err), + libcdoc::CRYPTO_ERROR); + // Missing scheme claims. + std::string noclaims = makeSessionToken(R"({"exp":2000000000})"); + BOOST_CHECK_EQUAL(libcdoc::validateSessionData(&crypto, "etsi/PNOEE-30303039903", noclaims, SID_CERT_B64URL, + scheme, rp, err), + libcdoc::DATA_FORMAT_ERROR); +} + +BOOST_AUTO_TEST_SUITE_END() From df0a4eda71260023fcb4d644e3094bf57c2533c3 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 7 Aug 2026 16:10:47 +0300 Subject: [PATCH 60/65] Preliminary working MobileId authentication --- cdoc/CDoc2Reader.cpp | 51 ++++--- cdoc/Configuration.h | 16 +- cdoc/KeyShares.cpp | 10 +- cdoc/KeyShares.h | 39 ++--- cdoc/NetworkBackend.cpp | 330 +++++++++++++++++++++++++++------------- cdoc/NetworkBackend.h | 27 +++- cdoc/ToolConf.h | 5 + cdoc/cdoc-tool.cpp | 6 + test/libcdoc_boost.cpp | 8 +- 9 files changed, 310 insertions(+), 182 deletions(-) diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index 0ad4d961..2df52982 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -269,8 +269,31 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) return libcdoc::CONFIGURATION_ERROR; } // auth_url = "https://cdoc2-auth.dev.riaint.ee"; + // fixme: + std::string signer = conf->getValue(Configuration::SHARE_SIGNER); + LOG_DBG("Signer: {}", signer); + bool mid = false; + if (signer == Configuration::SHARE_SIGNER_SID) { + // + } else if (signer == Configuration::SHARE_SIGNER_MID) { + mid = true; + } else { + setLastError(t_("Unknown or missing signer type")); + LOG_ERROR("Unknown or missing signer type"); + return libcdoc::CONFIGURATION_ERROR; + } + std::string phone; + if (mid) { + phone = conf->getValue({}, Configuration::PHONE_NUMBER); + if (phone.empty()) { + setLastError(t_("Missing phone number")); + LOG_ERROR("Missing phone number"); + return libcdoc::CONFIGURATION_ERROR; + } + } + SessionData session; - if (auto rv = network->authenticateForShares(auth_url, rcpt_id, session.token, session.cert); rv != OK) { + if (auto rv = network->authenticateForShares(auth_url, rcpt_id, phone, session.token, session.cert); rv != OK) { setLastError(network->getLastErrorStr(rv)); LOG_ERROR("{}", last_error); return rv; @@ -298,7 +321,7 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) // must not be expired. Also learns the schemeName/rpName claims needed // to verify the signed ticket later. std::string scheme_name, rp_name, v_err; - if (auto rv = validateSessionData(crypto, rcpt_id, session.token, session.cert, scheme_name, rp_name, v_err); rv != OK) { + if (auto rv = validateSessionData(crypto, rcpt_id, mid, session.token, session.cert, scheme_name, rp_name, v_err); rv != OK) { setLastError(v_err); LOG_ERROR("{}", last_error); return rv; @@ -328,10 +351,8 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) std::vector auth_tokens; AuthenticationData auth; result_t result = NOT_IMPLEMENTED; - // fixme: - std::string signer = "SMART_ID";// conf->getValue(Configuration::SHARE_SIGNER); - LOG_DBG("Signer: {}", signer); - if (signer == "SMART_ID") { + + if (!mid) { SIDSigner signer(rp_url, session, rcpt_id, network); result = signer.generateTickets(auth_tokens, shares); if (result != OK) { @@ -340,22 +361,15 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) auth.cert = std::move(signer.cert); auth.params = std::move(signer.params); } - } else if (signer == "MOBILE_ID") { - // "https://sid.demo.sk.ee/smart-id-rp/v2" - std::string url = conf->getValue(Configuration::MID_DOMAIN, Configuration::BASE_URL); - // "37200000566" - std::string phone = conf->getValue(Configuration::MID_DOMAIN, Configuration::PHONE_NUMBER); - MIDSigner signer(url, {}, {}, phone, rcpt_id, network); + } else { + MIDSigner signer(rp_url, phone, session, rcpt_id, network); result = signer.generateTickets(auth_tokens, shares); if (result != OK) { setLastError(signer.error); } else { auth.cert = std::move(signer.cert); + auth.params = std::move(signer.params); } - } else { - setLastError(t_("Unknown or missing signer type")); - LOG_ERROR("Unknown or missing signer type"); - return result; } if (result != libcdoc::OK) { LOG_ERROR("Cannot generate share tickets"); @@ -366,8 +380,9 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) // signature must verify (binds identity, the consent text shown to // the user, and freshness). All tickets share the same signed JWT, // so validating the first one covers them all. - if (!auth_tokens.empty()) { - if (auto rv = validateAuthTicket(crypto, rcpt_id, auth_tokens[0], auth.cert, auth.params, scheme_name, rp_name, v_err); rv != OK) { + if (!auth_tokens.empty() && !mid) { + std::vector params = fromBase64URL(auth.params[network->X_CDOC2_SID_RPV3_SIGNATURE_PARAMETERS]); + if (auto rv = validateAuthTicket(crypto, rcpt_id, auth_tokens[0], auth.cert, std::string(params.cbegin(), params.cend()), scheme_name, rp_name, v_err); rv != OK) { setLastError(v_err); LOG_ERROR("{}", last_error); return rv; diff --git a/cdoc/Configuration.h b/cdoc/Configuration.h index e1c0451d..adc47e2f 100644 --- a/cdoc/Configuration.h +++ b/cdoc/Configuration.h @@ -59,20 +59,10 @@ struct CDOC_EXPORT Configuration { * @brief Method for signing keyshare tickets (SMART_ID or MOBILE_ID) */ static constexpr char const *SHARE_SIGNER = "SHARE_SIGNER"; + static constexpr char const *SHARE_SIGNER_SID = "SMART_ID"; + static constexpr char const *SHARE_SIGNER_MID = "MOBILE_ID"; /** - * @brief Domain of SmartID settings - */ - static constexpr char const *SID_DOMAIN = "SMART_ID"; - /** - * @brief Domain of Mobile ID settings - */ - static constexpr char const *MID_DOMAIN = "MOBILE_ID"; - /** - * @brief MID/SID base url (domain is SMART_ID or MOBILE_ID) - */ - static constexpr char const *BASE_URL = "BASE_URL"; - /** - * @brief Mobile ID phone number (domain is MOBILE_ID) + * @brief Mobile ID phone number */ static constexpr char const *PHONE_NUMBER = "PHONE_NUMBER"; #endif diff --git a/cdoc/KeyShares.cpp b/cdoc/KeyShares.cpp index 8a4591c6..a0ed7337 100644 --- a/cdoc/KeyShares.cpp +++ b/cdoc/KeyShares.cpp @@ -219,7 +219,7 @@ MIDSigner::signDigest(std::vector& dst, const std::vector& dig LOG_TRACE_KEY("MID signing: {}", digest); - result_t result = network->signMID(dst, cert, url, rp_uuid, rp_name, phone, rcpt_id, digest, libcdoc::CryptoBackend::SHA_256); + result_t result = network->signMID(dst, cert, params, url, phone, session.token, session.cert, rcpt_id, digest, libcdoc::CryptoBackend::SHA_256); if (result != OK) { error = network->getLastErrorStr(result); } @@ -345,7 +345,7 @@ decodeTicket(const std::string& ticket) std::string -libcdoc::buildAcspV2Payload(const std::string& scheme_name, const std::string& server_random, +buildAcspV2Payload(const std::string& scheme_name, const std::string& server_random, const std::string& rp_challenge, const std::string& user_challenge, const std::string& rp_name, const std::string& interactions_digest, const std::string& interaction_type_used, const std::string& flow_type) @@ -359,7 +359,7 @@ libcdoc::buildAcspV2Payload(const std::string& scheme_name, const std::string& s } libcdoc::result_t -libcdoc::validateSessionData(CryptoBackend *crypto, const std::string& rcpt_id, +validateSessionData(CryptoBackend *crypto, const std::string& rcpt_id, bool is_mid, const std::string& session_token, const std::string& session_cert_b64, std::string& scheme_name, std::string& rp_name, std::string& error) { @@ -393,7 +393,7 @@ libcdoc::validateSessionData(CryptoBackend *crypto, const std::string& rcpt_id, } scheme_name = json.get("schemeName").is() ? json.get("schemeName").get() : std::string(); rp_name = json.get("rpName").is() ? json.get("rpName").get() : std::string(); - if (scheme_name.empty() || rp_name.empty()) { + if (!is_mid && (scheme_name.empty() || rp_name.empty())) { error = "Session token misses schemeName/rpName claims"; return DATA_FORMAT_ERROR; } @@ -401,7 +401,7 @@ libcdoc::validateSessionData(CryptoBackend *crypto, const std::string& rcpt_id, } libcdoc::result_t -libcdoc::validateAuthTicket(CryptoBackend *crypto, const std::string& rcpt_id, +validateAuthTicket(CryptoBackend *crypto, const std::string& rcpt_id, const std::string& ticket, const std::vector& cert_der, const std::string& signature_params_json, const std::string& scheme_name, const std::string& rp_name, diff --git a/cdoc/KeyShares.h b/cdoc/KeyShares.h index f61b4092..4fc29e1a 100644 --- a/cdoc/KeyShares.h +++ b/cdoc/KeyShares.h @@ -71,7 +71,7 @@ struct SessionData { */ struct AuthenticationData { std::vector cert; - std::string params; + std::map params; }; /** @@ -100,6 +100,11 @@ struct Signer { * @return result_t error code or ok */ virtual result_t signDigest(std::vector& dst, const std::vector& digest) = 0; + /** + * @brief Full session token + * + */ + const SessionData& session; /** * @brief Signing algorithm name (RS256/ES256) * @@ -115,7 +120,7 @@ struct Signer { * */ std::vector cert; - std::string params; + std::map params; /** * @brief The text of last error * @@ -126,10 +131,11 @@ struct Signer { /** * @brief Construct a new Signer object * + * @param _session Full session data (token and certificate) * @param _rcpt_id Recipient full id in etsi format (ets/PNOEE-XYZXYZXYZXY) * @param _algo_name Signing algorithm name (RS256/ES256) */ - Signer(const std::string& _rcpt_id, const std::string _algo_name, NetworkBackend *_network) : rcpt_id(_rcpt_id), algo_name(_algo_name), network(_network) {} + Signer(const SessionData& _session, const std::string& _rcpt_id, const std::string& _algo_name, NetworkBackend *_network) : session(_session), rcpt_id(_rcpt_id), algo_name(_algo_name), network(_network) {} }; /** @@ -142,11 +148,6 @@ struct SIDSigner : public Signer { * */ const std::string url; - /** - * @brief Full session token - * - */ - const SessionData& session; /** * @brief Construct a new SIDSigner object @@ -156,7 +157,7 @@ struct SIDSigner : public Signer { * @param _rcpt_id Recipient full id in etsi format (ets/PNOEE-XYZXYZXYZXY) */ SIDSigner(const std::string& _url, const SessionData& _session, const std::string& _rcpt_id, NetworkBackend *network) - : Signer(_rcpt_id, "RSASSA-PSS+ACSP_V2", network), url(_url), session(_session) {} + : Signer(_session, _rcpt_id, "RSASSA-PSS+ACSP_V2", network), url(_url) {} result_t signDigest(std::vector& dst, const std::vector& digest) final; }; @@ -171,16 +172,6 @@ struct MIDSigner : public Signer { * */ const std::string url; - /** - * @brief Relying party UUID - * - */ - const std::string rp_uuid; - /** - * @brief Relying party name - * - */ - const std::string rp_name; /** * @brief Recipient phone number (with country code) * @@ -190,12 +181,10 @@ struct MIDSigner : public Signer { * @brief Construct a new MIDSigner object * * @param _url Mobile ID gateway url - * @param _rp_uuid Relying party UUID - * @param _rp_name Relying party name * @param _rcpt_id Recipient full id in etsi format (ets/PNOEE-XYZXYZXYZXY) */ - MIDSigner(const std::string& _url, const std::string& _rp_uuid, const std::string& _rp_name, const std::string& _phone, const std::string& _rcpt_id, NetworkBackend *network) - : Signer(_rcpt_id, "ES256", network), url(_url), rp_uuid(_rp_uuid), rp_name(_rp_name), phone(_phone) {} + MIDSigner(const std::string& _url, const std::string& _phone, const SessionData& _session, const std::string& _rcpt_id, NetworkBackend *network) + : Signer(_session, _rcpt_id, "ES256", network), url(_url), phone(_phone) {} result_t signDigest(std::vector& dst, const std::vector& digest) final; }; @@ -241,7 +230,7 @@ std::string buildAcspV2Payload(const std::string& scheme_name, const std::string * * Checks that the session signing certificate belongs to rcpt_id (via * CryptoBackend::validateCertificate), that the session token is not expired, - * and extracts the schemeName/rpName claims needed for ticket validation. + * and extracts the schemeName/rpName claims needed for ticket validation for SmartId. * * @param crypto crypto backend * @param rcpt_id recipient id from the lock (etsi/PNOEE-...) @@ -252,7 +241,7 @@ std::string buildAcspV2Payload(const std::string& scheme_name, const std::string * @param error output: error description on failure * @return error code or OK */ -result_t validateSessionData(CryptoBackend *crypto, const std::string& rcpt_id, +result_t validateSessionData(CryptoBackend *crypto, const std::string& rcpt_id, bool is_mid, const std::string& session_token, const std::string& session_cert_b64, std::string& scheme_name, std::string& rp_name, std::string& error); diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 7e37980c..e573e8ba 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -161,7 +161,7 @@ getMIDSIDDescription(libcdoc::result_t code) // will trigger -Wswitch (no default branch covers it) and the // static_asserts will catch it explicitly. static constexpr std::string_view -hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm algo) noexcept +hashAlgorithmToSidName(libcdoc::CryptoBackend::HashAlgorithm algo) noexcept { switch (algo) { case libcdoc::CryptoBackend::HashAlgorithm::SHA_256: return "SHA-256"; @@ -173,12 +173,25 @@ hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm algo) noexcept return {}; } -static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_256) == "SHA-256"); -static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_384) == "SHA-384"); -static_assert(hashAlgorithmToSidMidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_512) == "SHA-512"); +static_assert(hashAlgorithmToSidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_256) == "SHA-256"); +static_assert(hashAlgorithmToSidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_384) == "SHA-384"); +static_assert(hashAlgorithmToSidName(libcdoc::CryptoBackend::HashAlgorithm::SHA_512) == "SHA-512"); // Out-of-range value (e.g. coming from a SWIG-generated foreign caller) // must produce an empty result rather than reading past the array. -static_assert(hashAlgorithmToSidMidName(static_cast(99)).empty()); +static_assert(hashAlgorithmToSidName(static_cast(99)).empty()); + +static constexpr std::string_view +hashAlgorithmToMidName(libcdoc::CryptoBackend::HashAlgorithm algo) noexcept +{ + switch (algo) { + case libcdoc::CryptoBackend::HashAlgorithm::SHA_256: return "SHA256"; + case libcdoc::CryptoBackend::HashAlgorithm::SHA_384: return "SHA384"; + case libcdoc::CryptoBackend::HashAlgorithm::SHA_512: return "SHA512"; + default: + break; + } + return {}; +} #endif thread_local std::string error; @@ -357,7 +370,7 @@ post(httplib::SSLClient& cli, const std::string& path, httplib::Headers& hdrs, c // Get url and fetch JSON response // static libcdoc::result_t -get(httplib::SSLClient& cli, httplib::Headers& hdrs, const std::string& path, picojson::value& rsp_json) +get(httplib::SSLClient& cli, httplib::Headers& hdrs, const std::string& path, httplib::Response& rsp) { // Capture TLS and HTTP errors LOG_DBG("GET: {}", path); @@ -379,8 +392,7 @@ get(httplib::SSLClient& cli, httplib::Headers& hdrs, const std::string& path, pi error = FORMAT("Http status {}", status); return libcdoc::NetworkBackend::NETWORK_ERROR; } - httplib::Response rsp = res.value(); - picojson::parse(rsp_json, rsp.body); + rsp = res.value(); error = {}; return libcdoc::OK; } @@ -468,10 +480,23 @@ libcdoc::NetworkBackend::fetchKey (std::vector& dst, const std::string& std::string full = path + "/key-capsules/" + transaction_id; httplib::Headers hdrs; - picojson::value rsp_json; - result = get(cli, hdrs, full, rsp_json); + httplib::Response rsp;; + result = get(cli, hdrs, full, rsp); if (result != libcdoc::OK) return result; + picojson::value rsp_json; + std::string parse_err = picojson::parse(rsp_json, rsp.body); + if (!parse_err.empty()) { + error = FORMAT("JSON parse error: {}", parse_err); + LOG_ERROR("{}", error); + return NETWORK_ERROR; + } + if (!rsp_json.is()) { + error = "Invalid Authentication response"; + LOG_WARN("Invalid Authentication response"); + return NetworkBackend::NETWORK_ERROR; + } + std::string ks = getJsonString(rsp_json, "ephemeral_key_material", result); if (result != libcdoc::OK) return NETWORK_ERROR; dst = fromBase64(ks); @@ -551,16 +576,25 @@ waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& std::string full = path + auth_proc_uuid; LOG_DBG("SID/MID authentication query path: {}", full); while (getTime() < end) { - picojson::value rsp; + httplib::Response rsp; result_t result = get(cli, hdrs, full, rsp); if (result != OK) return result; - if (!rsp.is()) { - error = "Response is not a JSON object"; - LOG_WARN("{}", error); + + picojson::value rsp_json; + std::string parse_err = picojson::parse(rsp_json, rsp.body); + if (!parse_err.empty()) { + error = FORMAT("JSON parse error: {}", parse_err); + LOG_ERROR("{}", error); + return NetworkBackend::NETWORK_ERROR; + } + if (!rsp_json.is()) { + error = "Invalid Authentication response"; + LOG_WARN("Invalid Authentication response"); return NetworkBackend::NETWORK_ERROR; } + // Status - dst.status = getJsonString(rsp, "status", result); + dst.status = getJsonString(rsp_json, "status", result); if (result != OK) return NetworkBackend::NETWORK_ERROR; LOG_DBG("Status: {}", dst.status); @@ -576,7 +610,7 @@ waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& } // State is complete, check for end result - dst.endResult = getJsonString(rsp, "endResult", result); + dst.endResult = getJsonString(rsp_json, "endResult", result); if (result != OK) return NetworkBackend::NETWORK_ERROR; LOG_DBG("EndResult: {}", dst.endResult); if (dst.endResult != "OK") { @@ -585,10 +619,10 @@ waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& } // Fetch session token and certificate - dst.sessionToken = getJsonString(rsp, "sessionToken", result); + dst.sessionToken = getJsonString(rsp_json, "sessionToken", result); if (result != OK) return NetworkBackend::NETWORK_ERROR; LOG_TRACE("Session token: {}", dst.sessionToken); - dst.cert = getJsonString(rsp, "signingCertificate", result); + dst.cert = getJsonString(rsp_json, "signingCertificate", result); if (result != OK) return NetworkBackend::NETWORK_ERROR; LOG_TRACE("Certificate: {}", dst.cert); error = {}; @@ -603,7 +637,7 @@ waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& } libcdoc::result_t -libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std::string& rcpt_id, std::string& token, std::string& cert) +libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std::string& rcpt_id, const std::string& phone, std::string& token, std::string& cert) { // Start authentication std::string host, path; @@ -628,8 +662,11 @@ libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std if (result = setProxy(cli, this); result != OK) return result; picojson::object obj = { - {"identifier", picojson::value(rcpt_id)}, + {"identifier", picojson::value(rcpt_id)} }; + if (!phone.empty()) { + obj.emplace("mobileNr", picojson::value(phone)); + } picojson::value req_json(obj); std::string req_str = req_json.serialize(); LOG_DBG("POST authentication request to: {}", url); @@ -640,8 +677,6 @@ libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std httplib::Response rsp; result = post(cli, full, hdrs, req_str, rsp); if (result != libcdoc::OK) return result; - LOG_DBG("Status: {}", rsp.status); - LOG_DBG("Response: {}", rsp.body); std::string location = rsp.get_header_value("Location"); LOG_DBG("Location: {}", location); @@ -773,7 +808,8 @@ libcdoc::NetworkBackend::fetchNonce(std::vector& dst, const std::string } libcdoc::result_t -libcdoc::NetworkBackend::fetchShare(ShareInfo& share, const std::string& url, const std::string& share_id, const std::string& session_token, const std::string& session_cert, const std::string& auth_token, const std::vector& auth_cert, const std::string& auth_params) +libcdoc::NetworkBackend::fetchShare(ShareInfo& share, const std::string& url, const std::string& share_id, + const std::string& session_token, const std::string& session_cert, const std::string& auth_token, const std::vector& auth_cert, const std::map& auth_params) { LOG_DBG("Get share from: {}", url); @@ -800,11 +836,26 @@ libcdoc::NetworkBackend::fetchShare(ShareInfo& share, const std::string& url, co hdrs.insert({"x-cdoc2-session-x5c", session_cert}); hdrs.insert({"x-cdoc2-auth-token", auth_token}); hdrs.insert({"x-cdoc2-auth-x5c", toBase64URL(auth_cert)}); - hdrs.insert({"x-cdoc2-sid-rpv3-signature-parameters", toBase64URL(auth_params)}); - picojson::value rsp_json; - result = get(cli, hdrs, full, rsp_json); + for (const auto& val : auth_params) { + hdrs.insert({val.first, val.second}); + } + httplib::Response rsp; + result = get(cli, hdrs, full, rsp); if (result != libcdoc::OK) return result; + picojson::value rsp_json; + std::string parse_err = picojson::parse(rsp_json, rsp.body); + if (!parse_err.empty()) { + error = FORMAT("JSON parse error: {}", parse_err); + LOG_ERROR("{}", error); + return NETWORK_ERROR; + } + if (!rsp_json.is()) { + error = "Invalid Authentication response"; + LOG_WARN("Invalid Authentication response"); + return NetworkBackend::NETWORK_ERROR; + } + libcdoc::result_t rv = libcdoc::OK; std::string share64 = getJsonString(rsp_json, "share", rv); if (rv != libcdoc::OK) return rv; @@ -832,41 +883,63 @@ libcdoc::NetworkBackend::showFeedback(SIDMIDFeedback& feedback) } // -// https://github.com/SK-EID/smart-id-documentation +// https://open-eid.github.io/CDOC2/ // -struct SIDResponse { +struct SIDParams { + // Signature json without signature value to create verification info + picojson::object signature_json; + std::string inter_type_used; +}; + +struct MIDParams { + std::string x_rp_signed_hash; + std::string x_rp_name; + std::string signature_input; + std::string signature; +}; + +struct SIDMIDResponse { // Signature value, base64 encoded std::string signature; // Signer certificate, base64 encoded std::string cert; - // Full signature json to create verification info - picojson::object signature_json; - std::string inter_type_used; + // Protocol parameters + SIDParams sid; + MIDParams mid; }; namespace libcdoc { static result_t -waitForResult(SIDResponse& dst, httplib::SSLClient& cli, const std::string& path, const std::string& auth_token_disclosed, const std::string& auth_cert, const std::string& session_id, double seconds) +waitForResult(SIDMIDResponse& dst, httplib::SSLClient& cli, const std::string& path, const std::string& auth_token_disclosed, const std::string& auth_cert, const std::string& session_id, bool sid, double seconds) { double end = libcdoc::getTime() + seconds; std::string full = path + session_id; LOG_DBG("SID/MID session query path: {}", full); while (libcdoc::getTime() < end) { - picojson::value rsp; + httplib::Response rsp; httplib::Headers hdrs; hdrs.insert({"x-cdoc2-session-token", auth_token_disclosed}); hdrs.insert({"x-cdoc2-session-x5c", auth_cert}); result_t result = get(cli, hdrs, full, rsp); if (result != OK) return result; - if (!rsp.is()) { - error = "Response is not a JSON object"; - LOG_WARN("{}", error); + + picojson::value rsp_json; + std::string parse_err = picojson::parse(rsp_json, rsp.body); + if (!parse_err.empty()) { + error = FORMAT("JSON parse error: {}", parse_err); + LOG_ERROR("{}", error); return NetworkBackend::NETWORK_ERROR; } + if (!rsp_json.is()) { + error = "Invalid Authentication response"; + LOG_WARN("Invalid Authentication response"); + return NetworkBackend::NETWORK_ERROR; + } + // State - std::string str = getJsonString(rsp, "state", result); + std::string str = getJsonString(rsp_json, "state", result); if (result != OK) return result; if (str == "RUNNING") { // Pause for 0.5 seconds and repeat @@ -879,40 +952,67 @@ waitForResult(SIDResponse& dst, httplib::SSLClient& cli, const std::string& path return NetworkBackend::NETWORK_ERROR; } - // State is complete, check for end result - picojson::object result_obj = getJsonObject(rsp, "result", result); - if (result != OK) return result; - str = getJsonString(picojson::value(result_obj), "endResult", result); - if (result != OK) return result; - result = parseMIDSIDResult(str); - if (result == UNSPECIFIED_ERROR) { - // Unknown result - error = FORMAT("unknwon endResult value: {}", str); - LOG_WARN("{}", error); - return NetworkBackend::NETWORK_ERROR; - } else if (result != OK) { - LOG_WARN("EndResult is not OK: {}", str); - return result; - } - // documentNumber - // details + if (sid) { + // State is complete, check for end result + picojson::object result_obj = getJsonObject(rsp_json, "result", result); + if (result != OK) return result; + str = getJsonString(picojson::value(result_obj), "endResult", result); + if (result != OK) return result; + result = parseMIDSIDResult(str); + if (result == UNSPECIFIED_ERROR) { + // Unknown result + error = FORMAT("unknwon endResult value: {}", str); + LOG_WARN("{}", error); + return NetworkBackend::NETWORK_ERROR; + } else if (result != OK) { + LOG_WARN("EndResult is not OK: {}", str); + return result; + } + // documentNumber + // details + + // signatureProtocol + // Signature (optional field; rsp is a verified object here) + if (picojson::value sig = rsp_json.get("signature"); sig.is()) { + dst.signature = getJsonString(sig, "value", result); + if (result != OK) return result; + dst.sid.signature_json = sig.get(); + dst.sid.signature_json.erase("value"); + } + // Interaction type + dst.sid.inter_type_used = getJsonString(rsp_json, "interactionTypeUsed", result); + if (result != OK) return result; - // signatureProtocol - // Signature (optional field; rsp is a verified object here) - if (picojson::value sig = rsp.get("signature"); sig.is()) { - dst.signature = getJsonString(sig, "value", result); + // Certificate + picojson::object cert_obj = getJsonObject(rsp_json, "cert", result); + if (result != OK) return result; + dst.cert = getJsonString(picojson::value(cert_obj), "value", result); + if (result != OK) return result; + } else { + std::string str = getJsonString(rsp_json, "result", result); + if (result != OK) return result; + result = parseMIDSIDResult(str); + if (result == UNSPECIFIED_ERROR) { + // Unknown result + error = FORMAT("unknwon endResult value: {}", str); + LOG_WARN("{}", error); + return NetworkBackend::NETWORK_ERROR; + } else if (result != OK) { + LOG_WARN("EndResult is not OK: {}", str); + return result; + } + picojson::object sig = getJsonObject(rsp_json, "signature", result); + if (result != OK) return result; + dst.signature = getJsonString(picojson::value(sig), "value", result); + if (result != OK) return result; + dst.cert = getJsonString(picojson::value(rsp_json), "cert", result); if (result != OK) return result; - dst.signature_json = sig.get(); - } - // Interaction type - dst.inter_type_used = getJsonString(rsp, "interactionTypeUsed", result); - if (result != OK) return result; - // Certificate - picojson::object cert_obj = getJsonObject(rsp, "cert", result); - if (result != OK) return result; - dst.cert = getJsonString(picojson::value(cert_obj), "value", result); - if (result != OK) return result; + dst.mid.x_rp_signed_hash = rsp.get_header_value("x-rp-signed-hash"); + dst.mid.x_rp_name = rsp.get_header_value("x-rp-name"); + dst.mid.signature_input = rsp.get_header_value("Signature-Input"); + dst.mid.signature = rsp.get_header_value("Signature"); + } error = {}; return OK; @@ -926,7 +1026,7 @@ waitForResult(SIDResponse& dst, httplib::SSLClient& cli, const std::string& path } libcdoc::result_t -libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector& cert, std::string& params, +libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector& cert, std::map& params, const std::string& url, const std::string& session_token, const std::string& session_cert, const std::string& rcpt_id, const std::vector& digest, CryptoBackend::HashAlgorithm algo) { @@ -948,11 +1048,6 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector if (!rcpt_id.starts_with("etsi/")) return libcdoc::INTERNAL_ERROR; std::string semanticIdentifier = rcpt_id.substr(5); - auto nonce_bytes = Crypto::random(16); - if (nonce_bytes.empty()) - return libcdoc::CRYPTO_ERROR; - std::string nonce = libcdoc::toBase64(nonce_bytes); - SessionToken stoken(session_token); std::string session_token_disclosed = stoken.discloseForUrl(url); @@ -972,7 +1067,7 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector picojson::value(inter) }; //std::string inter_str = picojson::value(inter_arr).serialize(); - std::string inter_str = "[{\"type\":\"confirmationnMessageAndVerificationCodeChoice\",\"displayText200\":\"Do you want to decrypt the document\"}]"; + std::string inter_str = "[{\"type\":\"confirmationMessageAndVerificationCodeChoice\",\"displayText200\":\"Do you want to decrypt the document\"}]"; LOG_DBG("Interactions: {}", inter_str); inter_str = toBase64((const uint8_t *) inter_str.data(), inter_str.size()); std::string inter_str_64 = toBase64((const uint8_t *) inter_str.data(), inter_str.size()); @@ -1044,35 +1139,42 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector if (rv != libcdoc::OK) return rv; LOG_DBG("SessionID: {}", sessionId); - SIDResponse sidrsp; - result = waitForResult(sidrsp, cli, path + "/sid/session/", session_token_disclosed, session_cert, sessionId, 60); + SIDMIDResponse sidrsp; + result = waitForResult(sidrsp, cli, path + "/sid/session/", session_token_disclosed, session_cert, sessionId, true, 60); if (result != OK) return result; LOG_DBG("Certificate: {}", sidrsp.cert); LOG_DBG("Signature: {}", sidrsp.signature); - sidrsp.signature_json.erase("value"); SHA256((uint8_t *) inter_str.c_str(), inter_str.size(), b.data()); std::string inter_hash_64 = toBase64(b.data(), b.size()); picojson::object sig_parms = { {"interactionsDigest", picojson::value(inter_hash_64)}, - {"interactionTypeUsed", picojson::value(sidrsp.inter_type_used)}, - {"signature", picojson::value(sidrsp.signature_json)}, + {"interactionTypeUsed", picojson::value(sidrsp.sid.inter_type_used)}, + {"signature", picojson::value(sidrsp.sid.signature_json)}, }; dst = fromBase64(sidrsp.signature); cert = fromBase64(sidrsp.cert); - params = picojson::value(sig_parms).serialize(); + params[X_CDOC2_SID_RPV3_SIGNATURE_PARAMETERS] = toBase64URL(picojson::value(sig_parms).serialize()); return OK; } libcdoc::result_t -libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector& cert, - const std::string& url, const std::string& rp_uuid, const std::string& rp_name, const std::string& phone, +libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector& cert, std::map& params, + const std::string& url, const std::string& phone, const std::string& session_token, const std::string& session_cert, const std::string& rcpt_id, const std::vector& digest, CryptoBackend::HashAlgorithm algo) { + //phoneNumber: '+3726234566' + //nationalIdentityNumber: '38412319871' + //hash: 0nbgC2fVdLVQFZJdBbmG8B+kXnZtX1FSTM59UVDQ4Gc= + //hashType: SHA256 + //language: ENG + //displayText: Decrypting container file "test.txt" + //displayTextFormat: GSM-7 + // Validate rcpt_id BEFORE doing anything else (network setup, key // material, etc.). The previous implementation called // rcpt_id.substr(11, 11) which throws std::out_of_range when @@ -1098,12 +1200,6 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector return libcdoc::WRONG_ARGUMENTS; } - std::string certificateLevel = "QUALIFIED"; - auto nonce_bytes = Crypto::random(16); - if (nonce_bytes.empty()) - return libcdoc::CRYPTO_ERROR; - std::string nonce = libcdoc::toBase64(nonce_bytes); - std::string host, path; int port; int result = libcdoc::parseURL(url, host, port, path); @@ -1113,6 +1209,9 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector LOG_DBG("PORT:{}", port); LOG_DBG("PATH:{}", path); + SessionToken stoken(session_token); + std::string session_token_disclosed = stoken.discloseForUrl(url); + LOG_DBG("Starting client: {} {}", host, port); httplib::SSLClient cli(host, port); if (result = applySSLTimeout(cli, this); result != OK) return result; @@ -1123,7 +1222,7 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector // // Authenticate // - std::string_view algo_name = hashAlgorithmToSidMidName(algo); + std::string_view algo_name = hashAlgorithmToMidName(algo); if (algo_name.empty()) { error = "Unsupported hash algorithm for Mobile-ID"; LOG_ERROR("Unsupported hash algorithm for Mobile-ID: {}", @@ -1138,8 +1237,6 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector if (result != OK) return result; picojson::object qobj = { - {"relyingPartyUUID", picojson::value(rp_uuid)}, - {"relyingPartyName", picojson::value(rp_name)}, {"phoneNumber", picojson::value(phone)}, {"nationalIdentityNumber", picojson::value(id_num)}, {"hash", picojson::value(toBase64(digest))}, @@ -1151,42 +1248,57 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector picojson::value query = picojson::value(qobj); LOG_DBG("JSON:{}", query.serialize()); // - // Sign digest + // Begin authentication session // - std::string full = path + "/authentication"; - LOG_DBG("Mobile ID path: {}", full); + std::string full = path + "/mid/authenticate"; + LOG_DBG("MobileID path: {}", full); httplib::Headers hdrs; + hdrs.insert({"x-cdoc2-session-token", session_token_disclosed}); + hdrs.insert({"x-cdoc2-session-x5c", session_cert}); httplib::Response rsp; result = post(cli, full, hdrs, query.serialize(), rsp); if (result != libcdoc::OK) return result; LOG_DBG("Response: {}", rsp.body); - picojson::value v; - std::string parse_err = picojson::parse(v, rsp.body); + // Reply: + // + // {"sessionID":"xyz..."} + // + picojson::value rsp_json; + std::string parse_err = picojson::parse(rsp_json, rsp.body); if (!parse_err.empty()) { error = FORMAT("JSON parse error: {}", parse_err); LOG_ERROR("{}", error); - return NetworkBackend::NETWORK_ERROR; + return NETWORK_ERROR; } - if (!v.is()) { - error = "Invalid Mobile ID response"; - LOG_WARN("Invalid Mobile ID response"); + if (!rsp_json.is()) { + error = "Invalid Authentication response"; + LOG_WARN("Invalid Authentication response"); return NetworkBackend::NETWORK_ERROR; } libcdoc::result_t rv = libcdoc::OK; - std::string sessionID = getJsonString(v, "sessionID", rv); + std::string sessionId = getJsonString(rsp_json, "sessionID", rv); if (rv != libcdoc::OK) return rv; - LOG_DBG("SessionID: {}", sessionID); + LOG_DBG("SessionID: {}", sessionId); - SIDResponse sidrsp; - result = waitForResult(sidrsp, cli, path + "/authentication/session/", {}, {}, sessionID, 60); + SIDMIDResponse midrsp; + result = waitForResult(midrsp, cli, path + "/mid/session/", session_token_disclosed, session_cert, sessionId, false, 60); if (result != OK) return result; - LOG_DBG("Certificate: {}", sidrsp.cert); - LOG_DBG("Signature: {}", sidrsp.signature); + LOG_DBG("Certificate: {}", midrsp.cert); + LOG_DBG("Signature: {}", midrsp.signature); + LOG_DBG("x-rp-signed-hash: {}", midrsp.mid.x_rp_signed_hash); + LOG_DBG("x-rp-name: {}", midrsp.mid.x_rp_name); + LOG_DBG("Signature-Input: {}", midrsp.mid.signature_input); + LOG_DBG("Signature: {}", midrsp.mid.signature); - dst = fromBase64(sidrsp.signature); - cert = fromBase64(sidrsp.cert); + params[X_RP_SIGNED_HASH] = midrsp.mid.x_rp_signed_hash; + params[X_RP_NAME] = midrsp.mid.x_rp_name; + params[HDR_SIGNATURE_INPUT] = midrsp.mid.signature_input; + params[HDR_SIGNATURE] = midrsp.mid.signature; + + dst = fromBase64(midrsp.signature); + cert = fromBase64(midrsp.cert); return OK; } diff --git a/cdoc/NetworkBackend.h b/cdoc/NetworkBackend.h index 36c71bd1..0e7df7ae 100644 --- a/cdoc/NetworkBackend.h +++ b/cdoc/NetworkBackend.h @@ -21,6 +21,8 @@ #include +#include + namespace libcdoc { struct CDOC_EXPORT NetworkBackend { @@ -188,6 +190,13 @@ struct CDOC_EXPORT NetworkBackend { virtual result_t fetchKey (std::vector& dst, const std::string& url, const std::string& transaction_id); #ifdef HAS_KEYSHARES + + const std::string X_CDOC2_SID_RPV3_SIGNATURE_PARAMETERS = "x-cdoc2-sid-rpv3-signature-parameters"; + const std::string X_RP_SIGNED_HASH = "x-rp-signed-hash"; + const std::string X_RP_NAME = "x-rp-name"; + const std::string HDR_SIGNATURE_INPUT = "Signature-Input"; + const std::string HDR_SIGNATURE = "Signature"; + /** * @brief Get a session token and certificate for share authentication * @@ -201,7 +210,7 @@ struct CDOC_EXPORT NetworkBackend { * @param cert Output parameter for session certificate * @return Error code or OK */ - virtual result_t authenticateForShares(const std::string& url, const std::string& rcpt_id, std::string& token, std::string& cert); + virtual result_t authenticateForShares(const std::string& url, const std::string& rcpt_id, const std::string& phone, std::string& token, std::string& cert); /** * @brief fetch authentication nonce from share server @@ -220,7 +229,8 @@ struct CDOC_EXPORT NetworkBackend { * @param cert a certificate of signing key (PEM without newlines) * @return error code or OK */ - virtual result_t fetchShare(ShareInfo& share, const std::string& url, const std::string& share_id, const std::string& session_token, const std::string& session_cert, const std::string& auth_token, const std::vector& auth_cert, const std::string& auth_params); + virtual result_t fetchShare(ShareInfo& share, const std::string& url, const std::string& share_id, + const std::string& session_token, const std::string& session_cert, const std::string& auth_token, const std::vector& auth_cert, const std::map& auth_params); #endif /** @@ -286,16 +296,17 @@ struct CDOC_EXPORT NetworkBackend { * * @param dst a container for signature * @param cert a container for certificate + * @param params SID signature parameters * @param url SmartID gateway base URL - * @param rp_uuid relying party UUID - * @param rp_name relying party name + * @param session_token session token + * @param session_cert session certificate * @param rcpt_id recipient id (etsi/PNOEE-XYZXYZXYZXY) * @param digest digest to sign * @param algo algorithm type (SHA256, SHA385, SHA512) * @return error code or OK */ - result_t signSID(std::vector& dst, std::vector& cert, std::string& params, - const std::string& url, const std::string& auth_token, const std::string& auth_cert, + result_t signSID(std::vector& dst, std::vector& cert, std::map& params, + const std::string& url, const std::string& session_token, const std::string& session_cert, const std::string& rcpt_id, const std::vector& digest, CryptoBackend::HashAlgorithm algo); /** @@ -312,8 +323,8 @@ struct CDOC_EXPORT NetworkBackend { * @param algo algorithm type (SHA256, SHA385, SHA512) * @return error code or OK */ - result_t signMID(std::vector& dst, std::vector& cert, - const std::string& url, const std::string& rp_uuid, const std::string& rp_name, const std::string& phone, + result_t signMID(std::vector& dst, std::vector& cert, std::map& params, + const std::string& url, const std::string& phone, const std::string& session_token, const std::string& session_cert, const std::string& rcpt_id, const std::vector& digest, CryptoBackend::HashAlgorithm algo); #endif }; diff --git a/cdoc/ToolConf.h b/cdoc/ToolConf.h index 2db4c1a5..3dc865b5 100644 --- a/cdoc/ToolConf.h +++ b/cdoc/ToolConf.h @@ -58,6 +58,7 @@ struct ToolConf : public JSONConfiguration { std::vector servers; std::string auth_server; std::string rp_server; + std::string phone; /** * @brief Files to be encrypted, or file to be decrypted. @@ -85,6 +86,10 @@ struct ToolConf : public JSONConfiguration { return auth_server; } else if (param == Configuration::RP_SERVER) { return rp_server; + } else if (param == Configuration::PHONE_NUMBER) { + return phone; + } else if (param == Configuration::SHARE_SIGNER) { + return (phone.empty()) ? Configuration::SHARE_SIGNER_SID : Configuration::SHARE_SIGNER_MID; } } for (auto& sdata : servers) { diff --git a/cdoc/cdoc-tool.cpp b/cdoc/cdoc-tool.cpp index 7d54dfe5..791eb00b 100644 --- a/cdoc/cdoc-tool.cpp +++ b/cdoc/cdoc-tool.cpp @@ -86,6 +86,8 @@ print_usage(ostream& ofs) ofs << " --key-label - PKCS11 key label" << endl; ofs << " --rp-server RP_SERVER - RP server URL" << endl; ofs << " --auth-server AUTH_SERVER - Authentication server URL" << endl; + ofs << " --phone NUMBER - Phone number for MID signing (starting with + and country prefix)" << endl; + ofs << " - If the phone number is present user is authenticated with MobileID, otherwise with SmartId" << endl; ofs << endl; ofs << "cdoc-tool locks FILE" << endl; ofs << endl; @@ -188,6 +190,10 @@ parse_common(ToolConf& conf, int arg_idx, int argc, char *argv[]) if ((arg_idx + 1) >= argc) return RESULT_USAGE; conf.rp_server = argv[arg_idx + 1]; return 2; + } else if (arg == "--phone") { + if ((arg_idx + 1) >= argc) return RESULT_USAGE; + conf.phone = argv[arg_idx + 1]; + return 2; } else if (arg == "--accept") { if ((arg_idx + 1) >= argc) return RESULT_USAGE; load_certs(conf, argv[arg_idx + 1]); diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 2989b791..109b9242 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -1881,23 +1881,23 @@ BOOST_AUTO_TEST_CASE(ValidateSessionDataChecks) libcdoc::CryptoBackend crypto; std::string err, scheme, rp; std::string good = makeSessionToken(R"({"schemeName":"smart-id-demo","rpName":"DEMO","exp":2000000000})"); - BOOST_CHECK_EQUAL(libcdoc::validateSessionData(&crypto, "etsi/PNOEE-30303039903", good, SID_CERT_B64URL, + BOOST_CHECK_EQUAL(libcdoc::validateSessionData(&crypto, "etsi/PNOEE-30303039903", false, good, SID_CERT_B64URL, scheme, rp, err), libcdoc::OK); BOOST_CHECK_EQUAL(scheme, "smart-id-demo"); BOOST_CHECK_EQUAL(rp, "DEMO"); // Expired session token. std::string expired = makeSessionToken(R"({"schemeName":"smart-id-demo","rpName":"DEMO","exp":1000000000})"); - BOOST_CHECK_EQUAL(libcdoc::validateSessionData(&crypto, "etsi/PNOEE-30303039903", expired, SID_CERT_B64URL, + BOOST_CHECK_EQUAL(libcdoc::validateSessionData(&crypto, "etsi/PNOEE-30303039903", false, expired, SID_CERT_B64URL, scheme, rp, err), libcdoc::NetworkBackend::NETWORK_ERROR); // Identity mismatch. - BOOST_CHECK_EQUAL(libcdoc::validateSessionData(&crypto, "etsi/PNOEE-30303039914", good, SID_CERT_B64URL, + BOOST_CHECK_EQUAL(libcdoc::validateSessionData(&crypto, "etsi/PNOEE-30303039914", false, good, SID_CERT_B64URL, scheme, rp, err), libcdoc::CRYPTO_ERROR); // Missing scheme claims. std::string noclaims = makeSessionToken(R"({"exp":2000000000})"); - BOOST_CHECK_EQUAL(libcdoc::validateSessionData(&crypto, "etsi/PNOEE-30303039903", noclaims, SID_CERT_B64URL, + BOOST_CHECK_EQUAL(libcdoc::validateSessionData(&crypto, "etsi/PNOEE-30303039903", false, noclaims, SID_CERT_B64URL, scheme, rp, err), libcdoc::DATA_FORMAT_ERROR); } From b638e6d0457e8d2427bf863547db3524b6762bf7 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 7 Aug 2026 22:16:42 +0300 Subject: [PATCH 61/65] Added MID signature and certificate validation --- cdoc/CDoc2Reader.cpp | 32 +++++-- cdoc/Crypto.cpp | 78 +++++++++++++++++ cdoc/Crypto.h | 17 ++++ cdoc/KeyShares.cpp | 180 ++++++++++++++++++++++++++++++++++++++++ cdoc/KeyShares.h | 37 +++++++++ cdoc/NetworkBackend.cpp | 46 ++++++++++ cdoc/NetworkBackend.h | 15 ++++ test/libcdoc_boost.cpp | 136 ++++++++++++++++++++++++++++++ 8 files changed, 534 insertions(+), 7 deletions(-) diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index 2df52982..ca1a4d79 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -376,16 +376,34 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) return result; } // S8: verify the signed auth ticket client-side before spending it - - // the signing certificate must belong to rcpt_id and the ACSP_V2 + // the signing certificate must belong to rcpt_id and the ticket // signature must verify (binds identity, the consent text shown to // the user, and freshness). All tickets share the same signed JWT, // so validating the first one covers them all. - if (!auth_tokens.empty() && !mid) { - std::vector params = fromBase64URL(auth.params[network->X_CDOC2_SID_RPV3_SIGNATURE_PARAMETERS]); - if (auto rv = validateAuthTicket(crypto, rcpt_id, auth_tokens[0], auth.cert, std::string(params.cbegin(), params.cend()), scheme_name, rp_name, v_err); rv != OK) { - setLastError(v_err); - LOG_ERROR("{}", last_error); - return rv; + if (!auth_tokens.empty()) { + if (!mid) { + // Smart-ID: RSASSA-PSS over the ACSP_V2 payload + std::vector params = fromBase64URL(auth.params[network->X_CDOC2_SID_RPV3_SIGNATURE_PARAMETERS]); + if (auto rv = validateAuthTicket(crypto, rcpt_id, auth_tokens[0], auth.cert, std::string(params.cbegin(), params.cend()), scheme_name, rp_name, v_err); rv != OK) { + setLastError(v_err); + LOG_ERROR("{}", last_error); + return rv; + } + } else { + // Mobile-ID: ECDSA (ES256) ticket signature plus the RP + // server RFC9421 HTTP countersignature. The RP signing keys + // are fetched from its well-known endpoint. + std::string jwks; + if (auto rv = network->fetchWellKnownKeys(jwks, rp_url); rv != OK) { + setLastError(network->getLastErrorStr(rv)); + LOG_ERROR("{}", last_error); + return rv; + } + if (auto rv = validateAuthTicketMID(crypto, rcpt_id, auth_tokens[0], auth.cert, auth.params, jwks, v_err); rv != OK) { + setLastError(v_err); + LOG_ERROR("{}", last_error); + return rv; + } } } std::vector& kek_build = kek.getTarget(32); diff --git a/cdoc/Crypto.cpp b/cdoc/Crypto.cpp index 53014e1b..4fe8507a 100644 --- a/cdoc/Crypto.cpp +++ b/cdoc/Crypto.cpp @@ -30,7 +30,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -59,6 +61,31 @@ const std::string Crypto::RSA_MTH = "http://www.w3.org/2001/04/xmlenc#rsa-1_5"; const std::string Crypto::CONCATKDF_MTH = "http://www.w3.org/2009/xmlenc11#ConcatKDF"; const std::string Crypto::AGREEMENT_MTH = "http://www.w3.org/2009/xmlenc11#ECDH-ES"; +// Convert a raw ECDSA r||s signature (JWS/RFC9421 convention) to the DER +// SEQUENCE-of-INTEGERs form expected by OpenSSL. +static std::vector +ecRawSigToDer(const std::vector &signature) +{ + if (signature.empty() || signature.size() % 2 != 0) + return {}; + size_t half = signature.size() / 2; + auto sig = make_unique_ptr(ECDSA_SIG_new()); + if (!sig) + return {}; + if (ECDSA_SIG_set0(sig.get(), + BN_bin2bn(signature.data(), int(half), nullptr), + BN_bin2bn(signature.data() + half, int(half), nullptr)) != 1) + return {}; + int len = i2d_ECDSA_SIG(sig.get(), nullptr); + if (len <= 0) + return {}; + auto der = std::vector(static_cast(len)); + uint8_t *out = der.data(); + if (i2d_ECDSA_SIG(sig.get(), &out) != len) + return {}; + return der; +} + bool Crypto::validateSignature(const std::vector &cert_der, const std::vector &data, @@ -92,10 +119,61 @@ Crypto::validateSignature(const std::vector &cert_der, return false; return EVP_PKEY_verify(ctx.get(), signature.data(), signature.size(), md_value, md_len) == 1; } + case SignatureAlgorithm::ES256: { + // ECDSA verifies the given digest directly; the signature arrives as + // raw r||s (JWS convention) and must be re-wrapped into DER. + if (data.size() != 32) + return false; + auto der = ecRawSigToDer(signature); + if (der.empty()) + return false; + if (EVP_PKEY_verify_init(ctx.get()) != 1) + return false; + if (EVP_PKEY_CTX_set_signature_md(ctx.get(), EVP_sha256()) <= 0) + return false; + return EVP_PKEY_verify(ctx.get(), der.data(), der.size(), data.data(), data.size()) == 1; + } } return false; } +bool +Crypto::validateSignatureECPoint(const std::vector &pubkey_point, + const std::vector &digest, + const std::vector &signature) +{ + if (pubkey_point.size() != 65 || pubkey_point[0] != 0x04 || digest.size() != 32) + return false; + auto ctx = make_unique_ptr( + EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr)); + if (!ctx) + return false; + // The group name string must outlive EVP_PKEY_fromdata (it is referenced, + // not copied) + char group_name[] = "P-256"; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME, group_name, 0), + OSSL_PARAM_construct_octet_string(OSSL_PKEY_PARAM_PUB_KEY, (void *) pubkey_point.data(), pubkey_point.size()), + OSSL_PARAM_construct_end() + }; + EVP_PKEY *raw_pkey = nullptr; + if (EVP_PKEY_fromdata_init(ctx.get()) != 1 || + EVP_PKEY_fromdata(ctx.get(), &raw_pkey, EVP_PKEY_PUBLIC_KEY, params) != 1) + return false; + auto pkey = make_unique_ptr(raw_pkey); + auto vctx = make_unique_ptr(EVP_PKEY_CTX_new(pkey.get(), nullptr)); + if (!vctx) + return false; + auto der = ecRawSigToDer(signature); + if (der.empty()) + return false; + if (EVP_PKEY_verify_init(vctx.get()) != 1) + return false; + if (EVP_PKEY_CTX_set_signature_md(vctx.get(), EVP_sha256()) <= 0) + return false; + return EVP_PKEY_verify(vctx.get(), der.data(), der.size(), digest.data(), digest.size()) == 1; +} + std::vector Crypto::AESWrap(const std::vector &key, const std::vector &data, bool encrypt) { // Note: AES_set_{encrypt,decrypt}_key return 0 on success and a negative diff --git a/cdoc/Crypto.h b/cdoc/Crypto.h index 4a2c771e..a3f49f9b 100644 --- a/cdoc/Crypto.h +++ b/cdoc/Crypto.h @@ -197,6 +197,7 @@ class Crypto */ enum class SignatureAlgorithm { RSASSA_PSS_SHA256, /**< RSASSA-PSS, SHA-256, MGF1/SHA-256, salt length = digest length */ + ES256, /**< ECDSA P-256/SHA-256; data must be the 32-byte digest, signature is raw r||s */ }; /** @@ -215,6 +216,22 @@ class Crypto const std::vector &signature, SignatureAlgorithm algo); + /** + * @brief Validate an ES256 signature with a raw EC public key point + * + * Used for the RFC9421 HTTP countersignature of the RP server (Mobile-ID + * flow), where the signing key is distributed as a JWK (x/y coordinates) + * rather than a certificate. + * + * @param pubkey_point uncompressed EC P-256 point (0x04 || x || y, 65 bytes) + * @param digest the 32-byte SHA-256 digest of the signed data + * @param signature raw r||s signature (64 bytes) + * @return true if the signature verifies + */ + static bool validateSignatureECPoint(const std::vector &pubkey_point, + const std::vector &digest, + const std::vector &signature); + static bool isError(int retval, const char* funcName, const char* file, int line) { if (retval < 1) { diff --git a/cdoc/KeyShares.cpp b/cdoc/KeyShares.cpp index a0ed7337..d45be6a5 100644 --- a/cdoc/KeyShares.cpp +++ b/cdoc/KeyShares.cpp @@ -476,6 +476,186 @@ validateAuthTicket(CryptoBackend *crypto, const std::string& rcpt_id, return OK; } +namespace { + +// Extract the uncompressed point (0x04 || x || y) of the EC P-256 JWK with +// the given kid from a JWK Set JSON. Returns empty if not found/malformed. +std::vector +jwkEcPoint(const std::string& jwks_json, const std::string& kid) +{ + picojson::value json; + if (!picojson::parse(json, jwks_json).empty() || !json.is()) + return {}; + picojson::value keys = json.get("keys"); + if (!keys.is()) + return {}; + for (const auto& kv : keys.get()) { + if (!kv.is()) + continue; + auto field = [&kv](const char *name) -> std::string { + picojson::value v = kv.get(name); + return v.is() ? v.get() : std::string(); + }; + if (field("kid") != kid) + continue; + if (field("kty") != "EC" || field("crv") != "P-256") + return {}; + std::vector x = fromBase64URL(field("x")); + std::vector y = fromBase64URL(field("y")); + if (x.empty() || y.empty()) + return {}; + std::vector point(1 + x.size() + y.size()); + point[0] = 0x04; + std::copy(x.begin(), x.end(), point.begin() + 1); + std::copy(y.begin(), y.end(), point.begin() + 1 + x.size()); + return point; + } + return {}; +} + +// Signature-Input header: rp-sig=();created=...;keyid="..." +// Returns the parameters part (everything after "rp-sig=") and the keyid. +bool +parseSignatureInput(const std::string& header, std::string& params, std::string& keyid) +{ + if (!header.starts_with("rp-sig=")) + return false; + params = header.substr(7); + auto pos = params.find("keyid=\""); + if (pos == std::string::npos) + return false; + auto end = params.find('"', pos + 7); + if (end == std::string::npos) + return false; + keyid = params.substr(pos + 7, end - pos - 7); + return !keyid.empty(); +} + +// Signature header: rp-sig=:: +std::string +parseSignatureHeader(const std::string& header) +{ + if (!header.starts_with("rp-sig=:") || !header.ends_with(":") || header.size() < 10) + return {}; + return header.substr(8, header.size() - 9); +} + +// RFC9421 section 2.5 signature base for the rp-sig covered components +std::string +buildRpSignatureBase(const std::string& rp_signed_hash, const std::string& rp_name, + const std::string& signature_params) +{ + return "\"x-rp-signed-hash\": " + rp_signed_hash + "\n" + + "\"x-rp-name\": " + rp_name + "\n" + + "\"@signature-params\": " + signature_params; +} + +} // namespace + +libcdoc::result_t +validateRpHttpSignature(const std::map& params, const std::string& rp_jwks, + std::string& error) +{ + auto getParam = [¶ms](const char *name, std::string& dst) -> bool { + auto it = params.find(name); + if (it == params.end() || it->second.empty()) + return false; + dst = it->second; + return true; + }; + std::string rp_signed_hash, rp_name, signature_input, signature; + if (!getParam("x-rp-signed-hash", rp_signed_hash) || + !getParam("x-rp-name", rp_name) || + !getParam("Signature-Input", signature_input) || + !getParam("Signature", signature)) { + error = "Missing RFC9421 signature parameters"; + return DATA_FORMAT_ERROR; + } + std::string sig_params, keyid; + if (!parseSignatureInput(signature_input, sig_params, keyid)) { + error = "Invalid Signature-Input header"; + return DATA_FORMAT_ERROR; + } + // RFC9421 byte sequences use standard base64 + std::vector sig = fromBase64(parseSignatureHeader(signature)); + if (sig.empty()) { + error = "Invalid Signature header"; + return DATA_FORMAT_ERROR; + } + std::vector point = jwkEcPoint(rp_jwks, keyid); + if (point.empty()) { + error = FORMAT("No matching key in RP server JWKS (kid {})", keyid); + return CRYPTO_ERROR; + } + std::string base = buildRpSignatureBase(rp_signed_hash, rp_name, sig_params); + std::vector digest(32); + SHA256(reinterpret_cast(base.data()), base.size(), digest.data()); + if (!Crypto::validateSignatureECPoint(point, digest, sig)) { + error = "RP HTTP signature verification failed"; + return CRYPTO_ERROR; + } + return OK; +} + +libcdoc::result_t +validateAuthTicketMID(CryptoBackend *crypto, const std::string& rcpt_id, + const std::string& ticket, const std::vector& cert_der, + const std::map& params, const std::string& rp_jwks, + std::string& error) +{ + if (!crypto) { + error = "No crypto backend"; + return CryptoBackend::INVALID_PARAMS; + } + // Signing certificate identity must match the container recipient. + if (auto rv = crypto->validateCertificate(rcpt_id, cert_der); rv != OK) { + error = FORMAT("Signing certificate does not match recipient {}", rcpt_id); + return rv; + } + + // The signed part of the ticket JWT is header64.payload64.sig64; the hash + // sent to Mobile-ID is SHA-256 of the signing input. + auto parts = split(ticket, '~'); + if (parts.empty()) { + error = "Invalid ticket"; + return DATA_FORMAT_ERROR; + } + auto jwt_parts = split(parts[0], '.'); + if (jwt_parts.size() != 3) { + error = "Invalid ticket JWT"; + return DATA_FORMAT_ERROR; + } + std::string signing_input = jwt_parts[0] + "." + jwt_parts[1]; + std::vector signature = fromBase64URL(jwt_parts[2]); + if (signature.size() != 64) { + error = "Invalid ticket signature"; + return DATA_FORMAT_ERROR; + } + std::vector digest(32); + SHA256(reinterpret_cast(signing_input.data()), signing_input.size(), digest.data()); + if (!Crypto::validateSignature(cert_der, digest, signature, Crypto::SignatureAlgorithm::ES256)) { + error = "Auth ticket signature verification failed"; + return CRYPTO_ERROR; + } + + // x-rp-signed-hash must be base64(SHA256(ticket signature)): this links + // the RP server's HTTP countersignature to the phone's signature. + auto it = params.find("x-rp-signed-hash"); + if (it == params.end()) { + error = "Missing x-rp-signed-hash"; + return DATA_FORMAT_ERROR; + } + std::vector sig_hash(32); + SHA256(signature.data(), signature.size(), sig_hash.data()); + if (it->second != toBase64(sig_hash)) { + error = "x-rp-signed-hash does not match the ticket signature"; + return CRYPTO_ERROR; + } + + // RP server RFC9421 HTTP countersignature + return validateRpHttpSignature(params, rp_jwks, error); +} + } // namespace libcdoc diff --git a/cdoc/KeyShares.h b/cdoc/KeyShares.h index 4fc29e1a..97cd780c 100644 --- a/cdoc/KeyShares.h +++ b/cdoc/KeyShares.h @@ -269,6 +269,43 @@ result_t validateAuthTicket(CryptoBackend *crypto, const std::string& rcpt_id, const std::string& scheme_name, const std::string& rp_name, std::string& error); +/** + * @brief Validate the RP server's RFC9421 HTTP countersignature (Mobile-ID flow) + * + * Reconstructs the signature base from the rp-sig covered components + * (x-rp-signed-hash, x-rp-name) and verifies the Signature header value with + * the RP server public key selected by keyid from the server JWKS. + * + * @param params the MID signature parameters (HTTP headers from the RP server) + * @param rp_jwks the RP server JWK Set JSON (from /.well-known/jwks.jws) + * @param error output: error description on failure + * @return error code or OK + */ +result_t validateRpHttpSignature(const std::map& params, + const std::string& rp_jwks, std::string& error); + +/** + * @brief Validate a signed Mobile-ID auth ticket client-side (S8) + * + * Checks that the signing certificate belongs to rcpt_id, that the phone's + * ECDSA (ES256) signature verifies over the ticket signing input, that + * x-rp-signed-hash matches the ticket signature, and that the RP server's + * RFC9421 HTTP countersignature verifies. + * + * @param crypto crypto backend + * @param rcpt_id recipient id from the lock (etsi/PNOEE-...) + * @param ticket the auth ticket (jwt~disclosures...) + * @param cert_der signing certificate in DER encoding + * @param params the MID signature parameters (HTTP headers from the RP server) + * @param rp_jwks the RP server JWK Set JSON (from /.well-known/jwks.jws) + * @param error output: error description on failure + * @return error code or OK + */ +result_t validateAuthTicketMID(CryptoBackend *crypto, const std::string& rcpt_id, + const std::string& ticket, const std::vector& cert_der, + const std::map& params, + const std::string& rp_jwks, std::string& error); + } // namespace libcdoc #endif // KEYSHARES_H diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index e573e8ba..0931a2ac 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -872,6 +872,52 @@ libcdoc::NetworkBackend::fetchShare(ShareInfo& share, const std::string& url, co return OK; } +libcdoc::result_t +libcdoc::NetworkBackend::fetchWellKnownKeys(std::string& dst, const std::string& url) +{ + LOG_DBG("Get well-known keys from: {}", url); + + std::string host, path; + int port; + int result = libcdoc::parseURL(url, host, port, path); + if (result != libcdoc::OK) return result; + + httplib::SSLClient cli(host, port); + if (result = applySSLTimeout(cli, this); result != OK) return result; + result = setPeerCertificates(cli, this, buildURL(host, port)); + if (result != OK) return result; + if (result = setProxy(cli, this); result != OK) return result; + + std::string full = path + "/.well-known/jwks.jws"; + httplib::Headers hdrs; + if (httplib::Result rsp = cli.Get(full, hdrs); !rsp) + return NETWORK_ERROR; + else if (rsp->status < 200 || rsp->status >= 300) { + error = FORMAT("Well-known keys request failed with status {}", rsp->status); + LOG_WARN("{}", error); + return NETWORK_ERROR; + } else + dst = std::move(rsp->body); + + // The endpoint name says .jws: accept both a plain JWK Set (what the + // servers currently return) and a JWS compact serialization + // (header64.payload64.signature64) whose payload is the JWK Set. + if (dst.find("\"keys\"") == std::string::npos) { + std::vector parts = split(dst, '.'); + if (parts.size() == 3) { + std::vector payload = fromBase64URL(parts[1]); + dst.assign(payload.begin(), payload.end()); + } + } + if (dst.find("\"keys\"") == std::string::npos) { + error = "Well-known keys response is not a JWK Set"; + LOG_WARN("{}", error); + dst.clear(); + return NETWORK_ERROR; + } + return libcdoc::OK; +} + libcdoc::result_t libcdoc::NetworkBackend::showFeedback(SIDMIDFeedback& feedback) { diff --git a/cdoc/NetworkBackend.h b/cdoc/NetworkBackend.h index 0e7df7ae..2419553e 100644 --- a/cdoc/NetworkBackend.h +++ b/cdoc/NetworkBackend.h @@ -231,6 +231,21 @@ struct CDOC_EXPORT NetworkBackend { */ virtual result_t fetchShare(ShareInfo& share, const std::string& url, const std::string& share_id, const std::string& session_token, const std::string& session_cert, const std::string& auth_token, const std::vector& auth_cert, const std::map& auth_params); + + /** + * @brief Fetch the server's public signing keys (JWKS) + * + * GET /.well-known/jwks.jws and return the JWK Set JSON. The + * response may be a plain JWK Set or a JWS compact serialization whose + * payload is the JWK Set; both are handled. The JWS signature is not + * verified - the pinned TLS channel is the trust anchor (and the keys + * are cross-checked by the share servers). + * + * @param dst a container for the JWK Set JSON + * @param url server url (RP server) + * @return error code or OK + */ + virtual result_t fetchWellKnownKeys(std::string& dst, const std::string& url); #endif /** diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 109b9242..7fe91d66 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -28,6 +28,8 @@ #include #include #include +#include + #include #include #include @@ -1903,3 +1905,137 @@ BOOST_AUTO_TEST_CASE(ValidateSessionDataChecks) } BOOST_AUTO_TEST_SUITE_END() + +// Mobile-ID (MID) ticket validation: ECDSA (ES256) phone signature plus the +// RP server RFC9421 HTTP countersignature. All vectors come from a working +// cdoc-tool Mobile-ID session log (2026-08-07, SK test identity +// PNOEE-51307149560) and the RP server's real well-known JWKS; both +// signatures were independently verified with OpenSSL. +BOOST_AUTO_TEST_SUITE(MidTicketValidation) + +namespace { + +static const char *MID_CERT_B64 = + "MIIDqDCCAy6gAwIBAgIQB9W11BzBABj+0d/AZx6UHzAKBggqhkjOPQQDAjBxMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQg" + "U29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEsMCoGA1UEAwwjVEVTVCBvZiBTSyBJRCBTb2x1dGlvbnMgRUlE" + "LVEgMjAyMUUwHhcNMjQwNjEyMDY0NTI4WhcNMjkwNjE2MDY0NTI3WjCBlTELMAkGA1UEBhMCRUUxLzAtBgNVBAMMJk1BUlkgw4RO" + "TixPJ0NPTk5Fxb0txaBVU0xJSyBURVNUTlVNQkVSMSUwIwYDVQQEDBxPJ0NPTk5Fxb0txaBVU0xJSyBURVNUTlVNQkVSMRIwEAYD" + "VQQqDAlNQVJZIMOETk4xGjAYBgNVBAUTEVBOT0VFLTUxMzA3MTQ5NTYwMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWlV1aVSX" + "w6WhagWmFmXE/oe+0R1xZzrHyoiVlgKpGiJ8cwIQLogRGQnWY7NwgQvRHCBmsl99bj57h7SWnd03m6OCAYEwggF9MAkGA1UdEwQC" + "MAAwHwYDVR0jBBgwFoAUScfc7QYUosdtnKbP11L9aOXoBBQwcAYIKwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5z" + "ay5lZS9URVNUX0VJRC1RXzIwMjFFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5zay5lZS9laWRxMjAyMWUw" + "eAYDVR0gBHEwbzAIBgYEAI96AQIwYwYJKwYBBAHOHxIBMFYwVAYIKwYBBQUHAgEWSGh0dHBzOi8vd3d3LnNraWRzb2x1dGlvbnMu" + "ZXUvcmVzb3VyY2VzL2NlcnRpZmljYXRpb24tcHJhY3RpY2Utc3RhdGVtZW50LzA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5z" + "ay5lZS90ZXN0X2VpZC1xXzIwMjFlLmNybDAdBgNVHQ4EFgQUj8KjnXvGQJCRYOd5LVfPku7QsZwwDgYDVR0PAQH/BAQDAgeAMAoG" + "CCqGSM49BAMCA2gAMGUCMQCocXWDbBnkM3WEyBdv9Vm0A1MNRv08WrR192dRBcX42Kz5oiH0SdHRJv2ffeuEeSwCMEw2tSA3ClJv" + "233Dl7rIYU/T6UG2NQhvDD5FhnP0umZRmVfAUQ6eVcmU8AhFtNJjwg=="; + +static const char *MID_CERT_B64URL = + "MIIDqDCCAy6gAwIBAgIQB9W11BzBABj-0d_AZx6UHzAKBggqhkjOPQQDAjBxMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQg" + "U29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEsMCoGA1UEAwwjVEVTVCBvZiBTSyBJRCBTb2x1dGlvbnMgRUlE" + "LVEgMjAyMUUwHhcNMjQwNjEyMDY0NTI4WhcNMjkwNjE2MDY0NTI3WjCBlTELMAkGA1UEBhMCRUUxLzAtBgNVBAMMJk1BUlkgw4RO" + "TixPJ0NPTk5Fxb0txaBVU0xJSyBURVNUTlVNQkVSMSUwIwYDVQQEDBxPJ0NPTk5Fxb0txaBVU0xJSyBURVNUTlVNQkVSMRIwEAYD" + "VQQqDAlNQVJZIMOETk4xGjAYBgNVBAUTEVBOT0VFLTUxMzA3MTQ5NTYwMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWlV1aVSX" + "w6WhagWmFmXE_oe-0R1xZzrHyoiVlgKpGiJ8cwIQLogRGQnWY7NwgQvRHCBmsl99bj57h7SWnd03m6OCAYEwggF9MAkGA1UdEwQC" + "MAAwHwYDVR0jBBgwFoAUScfc7QYUosdtnKbP11L9aOXoBBQwcAYIKwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5z" + "ay5lZS9URVNUX0VJRC1RXzIwMjFFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5zay5lZS9laWRxMjAyMWUw" + "eAYDVR0gBHEwbzAIBgYEAI96AQIwYwYJKwYBBAHOHxIBMFYwVAYIKwYBBQUHAgEWSGh0dHBzOi8vd3d3LnNraWRzb2x1dGlvbnMu" + "ZXUvcmVzb3VyY2VzL2NlcnRpZmljYXRpb24tcHJhY3RpY2Utc3RhdGVtZW50LzA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5z" + "ay5lZS90ZXN0X2VpZC1xXzIwMjFlLmNybDAdBgNVHQ4EFgQUj8KjnXvGQJCRYOd5LVfPku7QsZwwDgYDVR0PAQH_BAQDAgeAMAoG" + "CCqGSM49BAMCA2gAMGUCMQCocXWDbBnkM3WEyBdv9Vm0A1MNRv08WrR192dRBcX42Kz5oiH0SdHRJv2ffeuEeSwCMEw2tSA3ClJv" + "233Dl7rIYU_T6UG2NQhvDD5FhnP0umZRmVfAUQ6eVcmU8AhFtNJjwg"; + +static const char *MID_TICKET_JWT = + "eyJhbGciOiJFUzI1NiIsInR5cCI6InZuZC5jZG9jMi5hdXRoLXRva2VuLnYxK3NkLWp3dCJ9.eyJfc2QiOlsiWktZWWpYa01WRTB" + "hT2VGV2poUHJxY1d5M1o2dkpkek5qSkpnckVEenBIMCJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6ImV0c2lcL1BOT0VFLTU" + "xMzA3MTQ5NTYwIn0.qrq6MiMCovJOfoDPVmh5tlbKDBQaOpyjzg_IpA635zET1njSnxszqePa79WEmo2GRQ-XtloXlY8aQ5ZL7ys" + "WNA"; + +static const char *RP_JWKS_JSON = R"({"keys":[{"kid":"VFp4bd_XIQJWXT6M2bKaQs_uDBS32WibjycHVd8MQJo","kty":"EC","use":null,"crv":"P-256","x":"H0VsHWVwImGA4uolFRROI5MWsEnVFrOKkFlRsFHRGKQ","y":"kOSoL7uoujqoIgCIn867lq6E-LflpMy6E8fsEYBcYxM","n":null,"e":null,"alg":"ES256"}]})"; + +std::vector midCert() { return libcdoc::fromBase64(MID_CERT_B64); } + +std::string midTicketJwt() { return MID_TICKET_JWT; } + +std::map midParams() +{ + return { + {"x-rp-signed-hash", "hXPUTG2KxbSQSyb8vv2956q0aYta1K9tPZm8EFPJjUU="}, + {"x-rp-name", "DEMO"}, + {"Signature-Input", R"(rp-sig=("x-rp-signed-hash" "x-rp-name");created=1786108129;keyid="VFp4bd_XIQJWXT6M2bKaQs_uDBS32WibjycHVd8MQJo")"}, + {"Signature", "rp-sig=:Mv0mLPa0K1M7T8FzU3ilCZROfs9SqFQOfjIzS8tTEhC7Ih+C+1S1yTjhbVls0m6ViDxOTHBmk/xSvIWX6YQIwA==:"}, + }; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(MidPhoneSignatureVerifies) +{ + // ES256 over SHA256(JWT signing input) with the phone certificate key + // (verified independently with OpenSSL against the log data). + std::string jwt = midTicketJwt(); + auto parts = libcdoc::split(jwt, '.'); + BOOST_REQUIRE_EQUAL(parts.size(), 3); + std::vector sig = libcdoc::fromBase64URL(parts[2]); + BOOST_REQUIRE_EQUAL(sig.size(), 64); + std::string input = parts[0] + "." + parts[1]; + std::vector digest(32); + SHA256(reinterpret_cast(input.data()), input.size(), digest.data()); + BOOST_CHECK(libcdoc::Crypto::validateSignature(midCert(), digest, sig, + libcdoc::Crypto::SignatureAlgorithm::ES256)); + // Tampered digest must not verify. + digest[0] ^= 0x01; + BOOST_CHECK(!libcdoc::Crypto::validateSignature(midCert(), digest, sig, + libcdoc::Crypto::SignatureAlgorithm::ES256)); +} + +BOOST_AUTO_TEST_CASE(RpHttpSignatureVerifies) +{ + std::string err; + // The real log countersignature verifies against the real RP JWKS + // (verified independently with OpenSSL). + BOOST_CHECK_EQUAL(libcdoc::validateRpHttpSignature(midParams(), RP_JWKS_JSON, err), libcdoc::OK); + // Tampered covered component -> verification failure. + auto bad = midParams(); + bad["x-rp-name"] = "EVIL"; + BOOST_CHECK_EQUAL(libcdoc::validateRpHttpSignature(bad, RP_JWKS_JSON, err), libcdoc::CRYPTO_ERROR); + // Tampered signature metadata (created timestamp) -> verification failure. + bad = midParams(); + bad["Signature-Input"] = R"(rp-sig=("x-rp-signed-hash" "x-rp-name");created=1786108128;keyid="VFp4bd_XIQJWXT6M2bKaQs_uDBS32WibjycHVd8MQJo")"; + BOOST_CHECK_EQUAL(libcdoc::validateRpHttpSignature(bad, RP_JWKS_JSON, err), libcdoc::CRYPTO_ERROR); + // Unknown key id. + bad = midParams(); + bad["Signature-Input"] = R"(rp-sig=("x-rp-signed-hash" "x-rp-name");created=1786108129;keyid="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")"; + BOOST_CHECK_EQUAL(libcdoc::validateRpHttpSignature(bad, RP_JWKS_JSON, err), libcdoc::CRYPTO_ERROR); + // Missing headers. + BOOST_CHECK_EQUAL(libcdoc::validateRpHttpSignature({}, RP_JWKS_JSON, err), libcdoc::DATA_FORMAT_ERROR); +} + +BOOST_AUTO_TEST_CASE(ValidateAuthTicketMidEndToEnd) +{ + libcdoc::CryptoBackend crypto; + std::string err; + std::string ticket = midTicketJwt() + "~aud~ZGlzY2xvc3VyZQ"; + BOOST_CHECK_EQUAL(libcdoc::validateAuthTicketMID(&crypto, "etsi/PNOEE-51307149560", ticket, + midCert(), midParams(), RP_JWKS_JSON, err), + libcdoc::OK); + // Wrong recipient. + BOOST_CHECK_EQUAL(libcdoc::validateAuthTicketMID(&crypto, "etsi/PNOEE-30303039903", ticket, + midCert(), midParams(), RP_JWKS_JSON, err), + libcdoc::CRYPTO_ERROR); + // Tampered phone signature (flip a character in the JWT payload changes + // the signing input and therefore the digest). + std::string badTicket = ticket; + badTicket[badTicket.find('.') - 1] = (badTicket[badTicket.find('.') - 1] == 'A') ? 'B' : 'A'; + BOOST_CHECK_EQUAL(libcdoc::validateAuthTicketMID(&crypto, "etsi/PNOEE-51307149560", badTicket, + midCert(), midParams(), RP_JWKS_JSON, err), + libcdoc::CRYPTO_ERROR); + // x-rp-signed-hash not matching the ticket signature. + auto bad = midParams(); + bad["x-rp-signed-hash"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + BOOST_CHECK_EQUAL(libcdoc::validateAuthTicketMID(&crypto, "etsi/PNOEE-51307149560", ticket, + midCert(), bad, RP_JWKS_JSON, err), + libcdoc::CRYPTO_ERROR); +} + +BOOST_AUTO_TEST_SUITE_END() From 36545e68517d5933c4296abf4cc1dd9e494636bb Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 12 Aug 2026 12:26:03 +0300 Subject: [PATCH 62/65] Fixed keyshare KEK creatin, urlencode untrusted path components, log malformed session token --- cdoc/CDoc2Writer.cpp | 7 +++-- cdoc/KeyShares.cpp | 8 ++++- cdoc/NetworkBackend.cpp | 48 ++++++++++++++++++++++++++--- cdoc/Utils.cpp | 20 ++++++++++++ cdoc/Utils.h | 10 ++++++ test/libcdoc_boost.cpp | 68 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 154 insertions(+), 7 deletions(-) diff --git a/cdoc/CDoc2Writer.cpp b/cdoc/CDoc2Writer.cpp index 894824e0..87404114 100644 --- a/cdoc/CDoc2Writer.cpp +++ b/cdoc/CDoc2Writer.cpp @@ -375,8 +375,11 @@ CDoc2Writer::buildHeader(std::vector& header, const std::vector kek_pm = libcdoc::Crypto::extract(key_material_salt, key_material); + // KEK_i_pm = HKDF_Extract(KeyMaterialSalt_i, KeyMaterial_i) + // RFC 5869: HKDF-Extract(salt, IKM); Crypto::extract takes (IKM, salt). + // (S11: the arguments were swapped, deviating from the spec and + // the reference implementation.) + std::vector kek_pm = libcdoc::Crypto::extract(key_material, key_material_salt); libcdoc::Cleanser kek_pm_guard(kek_pm); // KEK_i = HKDF_Expand(KEK_i_pm, "CDOC2kek" + FMKEncryptionMethod + RecipientInfo_i, L) diff --git a/cdoc/KeyShares.cpp b/cdoc/KeyShares.cpp index d45be6a5..ceecb9ad 100644 --- a/cdoc/KeyShares.cpp +++ b/cdoc/KeyShares.cpp @@ -45,7 +45,9 @@ libcdoc::ShareData::getURL() std::string url = base_url; if (!base_url.ends_with('/')) url = url + "/"; - url = url + + "key-shares/" + share_id + "?nonce=" + nonce; + // S12: share_id comes from the (untrusted) container and nonce from the + // share server - percent-encode both before composing the URL. + url = url + "key-shares/" + urlEncodeComponent(share_id) + "?nonce=" + urlEncodeComponent(nonce); LOG_DBG("Share URL: {}", url); return url; } @@ -240,6 +242,10 @@ SessionToken::SessionToken(std::string_view str) for (size_t i = 2; i < parts.size(); i++) { disclosures.push_back(parts[i]); } + } else { + // S10: a token without disclosures can authorize nothing; log it so + // that the resulting "no disclosure" errors are diagnosable. + LOG_WARN("Session token is malformed ({} parts, expected at least 3)", parts.size()); } } diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 0931a2ac..3b140864 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -478,7 +478,8 @@ libcdoc::NetworkBackend::fetchKey (std::vector& dst, const std::string& if (result != OK) return result; if (result = setProxy(cli, this); result != OK) return result; - std::string full = path + "/key-capsules/" + transaction_id; + // S12: transaction_id comes from the (untrusted) container + std::string full = path + "/key-capsules/" + urlEncodeComponent(transaction_id); httplib::Headers hdrs; httplib::Response rsp;; result = get(cli, hdrs, full, rsp); @@ -783,7 +784,17 @@ libcdoc::NetworkBackend::fetchNonce(std::vector& dst, const std::string SessionToken stoken(auth_token); std::string session_token_disclosed = stoken.discloseForUrl(url); - std::string full = path + "/key-shares/" + share_id + "/nonce"; + // S10: never send an empty session token header. A missing disclosure + // means the token is malformed or the server is not authorized for this + // session - fail before making the request. + if (session_token_disclosed.empty()) { + error = FORMAT("Session token has no disclosure for {}", url); + LOG_WARN("{}", error); + return libcdoc::DATA_FORMAT_ERROR; + } + + // S12: share_id comes from the (untrusted) container + std::string full = path + "/key-shares/" + urlEncodeComponent(share_id) + "/nonce"; httplib::Headers hdrs; hdrs.insert({"x-cdoc2-session-token", session_token_disclosed}); hdrs.insert({"x-cdoc2-session-x5c", auth_cert}); @@ -829,7 +840,17 @@ libcdoc::NetworkBackend::fetchShare(ShareInfo& share, const std::string& url, co SessionToken stoken(session_token); std::string session_token_disclosed = stoken.discloseForUrl(url); - std::string full = path + "/key-shares/" + share_id; + // S10: never send an empty session token header. A missing disclosure + // means the token is malformed or the server is not authorized for this + // session - fail before making the request. + if (session_token_disclosed.empty()) { + error = FORMAT("Session token has no disclosure for {}", url); + LOG_WARN("{}", error); + return libcdoc::DATA_FORMAT_ERROR; + } + + // S12: share_id comes from the (untrusted) container + std::string full = path + "/key-shares/" + urlEncodeComponent(share_id); LOG_DBG("Share url: {}", full); httplib::Headers hdrs; hdrs.insert({"x-cdoc2-session-token", session_token_disclosed}); @@ -961,7 +982,8 @@ static result_t waitForResult(SIDMIDResponse& dst, httplib::SSLClient& cli, const std::string& path, const std::string& auth_token_disclosed, const std::string& auth_cert, const std::string& session_id, bool sid, double seconds) { double end = libcdoc::getTime() + seconds; - std::string full = path + session_id; + // S12: session_id comes from the server response + std::string full = path + urlEncodeComponent(session_id); LOG_DBG("SID/MID session query path: {}", full); while (libcdoc::getTime() < end) { httplib::Response rsp; @@ -1097,6 +1119,15 @@ libcdoc::NetworkBackend::signSID(std::vector& dst, std::vector SessionToken stoken(session_token); std::string session_token_disclosed = stoken.discloseForUrl(url); + // S10: never send an empty session token header. A missing disclosure + // means the token is malformed or the server is not authorized for this + // session - fail before making the request. + if (session_token_disclosed.empty()) { + error = FORMAT("Session token has no disclosure for {}", url); + LOG_WARN("{}", error); + return libcdoc::DATA_FORMAT_ERROR; + } + picojson::object sap = { {"hashAlgorithm", picojson::value(hashAlgorithm)} }; @@ -1258,6 +1289,15 @@ libcdoc::NetworkBackend::signMID(std::vector& dst, std::vector SessionToken stoken(session_token); std::string session_token_disclosed = stoken.discloseForUrl(url); + // S10: never send an empty session token header. A missing disclosure + // means the token is malformed or the server is not authorized for this + // session - fail before making the request. + if (session_token_disclosed.empty()) { + error = FORMAT("Session token has no disclosure for {}", url); + LOG_WARN("{}", error); + return libcdoc::DATA_FORMAT_ERROR; + } + LOG_DBG("Starting client: {} {}", host, port); httplib::SSLClient cli(host, port); if (result = applySSLTimeout(cli, this); result != OK) return result; diff --git a/cdoc/Utils.cpp b/cdoc/Utils.cpp index bebd69ee..15184899 100644 --- a/cdoc/Utils.cpp +++ b/cdoc/Utils.cpp @@ -265,6 +265,26 @@ parseEtsiRecipientId(std::string_view rcpt_id) return out; } +std::string +urlEncodeComponent(std::string_view value) +{ + static constexpr char UNRESERVED[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"; + static constexpr char HEX[] = "0123456789ABCDEF"; + std::string out; + out.reserve(value.size()); + for (char c : value) { + if (memchr(UNRESERVED, c, sizeof(UNRESERVED) - 1)) { + out += c; + } else { + out += '%'; + out += HEX[(static_cast(c) >> 4) & 0x0F]; + out += HEX[static_cast(c) & 0x0F]; + } + } + return out; +} + std::string sanitiseExtractedFilename(std::string_view name) { diff --git a/cdoc/Utils.h b/cdoc/Utils.h index 8115ce3d..988b2473 100644 --- a/cdoc/Utils.h +++ b/cdoc/Utils.h @@ -246,6 +246,16 @@ struct urlEncode { friend std::ostream& operator<<(std::ostream& escaped, urlEncode src); }; +/** + * @brief Percent-encode a string for use as a URL path segment or query value + * + * RFC 3986 unreserved characters (A-Z a-z 0-9 - _ . ~) are kept as-is, + * everything else (including space) is percent-encoded. Used to safely + * interpolate untrusted values (share ids, nonces, transaction ids) into + * request URLs (S12). + */ +std::string urlEncodeComponent(std::string_view value); + std::vector toUint8Vector(const auto* data) { return {data->cbegin(), data->cend()}; diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 7fe91d66..69e9b79f 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -1540,6 +1540,19 @@ BOOST_AUTO_TEST_CASE(EmptyOrDisclosurelessTokenFailsClosed) BOOST_CHECK(!twopart.hasDisclosureForUrl("https://share1.example.com")); } +// S10 regression: malformed tokens (fewer than 3 parts) must disclose +// nothing; callers treat an empty disclosure as a hard error and never send +// an empty x-cdoc2-session-token header. +BOOST_AUTO_TEST_CASE(MalformedTokenDisclosesNothing) +{ + libcdoc::SessionToken empty(""); + BOOST_CHECK(empty.discloseForUrl("https://share1.example.com").empty()); + libcdoc::SessionToken bare("jwt"); + BOOST_CHECK(bare.discloseForUrl("https://share1.example.com").empty()); + libcdoc::SessionToken twopart("jwt~aud"); + BOOST_CHECK(twopart.discloseForUrl("https://share1.example.com").empty()); +} + BOOST_AUTO_TEST_SUITE_END() // Regression coverage for the constant-time PKCS#1 v1.5 unpadding used by @@ -1706,6 +1719,61 @@ BOOST_AUTO_TEST_CASE(TwoServersPassTheCountCheck) BOOST_AUTO_TEST_SUITE_END() +// S12 regression: untrusted values (container share ids, server nonces) +// must be percent-encoded before being composed into request URLs. +BOOST_AUTO_TEST_SUITE(UrlEncoding) + +BOOST_AUTO_TEST_CASE(ComponentEncoding) +{ + // Unreserved characters pass through unchanged. + BOOST_CHECK_EQUAL(libcdoc::urlEncodeComponent("abcXYZ019-_.~"), "abcXYZ019-_.~"); + // Typical share ids (hex) and nonces (base64url) are unchanged. + BOOST_CHECK_EQUAL(libcdoc::urlEncodeComponent("a9e41c78982fc2079e7966ae885c1434"), + "a9e41c78982fc2079e7966ae885c1434"); + BOOST_CHECK_EQUAL(libcdoc::urlEncodeComponent("xESQowIVG_5riudd-NBUpQ"), "xESQowIVG_5riudd-NBUpQ"); + // Reserved and dangerous characters are percent-encoded. + BOOST_CHECK_EQUAL(libcdoc::urlEncodeComponent("../admin"), "..%2Fadmin"); + BOOST_CHECK_EQUAL(libcdoc::urlEncodeComponent("x?y=1&z=2"), "x%3Fy%3D1%26z%3D2"); + BOOST_CHECK_EQUAL(libcdoc::urlEncodeComponent("a b"), "a%20b"); + BOOST_CHECK_EQUAL(libcdoc::urlEncodeComponent("100%"), "100%25"); + BOOST_CHECK_EQUAL(libcdoc::urlEncodeComponent("x#y"), "x%23y"); + // Empty input. + BOOST_CHECK_EQUAL(libcdoc::urlEncodeComponent(""), ""); + // Non-ASCII bytes are percent-encoded per byte (UTF-8 'รค'). + BOOST_CHECK_EQUAL(libcdoc::urlEncodeComponent("\xc3\xa4"), "%C3%A4"); +} + +BOOST_AUTO_TEST_CASE(ShareUrlEncodesUntrustedParts) +{ + libcdoc::ShareData share("https://shares.example.com/", "1a81/../admin"); + share.nonce = "no nce+here"; + BOOST_CHECK_EQUAL(share.getURL(), + "https://shares.example.com/key-shares/1a81%2F..%2Fadmin?nonce=no%20nce%2Bhere"); +} + +BOOST_AUTO_TEST_SUITE_END() + +// S11 regression: Crypto::extract must implement HKDF-Extract(salt, IKM) +// with the arguments in (IKM, salt) order. The keyshare KEK derivation +// depends on this convention (spec: KEK_i_pm = HKDF_Extract(KeyMaterialSalt_i, +// KeyMaterial_i)). +BOOST_AUTO_TEST_SUITE(HkdfExtractConvention) + +BOOST_AUTO_TEST_CASE(Rfc5869TestCase1) +{ + // RFC 5869, Test Case 1 (SHA-256) + std::vector ikm(22, 0x0b); + std::vector salt {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c}; + std::vector prk = libcdoc::Crypto::extract(ikm, salt); + BOOST_CHECK_EQUAL(libcdoc::toHex(prk), + "077709362C2E32DF0DDC3F0DC47BBA6390B6C73BB50F9C3122EC844AD7C2B3E5"); + // Swapped arguments must give a different result (guards the convention). + std::vector swapped = libcdoc::Crypto::extract(salt, ikm); + BOOST_CHECK(prk != swapped); +} + +BOOST_AUTO_TEST_SUITE_END() + // S8 regression: client-side validation of Smart-ID (ACSP_V2) auth tickets. // All vectors come from a cdoc-tool Smart-ID session log (2026-08-06) using a // Smart-ID TEST identity (serialNumber PNOEE-30303039903) - no real PII. From 8492680f9bd53b7b4ab81172e11ce417bb78911a Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Thu, 13 Aug 2026 00:00:59 +0300 Subject: [PATCH 63/65] Verify shares recipient and vc code --- cdoc/CDoc2Reader.cpp | 10 ++++----- cdoc/NetworkBackend.cpp | 12 ++++++++++- cdoc/Recipient.cpp | 7 +++++-- cdoc/Recipient.h | 3 ++- cdoc/Utils.h | 24 +++++++++++++++++++++ test/libcdoc_boost.cpp | 46 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 92 insertions(+), 10 deletions(-) diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index ca1a4d79..43752d65 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -270,14 +270,12 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) } // auth_url = "https://cdoc2-auth.dev.riaint.ee"; // fixme: - std::string signer = conf->getValue(Configuration::SHARE_SIGNER); - LOG_DBG("Signer: {}", signer); + std::string signer_type = conf->getValue(Configuration::SHARE_SIGNER); + LOG_DBG("Signer: {}", signer_type); bool mid = false; - if (signer == Configuration::SHARE_SIGNER_SID) { - // - } else if (signer == Configuration::SHARE_SIGNER_MID) { + if (signer_type == Configuration::SHARE_SIGNER_MID) { mid = true; - } else { + } else if (signer_type != Configuration::SHARE_SIGNER_SID) { setLastError(t_("Unknown or missing signer type")); LOG_ERROR("Unknown or missing signer type"); return libcdoc::CONFIGURATION_ERROR; diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 3b140864..875615c2 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -709,8 +709,18 @@ libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std if (result != libcdoc::OK) return NETWORK_ERROR; LOG_DBG("Verification code: {}", ver_code); + // S16: the verification code is the user's consent anchor - a malformed + // server value must never be rendered as 0 or garbage. Smart-ID/Mobile-ID + // numeric4 codes are 0000-9999. + int vc = 0; + if (!libcdoc::parseBoundedUInt(ver_code, 9999, vc)) { + error = FORMAT("Invalid verification code in response: {}", ver_code); + LOG_ERROR("{}", error); + return NETWORK_ERROR; + } + SIDMIDFeedback fb = { - .code = (int) std::strtold(ver_code.c_str(), nullptr), + .code = vc, }; result = showFeedback(fb); if (result != OK) { diff --git a/cdoc/Recipient.cpp b/cdoc/Recipient.cpp index 0274dc70..2761d49b 100644 --- a/cdoc/Recipient.cpp +++ b/cdoc/Recipient.cpp @@ -239,8 +239,11 @@ Recipient::validate() const return !rcpt_key.empty(); #ifdef HAS_KEYSHARES case KEYSHARE: - // Server ID and recipient ID should not be empty - return !server_id.empty() && !id.empty(); + // S13: the recipient id must be a valid ETSI semantics identifier + // (PNO-, stored without the "etsi/" prefix). A malformed + // id would only fail late (at the share server) or, worse, bind the + // shares to a wrong identity. + return !server_id.empty() && libcdoc::parseEtsiRecipientId("etsi/" + id).valid(); #endif default: return false; diff --git a/cdoc/Recipient.h b/cdoc/Recipient.h index 2ac443b1..3a7b93f5 100644 --- a/cdoc/Recipient.h +++ b/cdoc/Recipient.h @@ -201,7 +201,8 @@ struct CDOC_EXPORT Recipient { * * @param label the label text * @param server_id the id of share server group - * @param recipient_id the recipient id (PNOEE-01234567890) + * @param recipient_id the recipient id (PNOEE-01234567890, without the "etsi/" + * prefix; validated by validate()) * @return Recipient a new Recipient structure */ static Recipient makeShare(std::string label, std::string server_id, std::string recipient_id); diff --git a/cdoc/Utils.h b/cdoc/Utils.h index 988b2473..d1f27ccc 100644 --- a/cdoc/Utils.h +++ b/cdoc/Utils.h @@ -86,6 +86,30 @@ static constexpr bool fromHex(auto pos, auto end, auto& val) return std::from_chars(p, p + 2, val, 16).ec == std::errc{}; } +/** + * @brief Parse a bounded non-negative decimal integer + * + * Reports failure explicitly: the whole string must be digits and the value + * must fit in [0, max_value]. Used for security-relevant numeric fields + * (e.g. the authentication verification code) where a malformed server + * value must never silently render as 0 (S16). + * + * @param str string to parse + * @param max_value maximum accepted value (inclusive) + * @param out parsed value on success + * @return true on success + */ +inline bool +parseBoundedUInt(std::string_view str, int max_value, int& out) +{ + int value = -1; + auto res = std::from_chars(str.data(), str.data() + str.size(), value); + if (res.ec != std::errc() || res.ptr != str.data() + str.size() || value < 0 || value > max_value) + return false; + out = value; + return true; +} + static std::vector fromHex(std::string_view hex) { std::vector val; diff --git a/test/libcdoc_boost.cpp b/test/libcdoc_boost.cpp index 69e9b79f..dd6dedd6 100644 --- a/test/libcdoc_boost.cpp +++ b/test/libcdoc_boost.cpp @@ -1719,6 +1719,52 @@ BOOST_AUTO_TEST_CASE(TwoServersPassTheCountCheck) BOOST_AUTO_TEST_SUITE_END() +// S13 regression: keyshare recipient ids must be valid ETSI semantics +// identifiers - malformed ids must fail at encryption time, not at the +// share server. +BOOST_AUTO_TEST_SUITE(KeyShareRecipientValidation) + +BOOST_AUTO_TEST_CASE(RecipientIdValidated) +{ + // Valid Estonian and Lithuanian personal codes. + BOOST_CHECK(libcdoc::Recipient::makeShare("label", "server1", "PNOEE-30303039903").validate()); + BOOST_CHECK(libcdoc::Recipient::makeShare("label", "server1", "PNOLT-30303039903").validate()); + // Malformed ids are rejected. + BOOST_CHECK(!libcdoc::Recipient::makeShare("label", "server1", "").validate()); + BOOST_CHECK(!libcdoc::Recipient::makeShare("label", "server1", "30303039903").validate()); + BOOST_CHECK(!libcdoc::Recipient::makeShare("label", "server1", "PNOEE-3030303990A").validate()); + BOOST_CHECK(!libcdoc::Recipient::makeShare("label", "server1", "PNO-30303039903").validate()); + BOOST_CHECK(!libcdoc::Recipient::makeShare("label", "server1", "PNOEE-30303039903/../../x").validate()); + // Missing server id is still rejected. + BOOST_CHECK(!libcdoc::Recipient::makeShare("label", "", "PNOEE-30303039903").validate()); +} + +BOOST_AUTO_TEST_SUITE_END() + +// S16 regression: the authentication verification code is the user's +// consent anchor - a malformed server value must never render as 0. +BOOST_AUTO_TEST_SUITE(BoundedUIntParsing) + +BOOST_AUTO_TEST_CASE(ParseBoundedUInt) +{ + int out = -1; + // Valid codes (Smart-ID/Mobile-ID numeric4 range 0000-9999). + BOOST_CHECK(libcdoc::parseBoundedUInt("6434", 9999, out) && out == 6434); + BOOST_CHECK(libcdoc::parseBoundedUInt("0000", 9999, out) && out == 0); + BOOST_CHECK(libcdoc::parseBoundedUInt("9999", 9999, out) && out == 9999); + // Malformed or out-of-range values are rejected (previously these + // rendered as verification code 0 via unchecked strtold). + BOOST_CHECK(!libcdoc::parseBoundedUInt("10000", 9999, out)); + BOOST_CHECK(!libcdoc::parseBoundedUInt("abc", 9999, out)); + BOOST_CHECK(!libcdoc::parseBoundedUInt("12x", 9999, out)); + BOOST_CHECK(!libcdoc::parseBoundedUInt("", 9999, out)); + BOOST_CHECK(!libcdoc::parseBoundedUInt("-1", 9999, out)); + BOOST_CHECK(!libcdoc::parseBoundedUInt(" 42", 9999, out)); + BOOST_CHECK(!libcdoc::parseBoundedUInt("42 ", 9999, out)); +} + +BOOST_AUTO_TEST_SUITE_END() + // S12 regression: untrusted values (container share ids, server nonces) // must be percent-encoded before being composed into request URLs. BOOST_AUTO_TEST_SUITE(UrlEncoding) From 9b01b40796e542d53bf2afc7d173b6c6abc01912 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 14 Aug 2026 12:03:47 +0300 Subject: [PATCH 64/65] Made SessionData public --- cdoc/CDoc2Reader.cpp | 4 ++-- cdoc/CMakeLists.txt | 3 ++- cdoc/KeyShares.h | 19 ++++--------------- cdoc/NetworkBackend.cpp | 6 +++--- cdoc/NetworkBackend.h | 13 ++++++++++++- libcdoc.i | 2 ++ 6 files changed, 25 insertions(+), 22 deletions(-) diff --git a/cdoc/CDoc2Reader.cpp b/cdoc/CDoc2Reader.cpp index 43752d65..8761fc6d 100644 --- a/cdoc/CDoc2Reader.cpp +++ b/cdoc/CDoc2Reader.cpp @@ -290,8 +290,8 @@ CDoc2Reader::getFMK(std::vector& fmk, unsigned int lock_idx) } } - SessionData session; - if (auto rv = network->authenticateForShares(auth_url, rcpt_id, phone, session.token, session.cert); rv != OK) { + NetworkBackend::SessionData session; + if (auto rv = network->authenticateForShares(auth_url, rcpt_id, phone, session); rv != OK) { setLastError(network->getLastErrorStr(rv)); LOG_ERROR("{}", last_error); return rv; diff --git a/cdoc/CMakeLists.txt b/cdoc/CMakeLists.txt index d82d3c4f..ffafa9bf 100644 --- a/cdoc/CMakeLists.txt +++ b/cdoc/CMakeLists.txt @@ -160,8 +160,9 @@ if(SWIG_FOUND) set_target_properties(cdoc_java PROPERTIES INSTALL_RPATH $<$:/Library/Frameworks> SWIG_COMPILE_DEFINITIONS $<$:SWIGWIN> + SWIG_COMPILE_DEFINITIONS HAS_KEYSHARES ) - #install(TARGETS cdoc_java DESTINATION $,/Library/Java/Extensions,${CMAKE_INSTALL_LIBDIR}>) # FIXME: build mac packages + target_compile_definitions(cdoc_java PRIVATE HAS_KEYSHARES) install(TARGETS cdoc_java DESTINATION ${CMAKE_INSTALL_LIBDIR}) install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/java/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/ee/ria/cdoc FILES_MATCHING PATTERN "*.java") if(WIN32) diff --git a/cdoc/KeyShares.h b/cdoc/KeyShares.h index 97cd780c..6b650121 100644 --- a/cdoc/KeyShares.h +++ b/cdoc/KeyShares.h @@ -53,17 +53,6 @@ struct ShareData { std::string getURL(); }; -/** - * @brief Session data - * - * The session token and certificate provided by AUTH server - * - */ -struct SessionData { - std::string token; - std::string cert; -}; - /** * @brief Authentication data for share tickets * @@ -104,7 +93,7 @@ struct Signer { * @brief Full session token * */ - const SessionData& session; + const NetworkBackend::SessionData& session; /** * @brief Signing algorithm name (RS256/ES256) * @@ -135,7 +124,7 @@ struct Signer { * @param _rcpt_id Recipient full id in etsi format (ets/PNOEE-XYZXYZXYZXY) * @param _algo_name Signing algorithm name (RS256/ES256) */ - Signer(const SessionData& _session, const std::string& _rcpt_id, const std::string& _algo_name, NetworkBackend *_network) : session(_session), rcpt_id(_rcpt_id), algo_name(_algo_name), network(_network) {} + Signer(const NetworkBackend::SessionData& _session, const std::string& _rcpt_id, const std::string& _algo_name, NetworkBackend *_network) : session(_session), rcpt_id(_rcpt_id), algo_name(_algo_name), network(_network) {} }; /** @@ -156,7 +145,7 @@ struct SIDSigner : public Signer { * @param _session Full session data (token and certificate) * @param _rcpt_id Recipient full id in etsi format (ets/PNOEE-XYZXYZXYZXY) */ - SIDSigner(const std::string& _url, const SessionData& _session, const std::string& _rcpt_id, NetworkBackend *network) + SIDSigner(const std::string& _url, const NetworkBackend::SessionData& _session, const std::string& _rcpt_id, NetworkBackend *network) : Signer(_session, _rcpt_id, "RSASSA-PSS+ACSP_V2", network), url(_url) {} result_t signDigest(std::vector& dst, const std::vector& digest) final; @@ -183,7 +172,7 @@ struct MIDSigner : public Signer { * @param _url Mobile ID gateway url * @param _rcpt_id Recipient full id in etsi format (ets/PNOEE-XYZXYZXYZXY) */ - MIDSigner(const std::string& _url, const std::string& _phone, const SessionData& _session, const std::string& _rcpt_id, NetworkBackend *network) + MIDSigner(const std::string& _url, const std::string& _phone, const NetworkBackend::SessionData& _session, const std::string& _rcpt_id, NetworkBackend *network) : Signer(_session, _rcpt_id, "ES256", network), url(_url), phone(_phone) {} result_t signDigest(std::vector& dst, const std::vector& digest) final; diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index 875615c2..cdaaa814 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -638,7 +638,7 @@ waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& } libcdoc::result_t -libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std::string& rcpt_id, const std::string& phone, std::string& token, std::string& cert) +libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std::string& rcpt_id, const std::string& phone, SessionData& session) { // Start authentication std::string host, path; @@ -734,7 +734,7 @@ libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std result = waitForAuthResult(auth_rsp, cli, path + "/auth/status/", location, 60); if (result != OK) return result; - cert = auth_rsp.cert; + session.cert = auth_rsp.cert; auto parts = split(auth_rsp.sessionToken, '~'); // In minimum we need JWT, AUD, RP disclosure and 2 share disclosures @@ -754,7 +754,7 @@ libcdoc::NetworkBackend::authenticateForShares(const std::string& url, const std } } - token = auth_rsp.sessionToken; + session.token = auth_rsp.sessionToken; auto decoded = decodeTicket(jwt); LOG_TRACE("Session token: {}", decoded); diff --git a/cdoc/NetworkBackend.h b/cdoc/NetworkBackend.h index 2419553e..57e1ed9f 100644 --- a/cdoc/NetworkBackend.h +++ b/cdoc/NetworkBackend.h @@ -197,6 +197,17 @@ struct CDOC_EXPORT NetworkBackend { const std::string HDR_SIGNATURE_INPUT = "Signature-Input"; const std::string HDR_SIGNATURE = "Signature"; + /** + * @brief Session data + * + * The session token and certificate provided by AUTH server + * + */ + struct SessionData { + std::string token; + std::string cert; + }; + /** * @brief Get a session token and certificate for share authentication * @@ -210,7 +221,7 @@ struct CDOC_EXPORT NetworkBackend { * @param cert Output parameter for session certificate * @return Error code or OK */ - virtual result_t authenticateForShares(const std::string& url, const std::string& rcpt_id, const std::string& phone, std::string& token, std::string& cert); + virtual result_t authenticateForShares(const std::string& url, const std::string& rcpt_id, const std::string& phone, SessionData& session); /** * @brief fetch authentication nonce from share server diff --git a/libcdoc.i b/libcdoc.i index 6bc01d3f..9ddaf133 100644 --- a/libcdoc.i +++ b/libcdoc.i @@ -38,6 +38,7 @@ // Handle standard C++ types %include "std_string.i" %include "std_vector.i" +%include "std_map.i" %include "typemaps.i" @@ -517,6 +518,7 @@ static std::vector SWIG_JavaArrayToVectorUnsignedChar(JNIEnv *jen %typemap(javaout) std::map { return $jnicall; } +%typemap(javain) std::map "$javainput" // // std::vector> <- CertificateList From 08b5d4444f763215839e116ab4a6f572fa1d6625 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 14 Aug 2026 13:37:24 +0300 Subject: [PATCH 65/65] Added tests for MID/SID (not automatic) --- cdoc/NetworkBackend.cpp | 9 ++ libcdoc.i | 81 +++++++++++++++ test/CMakeLists.txt | 9 ++ test/libcdoc_live_boost.cpp | 199 ++++++++++++++++++++++++++++++++++++ 4 files changed, 298 insertions(+) create mode 100644 test/libcdoc_live_boost.cpp diff --git a/cdoc/NetworkBackend.cpp b/cdoc/NetworkBackend.cpp index cdaaa814..b462d239 100644 --- a/cdoc/NetworkBackend.cpp +++ b/cdoc/NetworkBackend.cpp @@ -572,6 +572,11 @@ static result_t waitForAuthResult(AuthResponse& dst, httplib::SSLClient& cli, const std::string& path, const std::string& auth_proc_uuid, double seconds) { httplib::Headers hdrs; + // Polling may take tens of seconds while the user approves the request; + // the server closes idle keep-alive connections (Connection: close), and + // reusing a dead socket fails with "Cannot connect". Open a fresh + // connection for each poll instead. + cli.set_keep_alive(false); double end = getTime() + seconds; std::string full = path + auth_proc_uuid; @@ -991,6 +996,10 @@ namespace libcdoc { static result_t waitForResult(SIDMIDResponse& dst, httplib::SSLClient& cli, const std::string& path, const std::string& auth_token_disclosed, const std::string& auth_cert, const std::string& session_id, bool sid, double seconds) { + // Same rationale as in waitForAuthResult: long user-approval waits make + // pooled keep-alive sockets go stale; poll on fresh connections. + cli.set_keep_alive(false); + double end = libcdoc::getTime() + seconds; // S12: session_id comes from the server response std::string full = path + urlEncodeComponent(session_id); diff --git a/libcdoc.i b/libcdoc.i index 9ddaf133..6eda9b1d 100644 --- a/libcdoc.i +++ b/libcdoc.i @@ -520,6 +520,86 @@ static std::vector SWIG_JavaArrayToVectorUnsignedChar(JNIEnv *jen } %typemap(javain) std::map "$javainput" +// +// std::map& (method arguments) <-> java.util.Map +// +// fetchShare/signSID/signMID take the signature parameters as a string map. +// The in-direction converts a Java Map to a temporary C++ map (read-only, +// the convention for these parameters). The directorin-direction converts +// C++ -> Java for upcalls into Java NetworkBackend implementations. +// + +%fragment("SWIG_JavaMapToStringMap", "header") { +static std::map SWIG_JavaMapToStringMap(JNIEnv *jenv, jobject jmap) { + std::map result; + if (!jmap) + return result; + jclass map_class = jenv->FindClass("java/util/Map"); + jmethodID entry_set_mid = jenv->GetMethodID(map_class, "entrySet", "()Ljava/util/Set;"); + jobject entry_set = jenv->CallObjectMethod(jmap, entry_set_mid); + jclass set_class = jenv->FindClass("java/util/Set"); + jmethodID iterator_mid = jenv->GetMethodID(set_class, "iterator", "()Ljava/util/Iterator;"); + jobject iterator = jenv->CallObjectMethod(entry_set, iterator_mid); + jclass iterator_class = jenv->FindClass("java/util/Iterator"); + jmethodID has_next_mid = jenv->GetMethodID(iterator_class, "hasNext", "()Z"); + jmethodID next_mid = jenv->GetMethodID(iterator_class, "next", "()Ljava/lang/Object;"); + jclass entry_class = jenv->FindClass("java/util/Map$Entry"); + jmethodID get_key_mid = jenv->GetMethodID(entry_class, "getKey", "()Ljava/lang/Object;"); + jmethodID get_value_mid = jenv->GetMethodID(entry_class, "getValue", "()Ljava/lang/Object;"); + while (jenv->CallBooleanMethod(iterator, has_next_mid)) { + jobject entry = jenv->CallObjectMethod(iterator, next_mid); + jstring jkey = (jstring) jenv->CallObjectMethod(entry, get_key_mid); + jstring jval = (jstring) jenv->CallObjectMethod(entry, get_value_mid); + const char *key_chars = jenv->GetStringUTFChars(jkey, nullptr); + std::string key(key_chars ? key_chars : ""); + if (key_chars) jenv->ReleaseStringUTFChars(jkey, key_chars); + const char *val_chars = jenv->GetStringUTFChars(jval, nullptr); + std::string val(val_chars ? val_chars : ""); + if (val_chars) jenv->ReleaseStringUTFChars(jval, val_chars); + result.emplace(std::move(key), std::move(val)); + jenv->DeleteLocalRef(entry); + jenv->DeleteLocalRef(jkey); + jenv->DeleteLocalRef(jval); + } + jenv->DeleteLocalRef(entry_set); + jenv->DeleteLocalRef(iterator); + return result; +}} + +%fragment("SWIG_StringMapToJavaMap", "header") { +static jobject SWIG_StringMapToJavaMap(JNIEnv *jenv, const std::map &data) { + jclass map_class = jenv->FindClass("java/util/HashMap"); + jmethodID mid_new = jenv->GetMethodID(map_class, "", "()V"); + jmethodID mid_put = jenv->GetMethodID(map_class, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); + jobject jmap = jenv->NewObject(map_class, mid_new); + for (const auto &pair : data) { + jstring key = jenv->NewStringUTF(pair.first.c_str()); + jstring val = jenv->NewStringUTF(pair.second.c_str()); + jenv->CallObjectMethod(jmap, mid_put, key, val); + jenv->DeleteLocalRef(key); + jenv->DeleteLocalRef(val); + } + return jmap; +}} + +// in: Java Map -> temporary C++ map (input-only parameters) +%typemap(in, fragment="SWIG_JavaMapToStringMap") std::map&, + const std::map& %{ + std::map $1_map = SWIG_JavaMapToStringMap(jenv, $input); + $1 = &$1_map; +%} +%typemap(jni) std::map&, const std::map& "jobject" +%typemap(jtype) std::map&, const std::map& "java.util.Map" +%typemap(jstype) std::map&, const std::map& "java.util.Map" +%typemap(javain) std::map&, const std::map& "$javainput" + +// directorin: C++ map -> Java Map (upcall into a Java NetworkBackend) +%typemap(directorin, descriptor="Ljava/util/Map;", fragment="SWIG_StringMapToJavaMap") + std::map&, const std::map& %{ + $input = SWIG_StringMapToJavaMap(jenv, $1); +%} +%typemap(javadirectorin) std::map&, const std::map& "$jniinput" + // // std::vector> <- CertificateList // @@ -667,6 +747,7 @@ static std::vector SWIG_JavaArrayToVectorUnsignedChar(JNIEnv *jen %} %typemap(javaimports) libcdoc::NetworkBackend %{ import java.util.ArrayList; + import java.util.Map; %} #endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 07cb77bc..9bac7373 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,5 +1,6 @@ add_executable(unittests libcdoc_boost.cpp + libcdoc_live_boost.cpp ../cdoc/Crypto.cpp ../cdoc/KeyShares.cpp ../cdoc/Tar.cpp @@ -9,6 +10,14 @@ add_executable(unittests target_compile_definitions(unittests PRIVATE HAS_KEYSHARES) +# The live SID/MID tests (libcdoc_live_boost.cpp) run against the RIA test +# servers with TLS certificate checks disabled until the RIA cert-pinning +# infrastructure is ready - propagate the flag so the test skips cleanly +# in default builds. +if(LIBCDOC_ALLOW_INSECURE_TLS) + target_compile_definitions(unittests PRIVATE LIBCDOC_ALLOW_INSECURE_TLS) +endif() + target_link_libraries(unittests OpenSSL::SSL LibXml2::LibXml2 diff --git a/test/libcdoc_live_boost.cpp b/test/libcdoc_live_boost.cpp new file mode 100644 index 00000000..d4fa5bf8 --- /dev/null +++ b/test/libcdoc_live_boost.cpp @@ -0,0 +1,199 @@ +/* + * libcdoc + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +// +// Live integration tests for the keyshare (Smart-ID / Mobile-ID) flows. +// +// DISABLED BY DEFAULT: they require VPN connectivity to the RIA test +// environment and use the SK automated test identities (which approve +// requests automatically after a few seconds). +// +// Enable with: +// LIBCDOC_LIVE_TESTS=1 ./build/.../test/unittests --run_test=LiveSidMid +// +// The RIA certificate-pinning infrastructure is still in development, so the +// tests currently require a build with LIBCDOC_ALLOW_INSECURE_TLS=ON (TLS +// certificate checks disabled); without it they are skipped. +// + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace std::string_literals; + +namespace utf = boost::unit_test; + +namespace { + +// The RIA test servers (default). The dev environment can be selected via +// environment variables, e.g.: +// LIBCDOC_LIVE_SHARE_SERVERS="https://cdoc2-shares.dev.riaint.ee,https://cdoc2-sharesexternal.dev.riaint.ee" \ +// LIBCDOC_LIVE_AUTH_SERVER=https://cdoc2-auth.dev.riaint.ee \ +// LIBCDOC_LIVE_RP_SERVER=https://cdoc2-rp.dev.riaint.ee \ +// LIBCDOC_LIVE_TESTS=1 ./unittests --run_test=LiveSidMid +std::string +envOr(const char *name, const char *fallback) +{ + const char *v = std::getenv(name); + return (v && *v) ? v : fallback; +} + +const std::string SHARE_SERVERS = envOr("LIBCDOC_LIVE_SHARE_SERVERS", + "https://cdoc2-shares.test.riaint.ee,https://cdoc2-sharesexternal.test.riaint.ee"); +const std::string AUTH_SERVER = envOr("LIBCDOC_LIVE_AUTH_SERVER", "https://cdoc2-auth.test.riaint.ee"); +const std::string RP_SERVER = envOr("LIBCDOC_LIVE_RP_SERVER", "https://cdoc2-rp.test.riaint.ee"); + +constexpr const char *SERVER_ID = "test-shares"; + +// SK automated test identities +constexpr const char *SID_PNO = "30303039903"; // Smart-ID test identity +constexpr const char *MID_PNO = "51307149560"; // Mobile-ID test identity +constexpr const char *MID_PHONE = "+37269930366"; // Mobile-ID test phone number + +bool +liveTestsEnabled() +{ + std::string_view val = std::getenv("LIBCDOC_LIVE_TESTS") ? std::getenv("LIBCDOC_LIVE_TESTS") : ""; + return !val.empty() && val != "0"; +} + +// A network backend that does not pin peer certificates - relies on a +// LIBCDOC_ALLOW_INSECURE_TLS build until the RIA pinning infrastructure is +// ready. +struct LiveNetworkBackend : public libcdoc::NetworkBackend { + libcdoc::result_t getPeerTLSCertificates(std::vector> &dst, const std::string &url) override + { + dst.clear(); + return libcdoc::OK; + } + + libcdoc::result_t showFeedback(SIDMIDFeedback& feedback) override + { + std::cout << "[LIVE] Verification code: " << feedback.code << std::endl; + return libcdoc::OK; + } +}; + +void +sidMidRoundtrip(const std::string& pno, const std::string& phone) +{ + if (!liveTestsEnabled()) { + BOOST_TEST_MESSAGE("Live SID/MID tests are disabled (set LIBCDOC_LIVE_TESTS=1 to enable)"); + return; + } +#ifndef LIBCDOC_ALLOW_INSECURE_TLS + BOOST_TEST_MESSAGE("Live SID/MID tests require a LIBCDOC_ALLOW_INSECURE_TLS=ON build"); + BOOST_FAIL("LIBCDOC_ALLOW_INSECURE_TLS is not enabled in this build"); +#endif + + const std::string payload_str = "Live keyshare roundtrip test payload\n"; + const std::vector payload(payload_str.cbegin(), payload_str.cend()); + + libcdoc::ToolConf conf; + conf.servers.push_back({SERVER_ID, SHARE_SERVERS}); + conf.auth_server = AUTH_SERVER; + conf.rp_server = RP_SERVER; + conf.phone = phone; // ToolConf: empty phone -> SID, non-empty -> MID + + libcdoc::CryptoBackend crypto; + LiveNetworkBackend network; + + // + // Encrypt + // + std::vector container; + libcdoc::VectorConsumer consumer(container); + std::unique_ptr wrt( + libcdoc::CDocWriter::createWriter(2, &consumer, false, &conf, &crypto, &network)); + BOOST_REQUIRE(wrt != nullptr); + + libcdoc::Recipient rcpt = libcdoc::Recipient::makeShare("Live test", SERVER_ID, "PNOEE-" + pno); + BOOST_REQUIRE(wrt->addRecipient(rcpt) == libcdoc::OK); + BOOST_REQUIRE(wrt->beginEncryption() == libcdoc::OK); + BOOST_REQUIRE(wrt->addFile("test.txt", payload.size()) == libcdoc::OK); + BOOST_REQUIRE(wrt->writeData(payload.data(), payload.size()) == libcdoc::OK); + BOOST_REQUIRE(wrt->finishEncryption() == libcdoc::OK); + BOOST_REQUIRE(!container.empty()); + + // + // Decrypt + // + libcdoc::VectorSource src(container); + std::unique_ptr rdr( + libcdoc::CDocReader::createReader(&src, false, &conf, &crypto, &network)); + BOOST_REQUIRE(rdr != nullptr); + + // Find the share server lock + const std::vector& locks = rdr->getLocks(); + unsigned int lock_idx = locks.size(); + for (size_t i = 0; i < locks.size(); i++) { + if (locks[i].type == libcdoc::Lock::Type::SHARE_SERVER) + lock_idx = i; + } + BOOST_REQUIRE(lock_idx < locks.size()); + + // getFMK runs the full SID/MID flow (auth session, nonce, signing, shares) + std::vector fmk; + BOOST_REQUIRE_EQUAL(rdr->getFMK(fmk, lock_idx), libcdoc::OK); + libcdoc::Cleanser fmk_guard(fmk); + + BOOST_REQUIRE_EQUAL(rdr->beginDecryption(fmk), libcdoc::OK); + + libcdoc::FileInfo fi; + BOOST_REQUIRE_EQUAL(rdr->nextFile(fi), libcdoc::OK); + BOOST_CHECK_EQUAL(fi.name, "test.txt"); + + std::vector out; + uint8_t buf[4096]; + libcdoc::result_t n; + while ((n = rdr->readData(buf, sizeof(buf))) > 0) + out.insert(out.end(), buf, buf + n); + BOOST_REQUIRE_EQUAL(n, 0); + + BOOST_CHECK(out == payload); +} + +} // namespace + +BOOST_AUTO_TEST_SUITE(LiveSidMid) + +BOOST_AUTO_TEST_CASE(SID) +{ + sidMidRoundtrip(SID_PNO, {}); +} + +BOOST_AUTO_TEST_CASE(MID) +{ + sidMidRoundtrip(MID_PNO, MID_PHONE); +} + +BOOST_AUTO_TEST_SUITE_END()