lists.osmocom.org
Sign In Sign Up
Manage this list Sign In Sign Up

Keyboard Shortcuts

Thread View

  • j: Next unread message
  • k: Previous unread message
  • j a: Jump to all threads
  • j l: Jump to MailingList overview

gerrit-log

Thread Start a new thread
Download
Threads by month
  • ----- 2026 -----
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2025 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2024 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2023 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2022 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
gerrit-log@lists.osmocom.org

October 2025

  • 1 participants
  • 631 discussions
[S] Change in osmo-ttcn3-hacks[master]: testenv: add a --fast argument
by fixeria 08 Oct '25

08 Oct '25
Attention is currently required from: osmith. fixeria has posted comments on this change by osmith. ( https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41206?usp=email ) Change subject: testenv: add a --fast argument ...................................................................... Patch Set 1: Code-Review+1 (1 comment) Patchset: PS1: Nice! When running tests locally, I usually do `exit 0` in these scripts because the packet capture never worked for me. Now I can use this or simply set `TESTENV_FAST=1` in my env. -- To view, visit https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41206?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: comment Gerrit-Project: osmo-ttcn3-hacks Gerrit-Branch: master Gerrit-Change-Id: Ief3488293e5c5c1a1c98244f80ffd56eadb040c5 Gerrit-Change-Number: 41206 Gerrit-PatchSet: 1 Gerrit-Owner: osmith <osmith(a)sysmocom.de> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: fixeria <vyanitskiy(a)sysmocom.de> Gerrit-Attention: osmith <osmith(a)sysmocom.de> Gerrit-Comment-Date: Wed, 08 Oct 2025 08:31:29 +0000 Gerrit-HasComments: Yes Gerrit-Has-Labels: Yes
1 0
0 0
[S] Change in osmo-ttcn3-hacks[master]: testenv: add a --fast argument
by osmith 08 Oct '25

08 Oct '25
osmith has uploaded this change for review. ( https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41206?usp=email ) Change subject: testenv: add a --fast argument ...................................................................... testenv: add a --fast argument The ttcn3-{tcpdump,dumpcap}-*.sh scripts can easily spend 5s or more for each test with: * Waiting until the pcap gets created. * Waiting until pcap size does not increase anymore after the test. * Saving the talloc report. Add --fast to skip all this, to allow running a whole testsuite significantly faster locally to check for regressions. Developers can then still run failing tests again without --fast to get the pcap and talloc reports. Change-Id: Ief3488293e5c5c1a1c98244f80ffd56eadb040c5 --- M _testenv/testenv/__init__.py M _testenv/testenv/testsuite.py M ttcn3-dumpcap-start.sh M ttcn3-dumpcap-stop.sh M ttcn3-tcpdump-start.sh M ttcn3-tcpdump-stop.sh 6 files changed, 35 insertions(+), 12 deletions(-) git pull ssh://gerrit.osmocom.org:29418/osmo-ttcn3-hacks refs/changes/06/41206/1 diff --git a/_testenv/testenv/__init__.py b/_testenv/testenv/__init__.py index c9e63b4..44259c2 100644 --- a/_testenv/testenv/__init__.py +++ b/_testenv/testenv/__init__.py @@ -112,6 +112,12 @@ "--titan-version", help="which eclipse-titan version to use", ) + group.add_argument( + "-f", + "--fast", + action="store_true", + help="skip recording pcaps (+ sleep before/after) and talloc reports", + ) group = sub_run.add_argument_group("source/binary options", "All components are built from source by default.") group.add_argument( diff --git a/_testenv/testenv/testsuite.py b/_testenv/testenv/testsuite.py index 0007c57..97a340e 100644 --- a/_testenv/testenv/testsuite.py +++ b/_testenv/testenv/testsuite.py @@ -151,6 +151,9 @@ env["OSMO_SUT_HOST"] = host env["OSMO_SUT_PORT"] = port + if testenv.args.fast: + env["TESTENV_FAST"] = "1" + env = testenv.cmd.generate_env(env, testenv.args.podman) cmd = [start_testsuite, suite, section_data["config"]] diff --git a/ttcn3-dumpcap-start.sh b/ttcn3-dumpcap-start.sh index ddd4eb4..0141419 100755 --- a/ttcn3-dumpcap-start.sh +++ b/ttcn3-dumpcap-start.sh @@ -18,6 +18,10 @@ echo "------ $TESTCASE ------" date +if [ "$TESTENV_FAST" = 1 ]; then + exit 0 +fi + if [ "z$TTCN3_PCAP_PATH" = "z" ]; then TTCN3_PCAP_PATH=/tmp fi diff --git a/ttcn3-dumpcap-stop.sh b/ttcn3-dumpcap-stop.sh index 1ac8e7b..8c5d347 100755 --- a/ttcn3-dumpcap-stop.sh +++ b/ttcn3-dumpcap-stop.sh @@ -15,6 +15,10 @@ printf "\033[1;31m------ $TESTCASE $VERDICT ------\033[0m\n\n" fi +if [ "$TESTENV_FAST" = 1 ]; then + exit 0 +fi + if [ "z$TTCN3_PCAP_PATH" = "z" ]; then TTCN3_PCAP_PATH=/tmp fi diff --git a/ttcn3-tcpdump-start.sh b/ttcn3-tcpdump-start.sh index 2c2f8c5..652a428 100755 --- a/ttcn3-tcpdump-start.sh +++ b/ttcn3-tcpdump-start.sh @@ -11,7 +11,14 @@ GSMTAP_PORT=4729 TESTCASE=$1 +echo "$TESTCASE" > "$TTCN3_PCAP_PATH/.current_test" +echo "------ $TESTCASE ------" +date + +if [ "$TESTENV_FAST" = 1 ]; then + exit 0 +fi SUDOSTR="" if ! [ "$(id -u)" = "0" ]; then @@ -20,9 +27,6 @@ # and this script will hang indefinitely fi -echo "------ $TESTCASE ------" -date - if [ "z$TTCN3_PCAP_PATH" = "z" ]; then TTCN3_PCAP_PATH=/tmp fi @@ -111,5 +115,3 @@ fi done kill $PID - -echo "$TESTCASE" > "$TTCN3_PCAP_PATH/.current_test" diff --git a/ttcn3-tcpdump-stop.sh b/ttcn3-tcpdump-stop.sh index 65aeb2a..41d29ec 100755 --- a/ttcn3-tcpdump-stop.sh +++ b/ttcn3-tcpdump-stop.sh @@ -8,13 +8,6 @@ TESTCASE=$1 VERDICT="$2" -SUDOSTR="" -if ! [ "$(id -u)" = "0" ]; then - SUDOSTR="sudo -n" - # Otherwise, if sudo /usr/bin/kill, sudo /usr/bin/tcpdump cannot be run without a password prompt, - # and this script will hang indefinitely -fi - date if [ x"$VERDICT" = x"pass" ]; then @@ -23,6 +16,17 @@ printf "\033[1;31m------ $TESTCASE $VERDICT ------\033[0m\n\n" fi +if [ "$TESTENV_FAST" = 1 ]; then + exit 0 +fi + +SUDOSTR="" +if ! [ "$(id -u)" = "0" ]; then + SUDOSTR="sudo -n" + # Otherwise, if sudo /usr/bin/kill, sudo /usr/bin/tcpdump cannot be run without a password prompt, + # and this script will hang indefinitely +fi + if [ "z$TTCN3_PCAP_PATH" = "z" ]; then TTCN3_PCAP_PATH=/tmp fi -- To view, visit https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41206?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: newchange Gerrit-Project: osmo-ttcn3-hacks Gerrit-Branch: master Gerrit-Change-Id: Ief3488293e5c5c1a1c98244f80ffd56eadb040c5 Gerrit-Change-Number: 41206 Gerrit-PatchSet: 1 Gerrit-Owner: osmith <osmith(a)sysmocom.de>
1 0
0 0
[XL] Change in osmo-ttcn3-hacks[master]: smdpp: proper headers for native code
by osmith 08 Oct '25

08 Oct '25
osmith has submitted this change. ( https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41122?usp=email ) Change subject: smdpp: proper headers for native code ...................................................................... smdpp: proper headers for native code Change-Id: I9dcb3249fc712e2ddb2c7cf48d4fc54425848bc6 --- R smdpp/bsp_crypto.cc M smdpp/helpers.h A smdpp/http_client.h M smdpp/logger.h M smdpp/regen_makefile.sh A smdpp/rsp_client.cc D smdpp/rsp_client.cpp A smdpp/rsp_client.h M smdpp/smdpp_Tests_Functions.cc 9 files changed, 1,925 insertions(+), 1,747 deletions(-) Approvals: dexter: Looks good to me, approved laforge: Looks good to me, but someone else must approve Jenkins Builder: Verified diff --git a/smdpp/bsp_crypto.cpp b/smdpp/bsp_crypto.cc similarity index 100% rename from smdpp/bsp_crypto.cpp rename to smdpp/bsp_crypto.cc diff --git a/smdpp/helpers.h b/smdpp/helpers.h index afc20ae..d706926 100644 --- a/smdpp/helpers.h +++ b/smdpp/helpers.h @@ -1,3 +1,23 @@ +#ifndef HELPERS_H +#define HELPERS_H + +#include <iostream> +#include <vector> +#include <string> +#include <sstream> +#include <iterator> +#include <iomanip> +#include <stdexcept> + +// Include OpenSSL headers we need +extern "C" { +#include <openssl/bio.h> +#include <openssl/buffer.h> +#include <openssl/evp.h> +#include <openssl/x509.h> +#include <openssl/x509_vfy.h> +} + std::ostream& operator<<(std::ostream& os, const std::vector<std::string>& v) { using namespace std; copy(v.begin(), v.end(), std::ostream_iterator<std::string>(os, "\n")); @@ -32,6 +52,12 @@ } }; +struct EVP_PKEY_Deleter { + void operator()(EVP_PKEY* key) const { + EVP_PKEY_free(key); + } +}; + struct X509_STORE_Deleter { void operator()(X509_STORE* store) const { X509_STORE_free(store); @@ -44,12 +70,6 @@ } }; -struct EVP_PKEY_Deleter { - void operator()(EVP_PKEY* key) const { - EVP_PKEY_free(key); - } -}; - struct EVP_MD_CTX_Deleter { void operator()(EVP_MD_CTX* ctx) const { EVP_MD_CTX_free(ctx); @@ -146,4 +166,6 @@ } }; -} // namespace RspCrypto \ No newline at end of file +} // namespace RspCrypto + +#endif // HELPERS_H \ No newline at end of file diff --git a/smdpp/http_client.h b/smdpp/http_client.h new file mode 100644 index 0000000..6b9c6aa --- /dev/null +++ b/smdpp/http_client.h @@ -0,0 +1,60 @@ +/* HTTP Client Internal Header + * + * Author: Eric Wild <ewild(a)sysmocom.de> / sysmocom - s.f.m.c. GmbH + * + * Released under the terms of GNU General Public License, Version 2 or + * (at your option) any later version. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef HTTP_CLIENT_H +#define HTTP_CLIENT_H + +#include <string> +#include <vector> +#include <openssl/x509.h> + +namespace RspCrypto { + +class HttpClient { + public: + HttpClient(); + ~HttpClient(); + + struct ResponseData { + std::string body; + long statusCode; + std::string headers; + }; + + struct PostConfig { + bool useMutualTLS = false; + std::string clientCertPath; + std::string clientKeyPath; + bool includeAdminProtocolHeader = false; + bool verboseOutput = false; + std::string contentType = "application/json"; + }; + + ResponseData postJson(const std::string& url, unsigned int port, const std::string& jsonData, X509_STORE* store, std::vector<X509*>& certPool, + const PostConfig& config); + + private: + static size_t writeCallback(void* contents, size_t size, size_t nmemb, void* userp); + static size_t headerCallback(void* contents, size_t size, size_t nmemb, void* userp); + + struct SslCtxData { + X509_STORE* store; + std::vector<X509*>* certPool; + bool verifyResult; + std::string errorMessage; + }; + + static std::string get_cn_name(X509_NAME* const name); + static CURLcode sslCtxFunction(CURL* curl, SSL_CTX* sslCtx, void* arg); +}; + +} // namespace RspCrypto + +#endif // HTTP_CLIENT_H \ No newline at end of file diff --git a/smdpp/logger.h b/smdpp/logger.h index f885d7d..d567947 100644 --- a/smdpp/logger.h +++ b/smdpp/logger.h @@ -1,4 +1,7 @@ +#ifndef LOGGER_H +#define LOGGER_H + #include <cstddef> #include <filesystem> #include <iterator> @@ -248,4 +251,6 @@ } }; -} // namespace RspCrypto \ No newline at end of file +} // namespace RspCrypto + +#endif // LOGGER_H \ No newline at end of file diff --git a/smdpp/regen_makefile.sh b/smdpp/regen_makefile.sh index d8405b3..03fef41 100755 --- a/smdpp/regen_makefile.sh +++ b/smdpp/regen_makefile.sh @@ -13,6 +13,8 @@ TCCInterface.cc PIPEasp_PT.cc smdpp_Tests_Functions.cc + rsp_client.cc + bsp_crypto.cc " . ../_buildsystem/regen_makefile.inc.sh diff --git a/smdpp/rsp_client.cc b/smdpp/rsp_client.cc new file mode 100644 index 0000000..7d52e39 --- /dev/null +++ b/smdpp/rsp_client.cc @@ -0,0 +1,1666 @@ +/* RSP Cryptographic Operations Implementation + * + * Author: Eric Wild <ewild(a)sysmocom.de> / sysmocom - s.f.m.c. GmbH + * + * Released under the terms of GNU General Public License, Version 2 or + * (at your option) any later version. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include <algorithm> +#include <cstddef> +#include <ctime> +#include <filesystem> +#include <fstream> +#include <iostream> +#include <iterator> +#include <memory> +#include <stdexcept> +#include <string> +#include <vector> + +extern "C" { +#include <dirent.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <openssl/asn1.h> +#include <openssl/asn1t.h> +#include <openssl/bio.h> +#include <openssl/buffer.h> +#include <openssl/core_names.h> +#include <openssl/crypto.h> +#include <openssl/err.h> +#include <openssl/evp.h> +#include <openssl/param_build.h> +#include <openssl/pem.h> +#include <openssl/rand.h> +#include <openssl/types.h> + +#include <openssl/cmac.h> +#include <openssl/ec.h> +#include <openssl/sha.h> +#include <openssl/ssl.h> + +#include <openssl/safestack.h> +#include <openssl/stack.h> +#include <openssl/x509.h> +#include <openssl/x509_vfy.h> +#include <openssl/x509v3.h> + +#include <curl/curl.h> +} + +#include "rsp_client.h" +#include "http_client.h" +#include "logger.h" + +namespace RspCrypto { + +// Private helper functions for CertificateUtil +static std::unique_ptr<BIO, BIODeleter> readFileToBIO(const std::string& filePath) { + std::ifstream file(filePath); + if (!file) { + throw std::runtime_error("Failed to open file: " + filePath); + } + + std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); + std::unique_ptr<BIO, BIODeleter> bio(BIO_new_mem_buf(content.c_str(), -1)); + if (!bio) { + throw OpenSSLError("Failed to create BIO from file content"); + } + + return bio; +} + +static std::vector<uint8_t> extractPublicKeyData(EVP_PKEY* key) { + size_t pub_len = 0; + if (EVP_PKEY_get_octet_string_param(key, OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &pub_len) != 1) { + throw OpenSSLError("Failed to get public key size"); + } + + std::vector<uint8_t> publicKeyData(pub_len); + if (EVP_PKEY_get_octet_string_param(key, OSSL_PKEY_PARAM_PUB_KEY, publicKeyData.data(), pub_len, &pub_len) != 1) { + throw OpenSSLError("Failed to extract public key"); + } + + return publicKeyData; +} + +static std::vector<uint8_t> getCertificateExtension(X509* cert, int nid) { + int loc = X509_get_ext_by_NID(cert, nid, -1); + if (loc < 0) { + return std::vector<uint8_t>(); + } + + X509_EXTENSION* ext = X509_get_ext(cert, loc); + if (!ext) { + return std::vector<uint8_t>(); + } + + ASN1_OCTET_STRING* ext_data = X509_EXTENSION_get_data(ext); + if (!ext_data) { + return std::vector<uint8_t>(); + } + + return std::vector<uint8_t>(ASN1_STRING_get0_data(ext_data), ASN1_STRING_get0_data(ext_data) + ASN1_STRING_length(ext_data)); +} + +// CertificateUtil implementations +std::string CertificateUtil::getEID(X509* cert) { + X509_NAME* subject = X509_get_subject_name(cert); + if (!subject) { + return ""; + } + + // EID is stored in serialNumber field + int index = X509_NAME_get_index_by_NID(subject, NID_serialNumber, -1); + if (index < 0) { + return ""; + } + + X509_NAME_ENTRY* entry = X509_NAME_get_entry(subject, index); + if (!entry) { + return ""; + } + + ASN1_STRING* asn1_str = X509_NAME_ENTRY_get_data(entry); + if (!asn1_str) { + return ""; + } + + const unsigned char* str_data = ASN1_STRING_get0_data(asn1_str); + int str_len = ASN1_STRING_length(asn1_str); + + return std::string(reinterpret_cast<const char*>(str_data), str_len); +} + +std::string CertificateUtil::getSubjectName(X509* cert) { + char subjectName[256]; + X509_NAME_oneline(X509_get_subject_name(cert), subjectName, sizeof(subjectName)); + return std::string(subjectName); +} + +std::vector<uint8_t> CertificateUtil::getSubjectKeyIdentifier(X509* cert) { + auto ext_data = getCertificateExtension(cert, NID_subject_key_identifier); + if (ext_data.empty()) { + return std::vector<uint8_t>(); + } + + const unsigned char* p = ext_data.data(); + long xlen = ext_data.size(); + int tag, xclass; + + ASN1_get_object(&p, &xlen, &tag, &xclass, ext_data.size()); + + return std::vector<uint8_t>(p, p + xlen); +} + +std::vector<uint8_t> CertificateUtil::getAuthorityKeyIdentifier(X509* cert) { + auto ext_data = getCertificateExtension(cert, NID_authority_key_identifier); + if (ext_data.empty()) { + return std::vector<uint8_t>(); + } + + // AKI is more complex, need to extract keyid + const unsigned char* p = ext_data.data(); + AUTHORITY_KEYID* akid = d2i_AUTHORITY_KEYID(NULL, &p, ext_data.size()); + + std::vector<uint8_t> aki; + if (akid && akid->keyid) { + aki.assign(akid->keyid->data, akid->keyid->data + akid->keyid->length); + AUTHORITY_KEYID_free(akid); + } + + return aki; +} + +std::unique_ptr<X509, X509Deleter> CertificateUtil::loadCertFromDER(const std::vector<uint8_t>& derData) { + const unsigned char* p = derData.data(); + X509* cert = d2i_X509(nullptr, &p, derData.size()); + + if (!cert) { + throw OpenSSLError("Failed to parse DER certificate"); + } + + return std::unique_ptr<X509, X509Deleter>(cert); +} + +std::vector<std::unique_ptr<X509, X509Deleter>> CertificateUtil::loadCertificateChain(const std::string& filename) { + std::vector<std::unique_ptr<X509, X509Deleter>> chain; + + if (std::filesystem::path(filename).extension() == ".der") { + FILE* file = fopen(filename.c_str(), "rb"); + if (!file) { + throw std::runtime_error("No certificates found in file: " + filename); + } + + X509* cert = d2i_X509_fp(file, nullptr); + chain.push_back(std::unique_ptr<X509, X509Deleter>(cert)); + fclose(file); + + } else { + BIO* bio = BIO_new_file(filename.c_str(), "r"); + if (!bio) { + throw OpenSSLError("Failed to open certificate file: " + filename); + } + X509* cert = nullptr; + while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != nullptr) { + chain.push_back(std::unique_ptr<X509, X509Deleter>(cert)); + } + + BIO_free(bio); + } + + if (chain.empty()) { + throw std::runtime_error("No certificates found in file: " + filename); + } + + return chain; +} + +static std::vector<uint8_t> certToDER(X509* cert) { + BIO* bio = BIO_new(BIO_s_mem()); + if (!bio) { + throw OpenSSLError("Failed to create BIO for certificate conversion"); + } + + i2d_X509_bio(bio, cert); + BUF_MEM* bufferPtr; + BIO_get_mem_ptr(bio, &bufferPtr); + + std::vector<uint8_t> der; + der.assign(bufferPtr->data, bufferPtr->data + bufferPtr->length); + BIO_free(bio); + + return der; +} + +bool CertificateUtil::verifyCertificateChainDynamic(X509* cert, const std::vector<X509*>& certPool, X509* rootCA, bool verbose) { + LOG_INFO("Verifying certificate chain..."); + LOG_INFO("Target certificate: " + getSubjectName(cert)); + + // certificate store for trusted roots only + std::unique_ptr<X509_STORE, X509_STORE_Deleter> store(X509_STORE_new()); + if (!store) { + throw OpenSSLError("Failed to create X509_STORE"); + } + + // Only add the explicitly provided root CA to the trust store + if (rootCA) { + if (verbose) { + LOG_INFO("Adding trusted root CA: " + getSubjectName(rootCA)); + } + if (X509_STORE_add_cert(store.get(), rootCA) != 1) { + throw OpenSSLError("Failed to add root CA to store"); + } + } else { + // If no root CA provided, find self-signed certificates in the pool + LOG_WARNING("No explicit root CA provided - searching for self-signed " + "certificates"); + for (auto candidate : certPool) { + if (X509_check_issued(candidate, candidate) == X509_V_OK) { + if (verbose) { + LOG_INFO("Adding self-signed certificate as trusted root: " + getSubjectName(candidate)); + } + if (X509_STORE_add_cert(store.get(), candidate) != 1) { + unsigned long err = ERR_peek_last_error(); + // Ignore duplicate certificate errors + if (ERR_GET_REASON(err) != X509_R_CERT_ALREADY_IN_HASH_TABLE) { + throw OpenSSLError("Failed to add root CA to store"); + } + ERR_clear_error(); + } + } + } + } + + // Build untrusted chain from cert pool (excluding the root if it's in + // there) IMPORTANT: custom deleter that only frees the stack, not the + // certificates + auto stack_only_deleter = [](STACK_OF(X509) * stack) { + if (stack) { + sk_X509_free(stack); // This only frees the stack structure, not the contents + } + }; + std::unique_ptr<STACK_OF(X509), decltype(stack_only_deleter)> untrusted_chain(sk_X509_new_null(), stack_only_deleter); + if (!untrusted_chain) { + throw OpenSSLError("Failed to create untrusted chain"); + } + + // Add all certificates from the pool as untrusted intermediates + for (auto poolCert : certPool) { + // Skip if this is the target certificate + if (X509_cmp(poolCert, cert) == 0) { + continue; + } + // Skip if this is the root CA (already in trust store) + if (rootCA && X509_cmp(poolCert, rootCA) == 0) { + continue; + } + // Add to untrusted chain + if (sk_X509_push(untrusted_chain.get(), poolCert) == 0) { + throw OpenSSLError("Failed to add certificate to untrusted chain"); + } + } + + if (verbose) { + LOG_INFO("Untrusted chain contains " + std::to_string(sk_X509_num(untrusted_chain.get())) + " certificates"); + } + + std::unique_ptr<X509_STORE_CTX, X509_STORE_CTX_Deleter> ctx(X509_STORE_CTX_new()); + if (!ctx) { + throw OpenSSLError("Failed to create X509_STORE_CTX"); + } + + if (X509_STORE_CTX_init(ctx.get(), store.get(), cert, untrusted_chain.get()) != 1) { + throw OpenSSLError("Failed to initialize X509_STORE_CTX"); + } + + unsigned long flags = X509_V_FLAG_CHECK_SS_SIGNATURE; + X509_STORE_CTX_set_flags(ctx.get(), flags); + + X509_STORE_CTX_set_verify_cb(ctx.get(), [](int ok, X509_STORE_CTX* ctx) -> int { + if (!ok) { + int error = X509_STORE_CTX_get_error(ctx); + // SGP.22 EUM certificates use name constraints in a specific way + if (error == X509_V_ERR_PERMITTED_VIOLATION || error == X509_V_ERR_EXCLUDED_VIOLATION || error == X509_V_ERR_SUBTREE_MINMAX) { + // Get the certificate that triggered this + X509* cert = X509_STORE_CTX_get_current_cert(ctx); + if (cert) { + char subject_buf[256]; + X509_NAME_oneline(X509_get_subject_name(cert), subject_buf, sizeof(subject_buf)); + LOG_INFO("Processing certificate with name constraint abuse: " + std::string(subject_buf)); + } + + return 1; + } + } + return ok; // Use default behavior for other errors + }); + + int result = X509_verify_cert(ctx.get()); + + if (result != 1) { + int error = X509_STORE_CTX_get_error(ctx.get()); + int depth = X509_STORE_CTX_get_error_depth(ctx.get()); + X509* errorCert = X509_STORE_CTX_get_current_cert(ctx.get()); + + LOG_ERROR("Certificate verification failed:"); + LOG_ERROR(" Error: " + std::string(X509_verify_cert_error_string(error))); + LOG_ERROR(" Depth: " + std::to_string(depth)); + + if (errorCert) { + LOG_ERROR(" Cert: " + getSubjectName(errorCert)); + } + + return false; + } + + LOG_INFO("Certificate verification successful"); + + if (verbose) { + STACK_OF(X509)* verified_chain = X509_STORE_CTX_get1_chain(ctx.get()); + if (verified_chain) { + LOG_INFO("Verified certificate chain:"); + for (int i = 0; i < sk_X509_num(verified_chain); i++) { + X509* chainCert = sk_X509_value(verified_chain, i); + LOG_INFO(" " + std::to_string(i + 1) + ". " + getSubjectName(chainCert)); + } + sk_X509_pop_free(verified_chain, X509_free); + } + } + + return true; +} + +static std::vector<std::filesystem::path> find_cert_files(const std::filesystem::path& root_path, const std::vector<std::string>& name_filters = {}) { + std::vector<std::filesystem::path> result; + + if (!std::filesystem::exists(root_path)) { + std::cerr << "Path does not exist: " << root_path << std::endl; + return result; + } + + auto file_matches = [&name_filters](const std::filesystem::path& path) { + std::string ext = path.extension().string(); + if (!(ext == ".pem" || ext == ".der" || ext == ".crt")) { + return false; + } + + // If no filters specified, accept all matching extensions + if (name_filters.empty()) { + return true; + } + + // Check if filename contains any of the filters + std::string filename = path.filename().string(); + return std::any_of(name_filters.begin(), name_filters.end(), + [&filename](const std::string& filter) { return filename.find(filter) != std::string::npos; }); + }; + + if (std::filesystem::is_regular_file(root_path)) { + if (file_matches(root_path)) { + result.push_back(root_path); + } + } else if (std::filesystem::is_directory(root_path)) { + for (const auto& entry : std::filesystem::recursive_directory_iterator(root_path)) { + if (entry.is_regular_file() && file_matches(entry.path())) { + result.push_back(entry.path()); + } + } + } + + return result; +} + +std::vector<std::unique_ptr<X509, X509Deleter>> CertificateUtil::loadCertificatesFromDirectory(const std::string& directory, + const std::vector<std::string>& name_filters) { + std::vector<std::unique_ptr<X509, X509Deleter>> certificates; + + auto files_in_dir = find_cert_files(directory, name_filters); + + for (const auto& entry : files_in_dir) { + std::string ext = entry.extension().string(); + auto fpath = entry.string(); + + try { + if (ext == ".pem" || ext == ".crt") { + auto fileCerts = loadCertificateChain(entry); + for (auto& cert : fileCerts) { + certificates.push_back(std::move(cert)); + } + } else if (ext == ".der") { + FILE* file = fopen(fpath.c_str(), "rb"); + if (!file) { + LOG_WARNING("Failed to open certificate file: " + fpath); + continue; + } + + fseek(file, 0, SEEK_END); + long file_size = ftell(file); + rewind(file); + + std::vector<unsigned char> data(file_size); + if (fread(data.data(), 1, file_size, file) != static_cast<size_t>(file_size)) { + fclose(file); + LOG_WARNING("Failed to read certificate file: " + fpath); + continue; + } + + fclose(file); + + const unsigned char* p = data.data(); + X509* cert = d2i_X509(nullptr, &p, file_size); + + if (cert) { + certificates.push_back(std::unique_ptr<X509, X509Deleter>(cert)); + LOG_INFO("Loaded certificate: " + getSubjectName(cert) + " from " + fpath); + } + } + } catch (const std::exception& e) { + // already logged in called functions + } + } + + return certificates; +} + +static void xx_loadCertificate(const std::string& certPath, const std::string& typeName, std::unique_ptr<X509, X509Deleter>& certStorage) { + try { + auto certs = CertificateUtil::loadCertificateChain(certPath); + if (certs.empty()) { + throw std::runtime_error("No " + typeName + " certificate found in file: " + certPath); + } + + certStorage = std::move(certs[0]); + LOG_DEBUG("Loaded " + typeName + " certificate: " + CertificateUtil::getSubjectName(certStorage.get())); + + } catch (const std::exception& e) { + LOG_ERROR("Failed to load " + typeName + " certificate: " + std::string(e.what())); + throw; + } +} + +static void xx_loadPrivateKey(const std::string& keyPath, const std::string& typeName, std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter>& keyStorage) { + try { + if (!std::filesystem::exists(keyPath)) { + throw std::runtime_error(typeName + " private key file not found: " + keyPath); + } + + auto keyBio = readFileToBIO(keyPath); + + keyStorage.reset(PEM_read_bio_PrivateKey(keyBio.get(), nullptr, nullptr, nullptr)); + if (!keyStorage) { + throw OpenSSLError("Failed to read " + typeName + " private key"); + } + + LOG_DEBUG("Loaded " + typeName + " private key from: " + keyPath); + + } catch (const std::exception& e) { + LOG_ERROR("Failed to load " + typeName + " private key: " + std::string(e.what())); + throw; + } +} + +static bool xx_loadPublicKey(const std::string& pubKeyPath, const std::string& typeName, std::vector<uint8_t>& publicKeyStorage) { + try { + if (!std::filesystem::exists(pubKeyPath)) { + LOG_INFO(typeName + " public key file not found: " + pubKeyPath + " (will generate from private key)"); + return false; + } + + auto keyBio = readFileToBIO(pubKeyPath); + + std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> pubKey(PEM_read_bio_PUBKEY(keyBio.get(), nullptr, nullptr, nullptr)); + if (!pubKey) { + LOG_WARNING("Failed to read " + typeName + " public key from file (will generate from private key)"); + return false; + } + + publicKeyStorage = extractPublicKeyData(pubKey.get()); + + LOG_INFO("Loaded " + typeName + " public key from file (" + std::to_string(publicKeyStorage.size()) + " bytes)"); + return true; + + } catch (const std::exception& e) { + LOG_WARNING("Failed to load " + typeName + " public key from file: " + std::string(e.what()) + " (will generate from private key)"); + return false; + } +} + +static void xx_generatePublicKeyFromPrivate(const std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter>& privateKey, const std::string& typeName, + std::vector<uint8_t>& publicKeyStorage) { + if (!privateKey) { + throw std::runtime_error("No " + typeName + " private key available for public key generation"); + } + + publicKeyStorage = extractPublicKeyData(privateKey.get()); + + LOG_DEBUG("Generated " + typeName + " public key from private key (" + std::to_string(publicKeyStorage.size()) + " bytes)"); +} + +static std::vector<std::string> parse_permitted_eins_from_cert(X509* cert) { + std::vector<std::string> permitted_iins; + + bool is_old_variant = false; + int pos = X509_get_ext_by_NID(cert, NID_certificate_policies, -1); + if (pos >= 0) { + X509_EXTENSION* ext = X509_get_ext(cert, pos); + CERTIFICATEPOLICIES* policies = (CERTIFICATEPOLICIES*)X509V3_EXT_d2i(ext); + if (policies) { + for (int i = 0; i < sk_POLICYINFO_num(policies); i++) { + POLICYINFO* policy = sk_POLICYINFO_value(policies, i); + char oid_str[128]; + OBJ_obj2txt(oid_str, sizeof(oid_str), policy->policyid, 1); + if (std::string(oid_str) == "2.23.146.1.2.1.2") { + is_old_variant = true; + break; + } + } + CERTIFICATEPOLICIES_free(policies); + } + } + + if (is_old_variant) { + // Parse from nameConstraints + pos = X509_get_ext_by_NID(cert, NID_name_constraints, -1); + if (pos >= 0) { + X509_EXTENSION* ext = X509_get_ext(cert, pos); + NAME_CONSTRAINTS* nc = (NAME_CONSTRAINTS*)X509V3_EXT_d2i(ext); + if (nc && nc->permittedSubtrees) { + for (int i = 0; i < sk_GENERAL_SUBTREE_num(nc->permittedSubtrees); i++) { + GENERAL_SUBTREE* subtree = sk_GENERAL_SUBTREE_value(nc->permittedSubtrees, i); + if (subtree->base->type == GEN_DIRNAME) { + X509_NAME* name = subtree->base->d.directoryName; + int idx = X509_NAME_get_index_by_NID(name, NID_serialNumber, -1); + if (idx >= 0) { + X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, idx); + ASN1_STRING* asn1_str = X509_NAME_ENTRY_get_data(entry); + std::string iin(reinterpret_cast<const char*>(ASN1_STRING_get0_data(asn1_str)), + ASN1_STRING_length(asn1_str)); + permitted_iins.push_back(iin); + } + } + } + } + if (nc) + NAME_CONSTRAINTS_free(nc); + } + } else { + // Parse from GSMA permittedEins extension + ASN1_OBJECT* obj = OBJ_txt2obj("2.23.146.1.2.2.0", 1); + pos = X509_get_ext_by_OBJ(cert, obj, -1); + ASN1_OBJECT_free(obj); + + if (pos >= 0) { + X509_EXTENSION* ext = X509_get_ext(cert, pos); + ASN1_OCTET_STRING* ext_data = X509_EXTENSION_get_data(ext); + const unsigned char* p = ext_data->data; + long remaining = ext_data->length; + + // SEQUENCE OF PrintableString + int tag, xclass; + long xlen; + + // SEQUENCE tag + ASN1_get_object(&p, &xlen, &tag, &xclass, remaining); + if (tag == V_ASN1_SEQUENCE) { + remaining = xlen; + + while (remaining > 0) { + const unsigned char* elem_start = p; + ASN1_get_object(&p, &xlen, &tag, &xclass, remaining); + + if (tag == V_ASN1_PRINTABLESTRING) { + std::string iin(reinterpret_cast<const char*>(p), xlen); + permitted_iins.push_back(iin); + } + + p += xlen; + remaining -= (p - elem_start); + } + } + } + } + + return permitted_iins; +} + +std::string CertificateUtil::getPermittedEINs(const std::vector<uint8_t>& eumCertData) { + try { + auto cert = loadCertFromDER(eumCertData); + auto eins = parse_permitted_eins_from_cert(cert.get()); + + // Join EINs with comma + std::string result; + for (size_t i = 0; i < eins.size(); i++) { + if (i > 0) + result += ","; + result += eins[i]; + } + + return result; + } catch (const std::exception& e) { + LOG_ERROR("getPermittedEINs failed: " + std::string(e.what())); + return ""; + } +} + +bool CertificateUtil::validateEIDRange(const std::string& eid, const std::vector<uint8_t>& eumCertData) { + try { + auto eumCert = loadCertFromDER(eumCertData); + std::vector<std::string> permittedEins = parse_permitted_eins_from_cert(eumCert.get()); + + if (permittedEins.empty()) { + LOG_WARNING("No permitted EINs found in EUM certificate"); + return false; + } + + // Check if EID starts with any permitted EIN + std::string eidNormalized = eid; + std::transform(eidNormalized.begin(), eidNormalized.end(), eidNormalized.begin(), ::toupper); + + for (const auto& ein : permittedEins) { + if (eidNormalized.find(ein) == 0) { + LOG_INFO("EID " + eidNormalized + " matches permitted EIN " + ein); + return true; + } + } + + LOG_ERROR("EID " + eidNormalized + " is not in any permitted EIN list"); + return false; + } catch (const std::exception& e) { + LOG_ERROR("validateEIDRange failed: " + std::string(e.what())); + return false; + } +} + +bool CertificateUtil::hasRSPRole(const std::vector<uint8_t>& certData, const std::string& roleOid) { + try { + auto cert = loadCertFromDER(certData); + + int pos = X509_get_ext_by_NID(cert.get(), NID_certificate_policies, -1); + if (pos < 0) + return false; + + X509_EXTENSION* ext = X509_get_ext(cert.get(), pos); + CERTIFICATEPOLICIES* policies = (CERTIFICATEPOLICIES*)X509V3_EXT_d2i(ext); + + if (!policies) + return false; + + for (int i = 0; i < sk_POLICYINFO_num(policies); i++) { + POLICYINFO* policy = sk_POLICYINFO_value(policies, i); + char oid_str[128]; + OBJ_obj2txt(oid_str, sizeof(oid_str), policy->policyid, 1); + + if (std::string(oid_str) == roleOid) { + CERTIFICATEPOLICIES_free(policies); + return true; + } + } + + CERTIFICATEPOLICIES_free(policies); + return false; + } catch (const std::exception& e) { + LOG_ERROR("hasRSPRole failed: " + std::string(e.what())); + return false; + } +} + +std::string CertificateUtil::getCurveOID(const std::vector<uint8_t>& certData) { + try { + auto cert = loadCertFromDER(certData); + + EVP_PKEY* pkey = X509_get0_pubkey(cert.get()); + if (!pkey) { + LOG_ERROR("Failed to get public key from certificate"); + return ""; + } + + if (EVP_PKEY_base_id(pkey) != EVP_PKEY_EC) { + LOG_ERROR("Certificate does not contain EC key"); + return ""; + } + + char curve_name[256] = { 0 }; + size_t curve_name_len = sizeof(curve_name); + + if (EVP_PKEY_get_utf8_string_param(pkey, "group", curve_name, sizeof(curve_name), &curve_name_len) != 1) { + LOG_ERROR("Failed to get curve name from EC key"); + return ""; + } + + std::string curve_str(curve_name); + + if (curve_str == "prime256v1") { + return "prime256v1"; + } else if (curve_str == "secp384r1") { + return "secp384r1"; + } else if (curve_str == "brainpoolP256r1") { + return "brainpoolP256r1"; + } else if (curve_str == "brainpoolP384r1") { + return "brainpoolP384r1"; + } + + return "unknown"; + } catch (const std::exception& e) { + LOG_ERROR("getCurveOID failed: " + std::string(e.what())); + return ""; + } +} + +// ECDH compatible (same curve) +bool CertificateUtil::verifyECDHCompatible(const std::vector<uint8_t>& pubKey1, const std::vector<uint8_t>& pubKey2) { + try { + // Both should be uncompressed EC points starting with 0x04 + if (pubKey1.empty() || pubKey2.empty() || pubKey1[0] != 0x04 || pubKey2[0] != 0x04) { + LOG_ERROR("Invalid EC public key format"); + return false; + } + + // For P-256, size should be 65 bytes (1 + 32 + 32) + if (pubKey1.size() == 65 && pubKey2.size() == 65) { + LOG_INFO("Both keys are P-256 format, ECDH compatible"); + return true; + } + + // For P-384, size should be 97 bytes (1 + 48 + 48) + if (pubKey1.size() == 97 && pubKey2.size() == 97) { + LOG_INFO("Both keys are P-384 format, ECDH compatible"); + return true; + } + + LOG_ERROR("Key sizes don't match or unsupported curve"); + return false; + } catch (const std::exception& e) { + LOG_ERROR("verifyECDHCompatible failed: " + std::string(e.what())); + return false; + } +} + +static std::vector<uint8_t> convertBSI_TR03111_to_DER(const std::vector<uint8_t>& bsi_signature) { + if (bsi_signature.size() != 64) { + std::cerr << "Invalid BSI TR-03111 signature size: " << bsi_signature.size() << std::endl; + return {}; + } + + BIGNUM* r = BN_new(); + BIGNUM* s = BN_new(); + + if (!r || !s) { + if (r) + BN_free(r); + if (s) + BN_free(s); + return {}; + } + + BN_bin2bn(bsi_signature.data(), 32, r); + BN_bin2bn(bsi_signature.data() + 32, 32, s); + + ECDSA_SIG* ecdsa_sig = ECDSA_SIG_new(); + if (!ecdsa_sig) { + BN_free(r); + BN_free(s); + return {}; + } + + ECDSA_SIG_set0(ecdsa_sig, r, s); + + int der_len = i2d_ECDSA_SIG(ecdsa_sig, nullptr); + if (der_len <= 0) { + ECDSA_SIG_free(ecdsa_sig); + return {}; + } + + std::vector<uint8_t> der_signature(der_len); + unsigned char* der_ptr = der_signature.data(); + i2d_ECDSA_SIG(ecdsa_sig, &der_ptr); + + ECDSA_SIG_free(ecdsa_sig); + + return der_signature; +} + +static std::vector<uint8_t> convertDER_to_BSI_TR03111(const std::vector<uint8_t>& der_signature) { + if (der_signature.empty()) { + std::cerr << "Empty DER signature" << std::endl; + return {}; + } + + const unsigned char* der_ptr = der_signature.data(); + ECDSA_SIG* ecdsa_sig = d2i_ECDSA_SIG(nullptr, &der_ptr, der_signature.size()); + + if (!ecdsa_sig) { + std::cerr << "Failed to parse DER signature" << std::endl; + return {}; + } + + const BIGNUM *r, *s; + ECDSA_SIG_get0(ecdsa_sig, &r, &s); + + if (!r || !s) { + ECDSA_SIG_free(ecdsa_sig); + return {}; + } + + std::vector<uint8_t> bsi_signature(64, 0); + + int r_len = BN_num_bytes(r); + int s_len = BN_num_bytes(s); + + if (r_len > 32 || s_len > 32) { + std::cerr << "BIGNUM values too large for BSI TR-03111 format" << std::endl; + ECDSA_SIG_free(ecdsa_sig); + return {}; + } + + // Convert r to bytes (with leading zeros padding) + int r_offset = 32 - r_len; + BN_bn2bin(r, bsi_signature.data() + r_offset); + + // Convert s to bytes (with leading zeros padding) + int s_offset = 64 - s_len; + BN_bn2bin(s, bsi_signature.data() + s_offset); + + ECDSA_SIG_free(ecdsa_sig); + + return bsi_signature; +} + +static bool verify_TR031111(const std::vector<uint8_t>& message, const std::vector<uint8_t>& bsi_signature, EVP_PKEY* publicKey) { + std::vector<uint8_t> der_signature = convertBSI_TR03111_to_DER(bsi_signature); + if (der_signature.empty()) { + std::cerr << "Failed to convert signature to DER format" << std::endl; + return false; + } + + std::unique_ptr<EVP_MD_CTX, EVP_MD_CTX_Deleter> mdctx(EVP_MD_CTX_new()); + if (!mdctx.get()) { + std::cerr << "Failed to create MD context" << std::endl; + return false; + } + + if (EVP_DigestVerifyInit(mdctx.get(), nullptr, EVP_sha256(), nullptr, publicKey) != 1) { + std::cerr << "Failed to initialize digest verify" << std::endl; + return false; + } + + if (EVP_DigestVerifyUpdate(mdctx.get(), message.data(), message.size()) != 1) { + std::cerr << "Failed to update digest verify" << std::endl; + return false; + } + + int result = EVP_DigestVerifyFinal(mdctx.get(), der_signature.data(), der_signature.size()); + + return result == 1; +} + +// HttpClient implementations +HttpClient::HttpClient() { + // should be called once per application + curl_global_init(CURL_GLOBAL_DEFAULT); +} + +HttpClient::~HttpClient() { + curl_global_cleanup(); +} + +// Private helper functions for HttpClient +size_t HttpClient::writeCallback(void* contents, size_t size, size_t nmemb, void* userp) { + size_t realSize = size * nmemb; + auto* response = static_cast<std::string*>(userp); + response->append(static_cast<char*>(contents), realSize); + return realSize; +} + +size_t HttpClient::headerCallback(void* contents, size_t size, size_t nmemb, void* userp) { + size_t realSize = size * nmemb; + auto* headers = static_cast<std::string*>(userp); + headers->append(static_cast<char*>(contents), realSize); + return realSize; +} + +std::string HttpClient::get_cn_name(X509_NAME* const name) { + int idx = -1; + unsigned char* utf8 = NULL; + X509_NAME_ENTRY* entry; + ASN1_STRING* data; + + if (!name) + return {}; + + idx = X509_NAME_get_index_by_NID(name, NID_commonName, -1); + if (idx < 0) + return {}; + + entry = X509_NAME_get_entry(name, idx); + if (!entry) + return {}; + + data = X509_NAME_ENTRY_get_data(entry); + if (!data) + return {}; + + if (!ASN1_STRING_to_UTF8(&utf8, data) || !utf8) + return {}; + + std::string retstr((char*)utf8); + OPENSSL_free(utf8); + return retstr; +} + +CURLcode HttpClient::sslCtxFunction(CURL* curl, SSL_CTX* sslCtx, void* arg) { + SslCtxData* ctxData = static_cast<SslCtxData*>(arg); + SSL_CTX* ctx = (SSL_CTX*)sslCtx; + X509_STORE* store = SSL_CTX_get_cert_store(ctx); + + for (auto cert : *ctxData->certPool) { + LOG_DEBUG("ADDED Issuer: " + get_cn_name(X509_get_issuer_name(cert)) + " Subject: " + get_cn_name(X509_get_subject_name(cert)) + + " SKI: " + HexUtil::bytesToHex(CertificateUtil::getSubjectKeyIdentifier(cert)) + + " AKI: " + HexUtil::bytesToHex(CertificateUtil::getAuthorityKeyIdentifier(cert))); + + auto result = X509_STORE_add_cert(store, cert); + if (result != 1) { + printf("Warning: Could not add certificate\n"); + + unsigned long err = ERR_get_error(); + if (ERR_GET_REASON(err) != X509_R_CERT_ALREADY_IN_HASH_TABLE) { + ERR_print_errors_fp(stderr); + } + } + } + + SSL_CTX_set_verify(sslCtx, SSL_VERIFY_PEER, nullptr); + + return CURLE_OK; +} + +HttpClient::ResponseData HttpClient::postJson(const std::string& url, unsigned int port, const std::string& jsonData, X509_STORE* store, + std::vector<X509*>& certPool, const PostConfig& config) { + ResponseData response; + CURL* curl = curl_easy_init(); + + if (!curl) { + throw std::runtime_error("Failed to initialize CURL"); + } + + struct curl_slist* headers = nullptr; + std::string contentTypeHeader = "Content-Type: " + config.contentType; + if (config.useMutualTLS) { + if (config.contentType == "application/json") { + contentTypeHeader += ";charset=UTF-8"; + } + headers = curl_slist_append(headers, contentTypeHeader.c_str()); + headers = curl_slist_append(headers, "Accept: application/json"); + if (config.includeAdminProtocolHeader) { + headers = curl_slist_append(headers, "X-Admin-Protocol: gsma/rsp/v2.5.0"); + } + } else { + headers = curl_slist_append(headers, contentTypeHeader.c_str()); + // For ASN.1, accept the same content type + std::string acceptHeader = "Accept: " + (config.contentType == "application/x-gsma-rsp-asn1" ? config.contentType : "application/json"); + headers = curl_slist_append(headers, acceptHeader.c_str()); + // Always add X-Admin-Protocol for ES9+ regardless of content type + headers = curl_slist_append(headers, "X-Admin-Protocol: gsma/rsp/v2.5.0"); + } + + SslCtxData ctxData = { .store = store, .certPool = &certPool, .verifyResult = false, .errorMessage = "" }; + + try { + // Basic CURL setup + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_PORT, port); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonData.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, jsonData.size()); // Important for binary data + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response.headers); + + // Enable SSL verification + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + + // Set reasonable timeouts + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); + + if (config.useMutualTLS) { + if (!config.clientCertPath.empty()) { + curl_easy_setopt(curl, CURLOPT_SSLCERT, config.clientCertPath.c_str()); + + std::string certType = "PEM"; + if (config.clientCertPath.find(".der") != std::string::npos || config.clientCertPath.find(".DER") != std::string::npos) { + certType = "DER"; + } + curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, certType.c_str()); + LOG_DEBUG("Set client certificate: " + config.clientCertPath + " (type: " + certType + ")"); + } + + if (!config.clientKeyPath.empty()) { + curl_easy_setopt(curl, CURLOPT_SSLKEY, config.clientKeyPath.c_str()); + curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "PEM"); + LOG_DEBUG("Set client private key: " + config.clientKeyPath); + } + } + + // Use our custom SSL context function for server cert verification + curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, sslCtxFunction); + curl_easy_setopt(curl, CURLOPT_SSL_CTX_DATA, &ctxData); + + curl_easy_setopt(curl, CURLOPT_VERBOSE, config.verboseOutput ? 1L : 0L); + + CURLcode res = curl_easy_perform(curl); + + if (res != CURLE_OK) { + throw std::runtime_error(std::string("CURL request failed: ") + curl_easy_strerror(res)); + } + + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.statusCode); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + return response; + } catch (...) { + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + throw; + } +} + +// Hashed Confirmation Code = SHA256(SHA256(Confirmation Code) | TransactionID) +static std::vector<uint8_t> computeConfirmationCodeHash(const std::string& confirmationCode, const std::vector<uint8_t>& transactionId) { + // Convert confirmation code from hex string to bytes (like EID handling) + std::vector<uint8_t> ccBytes; + for (size_t i = 0; i < confirmationCode.length(); i += 2) { + if (i + 1 < confirmationCode.length()) { + std::string byteString = confirmationCode.substr(i, 2); + uint8_t byte = static_cast<uint8_t>(std::stoi(byteString, nullptr, 16)); + ccBytes.push_back(byte); + } + } + + unsigned char firstHash[SHA256_DIGEST_LENGTH]; + SHA256(ccBytes.data(), ccBytes.size(), firstHash); + + std::vector<uint8_t> concatenated; + concatenated.insert(concatenated.end(), firstHash, firstHash + SHA256_DIGEST_LENGTH); + concatenated.insert(concatenated.end(), transactionId.begin(), transactionId.end()); + + unsigned char finalHash[SHA256_DIGEST_LENGTH]; + SHA256(concatenated.data(), concatenated.size(), finalHash); + + return std::vector<uint8_t>(finalHash, finalHash + SHA256_DIGEST_LENGTH); +} + +// RSPClient implementations +RSPClient::RSPClient(const std::string& serverUrl, const unsigned int serverPort, const std::vector<std::string>& certPath, + const std::vector<std::string>& name_filters) + : m_serverUrl(serverUrl) { + // OpenSSL 3.0+ initializes automatically + + for (auto cpath : certPath) { + bool isDirectory = std::filesystem::is_directory(cpath); + try { + if (isDirectory) { + LOG_DEBUG("Loading certificates from directory: " + cpath); + + auto certs = CertificateUtil::loadCertificatesFromDirectory(cpath, name_filters); + LOG_DEBUG("Loaded " + std::to_string(certs.size()) + " certificates"); + + // Separate root CAs and intermediates + for (auto& cert : certs) { + // Identify root certificates (self-signed) + if (X509_check_issued(cert.get(), cert.get()) == X509_V_OK) { + LOG_DEBUG("Found root CA: " + CertificateUtil::getSubjectName(cert.get())); + + // Store the first root CA as our primary root + if (!m_rootCA) { + m_rootCA = std::move(cert); + } else { + // Store additional roots for chain verification + m_certPool.push_back(std::move(cert)); + } + } else { + // Store intermediate certificates + m_certPool.push_back(std::move(cert)); + } + } + + if (!m_rootCA) { + LOG_WARNING("No root CA found in directory"); + } + } else { + // load certificate chain from single file + LOG_INFO("Loading certificates from file: " + cpath); + auto caChain = CertificateUtil::loadCertificateChain(cpath); + + if (!caChain.empty()) { + m_rootCA = std::move(caChain[0]); + + for (size_t i = 1; i < caChain.size(); ++i) { + m_intermediateCA.push_back(std::move(caChain[i])); + } + + LOG_INFO("Loaded root CA: " + CertificateUtil::getSubjectName(m_rootCA.get())); + + for (const auto& cert : m_intermediateCA) { + LOG_INFO("Loaded intermediate CA: " + CertificateUtil::getSubjectName(cert.get())); + + // add intermediates to certificate pool for compatibility + m_certPool.push_back(std::unique_ptr<X509, X509Deleter>(X509_dup(cert.get()))); + } + } + } + } catch (const std::exception& e) { + LOG_ERROR("Failed to load certificates: " + std::string(e.what())); + throw; + } + } +} + +RSPClient::~RSPClient() { + // OpenSSL 3.0+ handles cleanup automatically +} + +void RSPClient::setCACertPath(const std::string& path) { + m_caCertPath = path; +} + +void RSPClient::loadEUICCCertificate(const std::string& euiccCertPath) { + loadCertificate(euiccCertPath, "eUICC", m_euiccCert); + + try { + m_EID = CertificateUtil::getEID(m_euiccCert.get()); + LOG_DEBUG("Using EID from certificate: " + m_EID); + + auto ski = CertificateUtil::getSubjectKeyIdentifier(m_euiccCert.get()); + m_euiccSKI = ski; + LOG_DEBUG("Using eUICC SKI as PKID: " + HexUtil::bytesToHex(m_euiccSKI)); + } catch (const std::exception& e) { + LOG_ERROR("Failed to extract EUICC-specific data: " + std::string(e.what())); + throw; + } +} + +void RSPClient::loadEUICCKeyPair(const std::string& euiccPrivateKeyPath, const std::string& euiccPublicKeyPath) { + loadKeyPair(euiccPrivateKeyPath, euiccPublicKeyPath, "eUICC", m_euiccPrivateKey, m_euiccPublicKeyData); +} + +void RSPClient::loadEUMCertificate(const std::string& eumCertPath) { + loadCertificate(eumCertPath, "EUM", m_eumCert); +} + +void RSPClient::loadEUMKeyPair(const std::string& eumPrivateKeyPath, const std::string& eumPublicKeyPath) { + loadKeyPair(eumPrivateKeyPath, eumPublicKeyPath, "EUM", m_eumPrivateKey, m_eumPublicKeyData); +} + +std::vector<uint8_t> RSPClient::generateChallenge() { + std::vector<uint8_t> challenge(16); + if (RAND_bytes(challenge.data(), challenge.size()) != 1) { + throw OpenSSLError("Failed to generate random challenge"); + } + return challenge; +} + +int getCertificateCurveNID(X509* cert) { + if (!cert) { + throw std::runtime_error("Certificate is null"); + } + + std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> pubkey(X509_get_pubkey(cert)); + if (!pubkey) { + throw OpenSSLError("Failed to extract public key from certificate"); + } + + char curve_name[80]; + size_t curve_name_len = sizeof(curve_name); + if (EVP_PKEY_get_utf8_string_param(pubkey.get(), OSSL_PKEY_PARAM_GROUP_NAME, curve_name, sizeof(curve_name), &curve_name_len) != 1) { + throw OpenSSLError("Failed to get curve name from certificate"); + } + + std::string curve_str(curve_name); + int curve_nid; + + if (curve_str == "prime256v1" || curve_str == "P-256") { + curve_nid = NID_X9_62_prime256v1; + } else if (curve_str == "brainpoolP256r1") { + curve_nid = NID_brainpoolP256r1; + } else { + throw std::runtime_error("Unsupported curve in certificate: " + curve_str); + } + + return curve_nid; +} + +int RSPClient::getEUICCCurveNID() { + if (!m_euiccCert) { + throw std::runtime_error("eUICC certificate not loaded"); + } + + int curve_nid = getCertificateCurveNID(m_euiccCert.get()); + + if (curve_nid == NID_X9_62_prime256v1) { + LOG_INFO("eUICC certificate uses P-256 curve"); + } else if (curve_nid == NID_brainpoolP256r1) { + LOG_INFO("eUICC certificate uses brainpoolP256r1 curve"); + } + + return curve_nid; +} + +void RSPClient::generateEUICCOtpk() { + int curve_nid = getEUICCCurveNID(); + std::string curve_name = (curve_nid == NID_X9_62_prime256v1) ? "P-256" : "brainpoolP256r1"; + + std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> pctx(EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr), EVP_PKEY_CTX_free); + if (!pctx) { + throw OpenSSLError("Failed to create EVP_PKEY_CTX"); + } + + if (EVP_PKEY_keygen_init(pctx.get()) <= 0) { + throw OpenSSLError("Failed to initialize key generation"); + } + + if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx.get(), curve_nid) <= 0) { + throw OpenSSLError("Failed to set " + curve_name + " curve"); + } + + EVP_PKEY* pkey_raw = nullptr; + if (EVP_PKEY_keygen(pctx.get(), &pkey_raw) <= 0) { + throw OpenSSLError("Failed to generate EC key pair"); + } + + m_euicc_ot_PrivateKey.reset(pkey_raw); + + size_t pub_len = 0; + + if (EVP_PKEY_get_octet_string_param(m_euicc_ot_PrivateKey.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &pub_len) != 1) { + throw OpenSSLError("Failed to get public key size"); + } + + // Both P-256 and brainpoolP256r1 have 256-bit (32 byte) coordinates + // Uncompressed format: 0x04 + 32 bytes X + 32 bytes Y = 65 bytes + if (pub_len != 65) { + throw OpenSSLError("Unexpected public key size for " + curve_name); + } + + m_euiccOtpk.resize(pub_len); + if (EVP_PKEY_get_octet_string_param(m_euicc_ot_PrivateKey.get(), OSSL_PKEY_PARAM_PUB_KEY, m_euiccOtpk.data(), pub_len, &pub_len) != 1) { + throw OpenSSLError("Failed to extract public key"); + } + + LOG_INFO("Generated eUICC OtPK (" + curve_name + "): " + HexUtil::bytesToHex(m_euiccOtpk)); + + if (m_euiccOtpk[0] != 0x04) { + throw std::runtime_error("Invalid public key format - expected uncompressed point"); + } + + LOG_DEBUG("Public key format verified - uncompressed EC point"); +} + +std::vector<uint8_t> RSPClient::computeECDHSharedSecret(const std::vector<uint8_t>& otherPublicKey) { + if (!m_euicc_ot_PrivateKey) { + throw std::runtime_error("eUICC ephemeral private key not available"); + } + + int curve_nid = getEUICCCurveNID(); + const char* curve_param_name = (curve_nid == NID_X9_62_prime256v1) ? "P-256" : "brainpoolP256r1"; + + std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> pctx(EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr), EVP_PKEY_CTX_free); + if (!pctx) { + throw OpenSSLError("Failed to create EVP_PKEY_CTX"); + } + + if (EVP_PKEY_fromdata_init(pctx.get()) <= 0) { + throw OpenSSLError("Failed to initialize fromdata"); + } + + OSSL_PARAM_BLD* param_bld = OSSL_PARAM_BLD_new(); + if (!param_bld) { + throw OpenSSLError("Failed to create OSSL_PARAM_BLD"); + } + + if (!OSSL_PARAM_BLD_push_utf8_string(param_bld, OSSL_PKEY_PARAM_GROUP_NAME, curve_param_name, 0)) { + OSSL_PARAM_BLD_free(param_bld); + throw OpenSSLError("Failed to set curve name"); + } + + if (!OSSL_PARAM_BLD_push_octet_string(param_bld, OSSL_PKEY_PARAM_PUB_KEY, otherPublicKey.data(), otherPublicKey.size())) { + OSSL_PARAM_BLD_free(param_bld); + throw OpenSSLError("Failed to set public key"); + } + + OSSL_PARAM* params = OSSL_PARAM_BLD_to_param(param_bld); + if (!params) { + OSSL_PARAM_BLD_free(param_bld); + throw OpenSSLError("Failed to build params"); + } + + EVP_PKEY* other_pkey_raw = nullptr; + if (EVP_PKEY_fromdata(pctx.get(), &other_pkey_raw, EVP_PKEY_PUBLIC_KEY, params) <= 0) { + OSSL_PARAM_free(params); + OSSL_PARAM_BLD_free(param_bld); + throw OpenSSLError("Failed to create public key from data"); + } + + OSSL_PARAM_free(params); + OSSL_PARAM_BLD_free(param_bld); + + std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> other_pkey(other_pkey_raw); + + std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> derive_ctx(EVP_PKEY_CTX_new(m_euicc_ot_PrivateKey.get(), nullptr), EVP_PKEY_CTX_free); + if (!derive_ctx) { + throw OpenSSLError("Failed to create derive context"); + } + + if (EVP_PKEY_derive_init(derive_ctx.get()) <= 0) { + throw OpenSSLError("Failed to initialize key derivation"); + } + + if (EVP_PKEY_derive_set_peer(derive_ctx.get(), other_pkey.get()) <= 0) { + throw OpenSSLError("Failed to set peer key"); + } + + size_t secret_len = 0; + if (EVP_PKEY_derive(derive_ctx.get(), nullptr, &secret_len) <= 0) { + throw OpenSSLError("Failed to get shared secret length"); + } + + std::vector<uint8_t> shared_secret(secret_len); + if (EVP_PKEY_derive(derive_ctx.get(), shared_secret.data(), &secret_len) <= 0) { + throw OpenSSLError("ECDH computation failed"); + } + + shared_secret.resize(secret_len); + + LOG_INFO("Computed ECDH shared secret: " + HexUtil::bytesToHex(shared_secret)); + return shared_secret; +} + +std::vector<uint8_t> RSPClient::signDataWithEUICC(const std::vector<uint8_t>& dataToSign) { + if (!m_euiccPrivateKey) { + throw std::runtime_error("eUICC private key not available for signing"); + } + auto rv = signDataWithKey(dataToSign, m_euiccPrivateKey.get()); + return convertDER_to_BSI_TR03111(rv); +} + +bool RSPClient::verifyServerSignature(const std::vector<uint8_t>& serverSigned1, const std::vector<uint8_t>& serverSignature1, + const std::vector<uint8_t>& derDataServerCert) { + auto serverCert = CertificateUtil::loadCertFromDER(derDataServerCert); + return verifyServerSignature(serverSigned1, serverSignature1, serverCert.get()); +} + +void RSPClient::setConfirmationCode(const std::string& confirmationCode) { + m_confirmationCode = confirmationCode; + if (!m_transactionId.empty()) { + m_confirmationCodeHash = computeConfirmationCodeHash(m_confirmationCode, m_transactionId); + LOG_INFO("Set confirmation code, computed hash: " + HexUtil::bytesToHex(m_confirmationCodeHash)); + } else { + LOG_ERROR("Cannot compute confirmation code hash - transaction ID not set"); + } +} + +void RSPClient::setTransactionId(const std::vector<uint8_t>& transactionId) { + m_transactionId = transactionId; + LOG_INFO("Set transaction ID: " + HexUtil::bytesToHex(transactionId)); + // If confirmation code was already set, recalculate hash + if (!m_confirmationCode.empty()) { + m_confirmationCodeHash = computeConfirmationCodeHash(m_confirmationCode, m_transactionId); + LOG_INFO("Recalculated confirmation code hash: " + HexUtil::bytesToHex(m_confirmationCodeHash)); + } +} + +std::vector<uint8_t> RSPClient::getConfirmationCodeHash() const { + return m_confirmationCodeHash; +} + +void RSPClient::setConfirmationCodeHash(const std::vector<uint8_t>& hash) { + m_confirmationCodeHash = hash; + LOG_INFO("Set confirmation code hash directly: " + HexUtil::bytesToHex(hash)); +} + +std::vector<uint8_t> RSPClient::getEUMCertificate() { + return certToDER(m_eumCert.get()); +} + +std::vector<uint8_t> RSPClient::getEUICCCertificate() { + return certToDER(m_euiccCert.get()); +} + +std::vector<uint8_t> RSPClient::getCICertificate() { + // If no eUICC certificate is loaded, return the primary root CA + if (!m_euiccCert) { + if (m_rootCA) { + return certToDER(m_rootCA.get()); + } + return std::vector<uint8_t>(); + } + + int euicc_curve_nid = getEUICCCurveNID(); + + if (m_rootCA) { + try { + int root_curve_nid = getCertificateCurveNID(m_rootCA.get()); + if (root_curve_nid == euicc_curve_nid) { + LOG_DEBUG("Primary root CA matches eUICC curve type"); + return certToDER(m_rootCA.get()); + } + } catch (const std::exception& e) { + LOG_WARNING("Failed to get curve type from primary root CA: " + std::string(e.what())); + } + } + + // Search for a matching root CA in the certificate pool + for (const auto& cert : m_certPool) { + // Check if this is a self-signed certificate (root CA) + if (X509_check_issued(cert.get(), cert.get()) == X509_V_OK) { + try { + int cert_curve_nid = getCertificateCurveNID(cert.get()); + if (cert_curve_nid == euicc_curve_nid) { + LOG_DEBUG("Found matching root CA in certificate pool for curve type"); + return certToDER(cert.get()); + } + } catch (const std::exception& e) { + LOG_WARNING("Failed to get curve type from certificate: " + std::string(e.what())); + } + } + } + + // If no matching root CA found, return the primary root CA as fallback + LOG_WARNING("No root CA found matching eUICC curve type, returning primary " + "root CA"); + if (m_rootCA) { + return certToDER(m_rootCA.get()); + } + return std::vector<uint8_t>(); +} + +std::vector<uint8_t> RSPClient::getEUICCOtpk() { + return m_euiccOtpk; +} + +// Private RSPClient methods +void RSPClient::loadCertificate(const std::string& certPath, const std::string& certType, std::unique_ptr<X509, X509Deleter>& certStorage) { + xx_loadCertificate(certPath, certType, certStorage); +} + +void RSPClient::loadKeyPair(const std::string& privateKeyPath, const std::string& publicKeyPath, const std::string& keyType, + std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter>& privateKeyStorage, std::vector<uint8_t>& publicKeyStorage) { + xx_loadPrivateKey(privateKeyPath, keyType, privateKeyStorage); + + if (!publicKeyPath.empty() && xx_loadPublicKey(publicKeyPath, keyType, publicKeyStorage)) { + return; + } + + xx_generatePublicKeyFromPrivate(privateKeyStorage, keyType, publicKeyStorage); +} + +std::vector<uint8_t> RSPClient::signDataWithKey(const std::vector<uint8_t>& dataToSign, EVP_PKEY* pkey) { + if (dataToSign.empty()) { + throw std::runtime_error("Data to sign is empty"); + } + + std::unique_ptr<EVP_MD_CTX, EVP_MD_CTX_Deleter> mdctx(EVP_MD_CTX_new()); + if (!mdctx) { + throw OpenSSLError("Failed to create signature context"); + } + + if (EVP_DigestSignInit(mdctx.get(), nullptr, EVP_sha256(), nullptr, pkey) != 1) { + throw OpenSSLError("Failed to initialize signature operation"); + } + + if (EVP_DigestSignUpdate(mdctx.get(), dataToSign.data(), dataToSign.size()) != 1) { + throw OpenSSLError("Failed to update signature"); + } + + size_t sigLen; + if (EVP_DigestSignFinal(mdctx.get(), nullptr, &sigLen) != 1) { + throw OpenSSLError("Failed to determine signature length"); + } + + if (sigLen == 0) { + throw std::runtime_error("signature length is zero"); + } + + std::vector<uint8_t> signature(sigLen); + if (EVP_DigestSignFinal(mdctx.get(), signature.data(), &sigLen) != 1) { + throw OpenSSLError("Failed to create signature"); + } + + signature.resize(sigLen); + LOG_DEBUG("Created signature (" + std::to_string(sigLen) + " bytes) over " + std::to_string(dataToSign.size()) + " bytes of data"); + + return signature; +} + +bool RSPClient::verifyServerSignature(const std::vector<uint8_t>& signedData, const std::vector<uint8_t>& signature, X509* scertdata) { + try { + // Handle prefix in serverSignature1 based on SGP.22 spec + const unsigned char* signatureData = signature.data(); + size_t signatureLen = signature.size(); + + if (signatureLen >= 3 && signatureData[0] == 0x5f && signatureData[1] == 0x37 && signatureData[2] == 0x40) { + LOG_DEBUG("Detected 5F3740 prefix in serverSignature1, skipping for " + "verification"); + signatureData += 3; + signatureLen -= 3; + } + + std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> pubkey(X509_get_pubkey(scertdata)); + if (!pubkey) { + throw OpenSSLError("Failed to extract public key from certificate"); + } + + auto ski = CertificateUtil::getSubjectKeyIdentifier(scertdata); + LOG_DEBUG(" using cert with this SKI to verify sig: " + HexUtil::bytesToHex(ski)); + + auto verifyResult = verify_TR031111(signedData, std::vector<unsigned char>(signatureData, signatureData + signatureLen), pubkey.get()); + + if (verifyResult) { + LOG_INFO("Signature verification successful!"); + return true; + } else { + LOG_ERROR("Signature verification failed - invalid signature"); + return false; + } + } catch (const std::exception& e) { + LOG_ERROR("Error verifying signature: " + std::string(e.what())); + return false; + } +} + +bool RSPClient::verifyServerSignature(const std::vector<uint8_t>& serverSigned1, const std::vector<uint8_t>& serverSignature1) { + return verifyServerSignature(serverSigned1, serverSignature1, m_serverCert.get()); +} + +void RSPClient::configureHttpClient(bool useCustomTlsCert, const std::string& customTlsCertPath) { + m_useCustomTlsCert = useCustomTlsCert; + m_customTlsCertPath = customTlsCertPath; +} + +// Set authentication parameters for mutual TLS auth +void RSPClient::setAuthParams(bool useMutualTLS, const std::string& clientCertPath, const std::string& clientKeyPath) { + m_useMutualTLS = useMutualTLS; + m_clientCertPath = clientCertPath; + m_clientKeyPath = clientKeyPath; + LOG_DEBUG("Auth params set: mutualTLS=" + std::to_string(useMutualTLS) + ", cert=" + clientCertPath + ", key=" + clientKeyPath); +} + +// Send HTTPS POST without client authentication (for ES9+) +std::string RSPClient::sendHttpsPost(const std::string& endpoint, const std::string& body, int& httpStatusCode, unsigned int portOverride) { + return sendHttpsPostUnified(endpoint, body, httpStatusCode, portOverride); +} + +// Send HTTPS POST with pre-configured client authentication (for ES2+) +std::string RSPClient::sendHttpsPostWithAuth(const std::string& endpoint, const std::string& body, int& httpStatusCode, unsigned int portOverride) { + if (!m_useMutualTLS) { + LOG_WARNING("sendHttpsPostWithAuth called but mutual TLS not configured"); + } + return sendHttpsPostUnified(endpoint, body, httpStatusCode, portOverride, m_useMutualTLS, m_clientCertPath, m_clientKeyPath); +} + +std::string RSPClient::sendHttpsPostWithContentType(const std::string& endpoint, const std::string& body, const std::string& contentType, int& httpStatusCode, + unsigned int portOverride) { + LOG_DEBUG("sendHttpsPostWithContentType: endpoint=" + endpoint + ", contentType=" + contentType + ", bodySize=" + std::to_string(body.size()) + + ", port=" + std::to_string(portOverride)); + return sendHttpsPostUnified(endpoint, body, httpStatusCode, portOverride, false, "", "", contentType); +} + +std::string RSPClient::sendHttpsPostUnified(const std::string& endpoint, const std::string& body, int& httpStatusCode, unsigned int portOverride, + bool useMutualTLS, const std::string& clientCertPath, const std::string& clientKeyPath, + const std::string& contentType) { + if (!m_httpClient) { + m_httpClient = std::make_unique<HttpClient>(); + } + + // full URL without port (port set via CURLOPT_PORT) + std::string url = "https://" + m_serverUrl + endpoint; + + // Build certificate pool for server verification + std::vector<X509*> rawCertPool; + for (const auto& cert : m_certPool) { + rawCertPool.push_back(cert.get()); + } + + // Add custom TLS server certificate if configured globally + if (m_useCustomTlsCert && !m_customTlsCertPath.empty()) { + try { + auto tlsCerts = CertificateUtil::loadCertificateChain(m_customTlsCertPath); + for (auto& cert : tlsCerts) { + rawCertPool.push_back(cert.get()); + } + LOG_DEBUG("Added custom TLS server certificate from: " + m_customTlsCertPath); + } catch (const std::exception& e) { + LOG_WARNING("Failed to load custom TLS server certificate: " + std::string(e.what())); + } + } + + HttpClient::PostConfig config; + config.useMutualTLS = useMutualTLS; + config.clientCertPath = clientCertPath; + config.clientKeyPath = clientKeyPath; + config.includeAdminProtocolHeader = useMutualTLS; // ES2+ requires this header + config.verboseOutput = false; + config.contentType = contentType; + + LOG_DEBUG("Sending " + std::string(useMutualTLS ? "ES2+ request with mutual TLS" : "HTTPS request") + " to: " + endpoint + + " (port: " + std::to_string(portOverride) + ")"); + + HttpClient::ResponseData response = m_httpClient->postJson(url, portOverride, body, nullptr, rawCertPool, config); + + httpStatusCode = response.statusCode; + + LOG_DEBUG("HTTP " + std::to_string(httpStatusCode) + " response, body length: " + std::to_string(response.body.length())); + + return response.body; +} + +} // namespace RspCrypto \ No newline at end of file diff --git a/smdpp/rsp_client.cpp b/smdpp/rsp_client.cpp deleted file mode 100644 index 97f5a5a..0000000 --- a/smdpp/rsp_client.cpp +++ /dev/null @@ -1,1737 +0,0 @@ -/* RSP Cryptographic Operations Implementation - * - * Author: Eric Wild <ewild(a)sysmocom.de> / sysmocom - s.f.m.c. GmbH - * - * Released under the terms of GNU General Public License, Version 2 or - * (at your option) any later version. - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include <algorithm> -#include <chrono> -#include <cstddef> -#include <ctime> -#include <filesystem> -#include <fstream> -#include <iomanip> -#include <iostream> -#include <iterator> -#include <map> -#include <memory> -#include <mutex> -#include <set> -#include <sstream> -#include <stdexcept> -#include <string> -#include <vector> - -extern "C" { -#include <dirent.h> -#include <sys/stat.h> -#include <sys/types.h> - -#include <openssl/asn1.h> -#include <openssl/asn1t.h> -#include <openssl/bio.h> -#include <openssl/buffer.h> -#include <openssl/core_names.h> -#include <openssl/crypto.h> -#include <openssl/err.h> -#include <openssl/evp.h> -#include <openssl/param_build.h> -#include <openssl/pem.h> -#include <openssl/rand.h> -#include <openssl/types.h> - -#include <openssl/cmac.h> -#include <openssl/ec.h> -#include <openssl/sha.h> -#include <openssl/ssl.h> - -#include <openssl/safestack.h> -#include <openssl/stack.h> -#include <openssl/x509.h> -#include <openssl/x509_vfy.h> -#include <openssl/x509v3.h> - -#include <curl/curl.h> -} - -#include "helpers.h" -#include "logger.h" -namespace RspCrypto { - -class CertificateUtil { - private: - static std::unique_ptr<BIO, BIODeleter> readFileToBIO(const std::string& filePath) { - std::ifstream file(filePath); - if (!file) { - throw std::runtime_error("Failed to open file: " + filePath); - } - - std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); - std::unique_ptr<BIO, BIODeleter> bio(BIO_new_mem_buf(content.c_str(), -1)); - if (!bio) { - throw OpenSSLError("Failed to create BIO from file content"); - } - - return bio; - } - - static std::vector<uint8_t> extractPublicKeyData(EVP_PKEY* key) { - size_t pub_len = 0; - if (EVP_PKEY_get_octet_string_param(key, OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &pub_len) != 1) { - throw OpenSSLError("Failed to get public key size"); - } - - std::vector<uint8_t> publicKeyData(pub_len); - if (EVP_PKEY_get_octet_string_param(key, OSSL_PKEY_PARAM_PUB_KEY, publicKeyData.data(), pub_len, &pub_len) != 1) { - throw OpenSSLError("Failed to extract public key"); - } - - return publicKeyData; - } - - static std::vector<uint8_t> getCertificateExtension(X509* cert, int nid) { - int loc = X509_get_ext_by_NID(cert, nid, -1); - if (loc < 0) { - return std::vector<uint8_t>(); - } - - X509_EXTENSION* ext = X509_get_ext(cert, loc); - if (!ext) { - return std::vector<uint8_t>(); - } - - ASN1_OCTET_STRING* ext_data = X509_EXTENSION_get_data(ext); - if (!ext_data) { - return std::vector<uint8_t>(); - } - - return std::vector<uint8_t>(ASN1_STRING_get0_data(ext_data), ASN1_STRING_get0_data(ext_data) + ASN1_STRING_length(ext_data)); - } - - public: - static std::string getEID(X509* cert) { - X509_NAME* subject = X509_get_subject_name(cert); - if (!subject) { - return ""; - } - - // EID is stored in serialNumber field - int index = X509_NAME_get_index_by_NID(subject, NID_serialNumber, -1); - if (index < 0) { - return ""; - } - - X509_NAME_ENTRY* entry = X509_NAME_get_entry(subject, index); - if (!entry) { - return ""; - } - - ASN1_STRING* asn1_str = X509_NAME_ENTRY_get_data(entry); - if (!asn1_str) { - return ""; - } - - const unsigned char* str_data = ASN1_STRING_get0_data(asn1_str); - int str_len = ASN1_STRING_length(asn1_str); - - return std::string(reinterpret_cast<const char*>(str_data), str_len); - } - - static std::string getSubjectName(X509* cert) { - char subjectName[256]; - X509_NAME_oneline(X509_get_subject_name(cert), subjectName, sizeof(subjectName)); - return std::string(subjectName); - } - - static std::vector<uint8_t> getSubjectKeyIdentifier(X509* cert) { - auto ext_data = getCertificateExtension(cert, NID_subject_key_identifier); - if (ext_data.empty()) { - return std::vector<uint8_t>(); - } - - const unsigned char* p = ext_data.data(); - long xlen = ext_data.size(); - int tag, xclass; - - ASN1_get_object(&p, &xlen, &tag, &xclass, ext_data.size()); - - return std::vector<uint8_t>(p, p + xlen); - } - - static std::vector<uint8_t> getAuthorityKeyIdentifier(X509* cert) { - auto ext_data = getCertificateExtension(cert, NID_authority_key_identifier); - if (ext_data.empty()) { - return std::vector<uint8_t>(); - } - - // AKI is more complex, need to extract keyid - const unsigned char* p = ext_data.data(); - AUTHORITY_KEYID* akid = d2i_AUTHORITY_KEYID(NULL, &p, ext_data.size()); - - std::vector<uint8_t> aki; - if (akid && akid->keyid) { - aki.assign(akid->keyid->data, akid->keyid->data + akid->keyid->length); - AUTHORITY_KEYID_free(akid); - } - - return aki; - } - - static std::unique_ptr<X509, X509Deleter> loadCertFromDER(const std::vector<uint8_t>& derData) { - const unsigned char* p = derData.data(); - X509* cert = d2i_X509(nullptr, &p, derData.size()); - - if (!cert) { - throw OpenSSLError("Failed to parse DER certificate"); - } - - return std::unique_ptr<X509, X509Deleter>(cert); - } - - static std::vector<std::unique_ptr<X509, X509Deleter>> loadCertificateChain(const std::string& filename) { - std::vector<std::unique_ptr<X509, X509Deleter>> chain; - - if (std::filesystem::path(filename).extension() == ".der") { - FILE* file = fopen(filename.c_str(), "rb"); - if (!file) { - throw std::runtime_error("No certificates found in file: " + filename); - } - - X509* cert = d2i_X509_fp(file, nullptr); - chain.push_back(std::unique_ptr<X509, X509Deleter>(cert)); - fclose(file); - - } else { - BIO* bio = BIO_new_file(filename.c_str(), "r"); - if (!bio) { - throw OpenSSLError("Failed to open certificate file: " + filename); - } - X509* cert = nullptr; - while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != nullptr) { - chain.push_back(std::unique_ptr<X509, X509Deleter>(cert)); - } - - BIO_free(bio); - } - - if (chain.empty()) { - throw std::runtime_error("No certificates found in file: " + filename); - } - - return chain; - } - - static std::vector<uint8_t> certToDER(X509* cert) { - BIO* bio = BIO_new(BIO_s_mem()); - if (!bio) { - throw OpenSSLError("Failed to create BIO for certificate conversion"); - } - - i2d_X509_bio(bio, cert); - BUF_MEM* bufferPtr; - BIO_get_mem_ptr(bio, &bufferPtr); - - std::vector<uint8_t> der; - der.assign(bufferPtr->data, bufferPtr->data + bufferPtr->length); - BIO_free(bio); - - return der; - } - - static bool verifyCertificateChainDynamic(X509* cert, const std::vector<X509*>& certPool, X509* rootCA = nullptr, bool verbose = false) { - LOG_INFO("Verifying certificate chain..."); - LOG_INFO("Target certificate: " + getSubjectName(cert)); - - // certificate store for trusted roots only - std::unique_ptr<X509_STORE, X509_STORE_Deleter> store(X509_STORE_new()); - if (!store) { - throw OpenSSLError("Failed to create X509_STORE"); - } - - // Only add the explicitly provided root CA to the trust store - if (rootCA) { - if (verbose) { - LOG_INFO("Adding trusted root CA: " + getSubjectName(rootCA)); - } - if (X509_STORE_add_cert(store.get(), rootCA) != 1) { - throw OpenSSLError("Failed to add root CA to store"); - } - } else { - // If no root CA provided, find self-signed certificates in the pool - LOG_WARNING("No explicit root CA provided - searching for self-signed " - "certificates"); - for (auto candidate : certPool) { - if (X509_check_issued(candidate, candidate) == X509_V_OK) { - if (verbose) { - LOG_INFO("Adding self-signed certificate as trusted root: " + getSubjectName(candidate)); - } - if (X509_STORE_add_cert(store.get(), candidate) != 1) { - unsigned long err = ERR_peek_last_error(); - // Ignore duplicate certificate errors - if (ERR_GET_REASON(err) != X509_R_CERT_ALREADY_IN_HASH_TABLE) { - throw OpenSSLError("Failed to add root CA to store"); - } - ERR_clear_error(); - } - } - } - } - - // Build untrusted chain from cert pool (excluding the root if it's in - // there) IMPORTANT: custom deleter that only frees the stack, not the - // certificates - auto stack_only_deleter = [](STACK_OF(X509) * stack) { - if (stack) { - sk_X509_free(stack); // This only frees the stack structure, not the contents - } - }; - std::unique_ptr<STACK_OF(X509), decltype(stack_only_deleter)> untrusted_chain(sk_X509_new_null(), stack_only_deleter); - if (!untrusted_chain) { - throw OpenSSLError("Failed to create untrusted chain"); - } - - // Add all certificates from the pool as untrusted intermediates - for (auto poolCert : certPool) { - // Skip if this is the target certificate - if (X509_cmp(poolCert, cert) == 0) { - continue; - } - // Skip if this is the root CA (already in trust store) - if (rootCA && X509_cmp(poolCert, rootCA) == 0) { - continue; - } - // Add to untrusted chain - if (sk_X509_push(untrusted_chain.get(), poolCert) == 0) { - throw OpenSSLError("Failed to add certificate to untrusted chain"); - } - } - - if (verbose) { - LOG_INFO("Untrusted chain contains " + std::to_string(sk_X509_num(untrusted_chain.get())) + " certificates"); - } - - std::unique_ptr<X509_STORE_CTX, X509_STORE_CTX_Deleter> ctx(X509_STORE_CTX_new()); - if (!ctx) { - throw OpenSSLError("Failed to create X509_STORE_CTX"); - } - - if (X509_STORE_CTX_init(ctx.get(), store.get(), cert, untrusted_chain.get()) != 1) { - throw OpenSSLError("Failed to initialize X509_STORE_CTX"); - } - - unsigned long flags = X509_V_FLAG_CHECK_SS_SIGNATURE; - X509_STORE_CTX_set_flags(ctx.get(), flags); - - X509_STORE_CTX_set_verify_cb(ctx.get(), [](int ok, X509_STORE_CTX* ctx) -> int { - if (!ok) { - int error = X509_STORE_CTX_get_error(ctx); - // SGP.22 EUM certificates use name constraints in a specific way - if (error == X509_V_ERR_PERMITTED_VIOLATION || error == X509_V_ERR_EXCLUDED_VIOLATION || error == X509_V_ERR_SUBTREE_MINMAX) { - // Get the certificate that triggered this - X509* cert = X509_STORE_CTX_get_current_cert(ctx); - if (cert) { - char subject_buf[256]; - X509_NAME_oneline(X509_get_subject_name(cert), subject_buf, sizeof(subject_buf)); - LOG_INFO("Processing certificate with name constraint abuse: " + std::string(subject_buf)); - } - - return 1; - } - } - return ok; // Use default behavior for other errors - }); - - int result = X509_verify_cert(ctx.get()); - - if (result != 1) { - int error = X509_STORE_CTX_get_error(ctx.get()); - int depth = X509_STORE_CTX_get_error_depth(ctx.get()); - X509* errorCert = X509_STORE_CTX_get_current_cert(ctx.get()); - - LOG_ERROR("Certificate verification failed:"); - LOG_ERROR(" Error: " + std::string(X509_verify_cert_error_string(error))); - LOG_ERROR(" Depth: " + std::to_string(depth)); - - if (errorCert) { - LOG_ERROR(" Cert: " + getSubjectName(errorCert)); - } - - return false; - } - - LOG_INFO("Certificate verification successful"); - - if (verbose) { - STACK_OF(X509)* verified_chain = X509_STORE_CTX_get1_chain(ctx.get()); - if (verified_chain) { - LOG_INFO("Verified certificate chain:"); - for (int i = 0; i < sk_X509_num(verified_chain); i++) { - X509* chainCert = sk_X509_value(verified_chain, i); - LOG_INFO(" " + std::to_string(i + 1) + ". " + getSubjectName(chainCert)); - } - sk_X509_pop_free(verified_chain, X509_free); - } - } - - return true; - } - - static std::vector<std::filesystem::path> find_cert_files(const std::filesystem::path& root_path, const std::vector<std::string>& name_filters = {}) { - std::vector<std::filesystem::path> result; - - if (!std::filesystem::exists(root_path)) { - std::cerr << "Path does not exist: " << root_path << std::endl; - return result; - } - - auto file_matches = [&name_filters](const std::filesystem::path& path) { - std::string ext = path.extension().string(); - if (!(ext == ".pem" || ext == ".der" || ext == ".crt")) { - return false; - } - - // If no filters specified, accept all matching extensions - if (name_filters.empty()) { - return true; - } - - // Check if filename contains any of the filters - std::string filename = path.filename().string(); - return std::any_of(name_filters.begin(), name_filters.end(), - [&filename](const std::string& filter) { return filename.find(filter) != std::string::npos; }); - }; - - if (std::filesystem::is_regular_file(root_path)) { - if (file_matches(root_path)) { - result.push_back(root_path); - } - } else if (std::filesystem::is_directory(root_path)) { - for (const auto& entry : std::filesystem::recursive_directory_iterator(root_path)) { - if (entry.is_regular_file() && file_matches(entry.path())) { - result.push_back(entry.path()); - } - } - } - - return result; - } - - static std::vector<std::unique_ptr<X509, X509Deleter>> loadCertificatesFromDirectory(const std::string& directory, - const std::vector<std::string>& name_filters) { - std::vector<std::unique_ptr<X509, X509Deleter>> certificates; - - auto files_in_dir = find_cert_files(directory, name_filters); - - for (const auto& entry : files_in_dir) { - std::string ext = entry.extension().string(); - auto fpath = entry.string(); - - try { - if (ext == ".pem" || ext == ".crt") { - auto fileCerts = loadCertificateChain(entry); - for (auto& cert : fileCerts) { - certificates.push_back(std::move(cert)); - } - } else if (ext == ".der") { - FILE* file = fopen(fpath.c_str(), "rb"); - if (!file) { - LOG_WARNING("Failed to open certificate file: " + fpath); - continue; - } - - fseek(file, 0, SEEK_END); - long file_size = ftell(file); - rewind(file); - - std::vector<unsigned char> data(file_size); - if (fread(data.data(), 1, file_size, file) != static_cast<size_t>(file_size)) { - fclose(file); - LOG_WARNING("Failed to read certificate file: " + fpath); - continue; - } - - fclose(file); - - const unsigned char* p = data.data(); - X509* cert = d2i_X509(nullptr, &p, file_size); - - if (cert) { - certificates.push_back(std::unique_ptr<X509, X509Deleter>(cert)); - LOG_INFO("Loaded certificate: " + getSubjectName(cert) + " from " + fpath); - } - } - } catch (const std::exception& e) { - // already logged in called functions - } - } - - return certificates; - } - - static void xx_loadCertificate(const std::string& certPath, const std::string& typeName, std::unique_ptr<X509, X509Deleter>& certStorage) { - try { - auto certs = loadCertificateChain(certPath); - if (certs.empty()) { - throw std::runtime_error("No " + typeName + " certificate found in file: " + certPath); - } - - certStorage = std::move(certs[0]); - LOG_DEBUG("Loaded " + typeName + " certificate: " + getSubjectName(certStorage.get())); - - } catch (const std::exception& e) { - LOG_ERROR("Failed to load " + typeName + " certificate: " + std::string(e.what())); - throw; - } - } - - static void xx_loadPrivateKey(const std::string& keyPath, const std::string& typeName, std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter>& keyStorage) { - try { - if (!std::filesystem::exists(keyPath)) { - throw std::runtime_error(typeName + " private key file not found: " + keyPath); - } - - auto keyBio = readFileToBIO(keyPath); - - keyStorage.reset(PEM_read_bio_PrivateKey(keyBio.get(), nullptr, nullptr, nullptr)); - if (!keyStorage) { - throw OpenSSLError("Failed to read " + typeName + " private key"); - } - - LOG_DEBUG("Loaded " + typeName + " private key from: " + keyPath); - - } catch (const std::exception& e) { - LOG_ERROR("Failed to load " + typeName + " private key: " + std::string(e.what())); - throw; - } - } - - static bool xx_loadPublicKey(const std::string& pubKeyPath, const std::string& typeName, std::vector<uint8_t>& publicKeyStorage) { - try { - if (!std::filesystem::exists(pubKeyPath)) { - LOG_INFO(typeName + " public key file not found: " + pubKeyPath + " (will generate from private key)"); - return false; - } - - auto keyBio = readFileToBIO(pubKeyPath); - - std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> pubKey(PEM_read_bio_PUBKEY(keyBio.get(), nullptr, nullptr, nullptr)); - if (!pubKey) { - LOG_WARNING("Failed to read " + typeName + " public key from file (will generate from private key)"); - return false; - } - - publicKeyStorage = extractPublicKeyData(pubKey.get()); - - LOG_INFO("Loaded " + typeName + " public key from file (" + std::to_string(publicKeyStorage.size()) + " bytes)"); - return true; - - } catch (const std::exception& e) { - LOG_WARNING("Failed to load " + typeName + " public key from file: " + std::string(e.what()) + " (will generate from private key)"); - return false; - } - } - - static void xx_generatePublicKeyFromPrivate(const std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter>& privateKey, const std::string& typeName, - std::vector<uint8_t>& publicKeyStorage) { - if (!privateKey) { - throw std::runtime_error("No " + typeName + " private key available for public key generation"); - } - - publicKeyStorage = extractPublicKeyData(privateKey.get()); - - LOG_DEBUG("Generated " + typeName + " public key from private key (" + std::to_string(publicKeyStorage.size()) + " bytes)"); - } - - static std::vector<std::string> parse_permitted_eins_from_cert(X509* cert) { - std::vector<std::string> permitted_iins; - - bool is_old_variant = false; - int pos = X509_get_ext_by_NID(cert, NID_certificate_policies, -1); - if (pos >= 0) { - X509_EXTENSION* ext = X509_get_ext(cert, pos); - CERTIFICATEPOLICIES* policies = (CERTIFICATEPOLICIES*)X509V3_EXT_d2i(ext); - if (policies) { - for (int i = 0; i < sk_POLICYINFO_num(policies); i++) { - POLICYINFO* policy = sk_POLICYINFO_value(policies, i); - char oid_str[128]; - OBJ_obj2txt(oid_str, sizeof(oid_str), policy->policyid, 1); - if (std::string(oid_str) == "2.23.146.1.2.1.2") { - is_old_variant = true; - break; - } - } - CERTIFICATEPOLICIES_free(policies); - } - } - - if (is_old_variant) { - // Parse from nameConstraints - pos = X509_get_ext_by_NID(cert, NID_name_constraints, -1); - if (pos >= 0) { - X509_EXTENSION* ext = X509_get_ext(cert, pos); - NAME_CONSTRAINTS* nc = (NAME_CONSTRAINTS*)X509V3_EXT_d2i(ext); - if (nc && nc->permittedSubtrees) { - for (int i = 0; i < sk_GENERAL_SUBTREE_num(nc->permittedSubtrees); i++) { - GENERAL_SUBTREE* subtree = sk_GENERAL_SUBTREE_value(nc->permittedSubtrees, i); - if (subtree->base->type == GEN_DIRNAME) { - X509_NAME* name = subtree->base->d.directoryName; - int idx = X509_NAME_get_index_by_NID(name, NID_serialNumber, -1); - if (idx >= 0) { - X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, idx); - ASN1_STRING* asn1_str = X509_NAME_ENTRY_get_data(entry); - std::string iin(reinterpret_cast<const char*>(ASN1_STRING_get0_data(asn1_str)), - ASN1_STRING_length(asn1_str)); - permitted_iins.push_back(iin); - } - } - } - } - if (nc) - NAME_CONSTRAINTS_free(nc); - } - } else { - // Parse from GSMA permittedEins extension - ASN1_OBJECT* obj = OBJ_txt2obj("2.23.146.1.2.2.0", 1); - pos = X509_get_ext_by_OBJ(cert, obj, -1); - ASN1_OBJECT_free(obj); - - if (pos >= 0) { - X509_EXTENSION* ext = X509_get_ext(cert, pos); - ASN1_OCTET_STRING* ext_data = X509_EXTENSION_get_data(ext); - const unsigned char* p = ext_data->data; - long remaining = ext_data->length; - - // SEQUENCE OF PrintableString - int tag, xclass; - long xlen; - - // SEQUENCE tag - ASN1_get_object(&p, &xlen, &tag, &xclass, remaining); - if (tag == V_ASN1_SEQUENCE) { - remaining = xlen; - - while (remaining > 0) { - const unsigned char* elem_start = p; - ASN1_get_object(&p, &xlen, &tag, &xclass, remaining); - - if (tag == V_ASN1_PRINTABLESTRING) { - std::string iin(reinterpret_cast<const char*>(p), xlen); - permitted_iins.push_back(iin); - } - - p += xlen; - remaining -= (p - elem_start); - } - } - } - } - - return permitted_iins; - } - - static std::string getPermittedEINs(const std::vector<uint8_t>& eumCertData) { - try { - auto cert = loadCertFromDER(eumCertData); - auto eins = parse_permitted_eins_from_cert(cert.get()); - - // Join EINs with comma - std::string result; - for (size_t i = 0; i < eins.size(); i++) { - if (i > 0) - result += ","; - result += eins[i]; - } - - return result; - } catch (const std::exception& e) { - LOG_ERROR("getPermittedEINs failed: " + std::string(e.what())); - return ""; - } - } - - static bool validateEIDRange(const std::string& eid, const std::vector<uint8_t>& eumCertData) { - try { - auto eumCert = loadCertFromDER(eumCertData); - std::vector<std::string> permittedEins = parse_permitted_eins_from_cert(eumCert.get()); - - if (permittedEins.empty()) { - LOG_WARNING("No permitted EINs found in EUM certificate"); - return false; - } - - // Check if EID starts with any permitted EIN - std::string eidNormalized = eid; - std::transform(eidNormalized.begin(), eidNormalized.end(), eidNormalized.begin(), ::toupper); - - for (const auto& ein : permittedEins) { - if (eidNormalized.find(ein) == 0) { - LOG_INFO("EID " + eidNormalized + " matches permitted EIN " + ein); - return true; - } - } - - LOG_ERROR("EID " + eidNormalized + " is not in any permitted EIN list"); - return false; - } catch (const std::exception& e) { - LOG_ERROR("validateEIDRange failed: " + std::string(e.what())); - return false; - } - } - - static bool hasRSPRole(const std::vector<uint8_t>& certData, const std::string& roleOid) { - try { - auto cert = loadCertFromDER(certData); - - int pos = X509_get_ext_by_NID(cert.get(), NID_certificate_policies, -1); - if (pos < 0) - return false; - - X509_EXTENSION* ext = X509_get_ext(cert.get(), pos); - CERTIFICATEPOLICIES* policies = (CERTIFICATEPOLICIES*)X509V3_EXT_d2i(ext); - - if (!policies) - return false; - - for (int i = 0; i < sk_POLICYINFO_num(policies); i++) { - POLICYINFO* policy = sk_POLICYINFO_value(policies, i); - char oid_str[128]; - OBJ_obj2txt(oid_str, sizeof(oid_str), policy->policyid, 1); - - if (std::string(oid_str) == roleOid) { - CERTIFICATEPOLICIES_free(policies); - return true; - } - } - - CERTIFICATEPOLICIES_free(policies); - return false; - } catch (const std::exception& e) { - LOG_ERROR("hasRSPRole failed: " + std::string(e.what())); - return false; - } - } - - static std::string getCurveOID(const std::vector<uint8_t>& certData) { - try { - auto cert = loadCertFromDER(certData); - - EVP_PKEY* pkey = X509_get0_pubkey(cert.get()); - if (!pkey) { - LOG_ERROR("Failed to get public key from certificate"); - return ""; - } - - if (EVP_PKEY_base_id(pkey) != EVP_PKEY_EC) { - LOG_ERROR("Certificate does not contain EC key"); - return ""; - } - - char curve_name[256] = { 0 }; - size_t curve_name_len = sizeof(curve_name); - - if (EVP_PKEY_get_utf8_string_param(pkey, "group", curve_name, sizeof(curve_name), &curve_name_len) != 1) { - LOG_ERROR("Failed to get curve name from EC key"); - return ""; - } - - std::string curve_str(curve_name); - - if (curve_str == "prime256v1") { - return "prime256v1"; - } else if (curve_str == "secp384r1") { - return "secp384r1"; - } else if (curve_str == "brainpoolP256r1") { - return "brainpoolP256r1"; - } else if (curve_str == "brainpoolP384r1") { - return "brainpoolP384r1"; - } - - return "unknown"; - } catch (const std::exception& e) { - LOG_ERROR("getCurveOID failed: " + std::string(e.what())); - return ""; - } - } - - // ECDH compatible (same curve) - static bool verifyECDHCompatible(const std::vector<uint8_t>& pubKey1, const std::vector<uint8_t>& pubKey2) { - try { - // Both should be uncompressed EC points starting with 0x04 - if (pubKey1.empty() || pubKey2.empty() || pubKey1[0] != 0x04 || pubKey2[0] != 0x04) { - LOG_ERROR("Invalid EC public key format"); - return false; - } - - // For P-256, size should be 65 bytes (1 + 32 + 32) - if (pubKey1.size() == 65 && pubKey2.size() == 65) { - LOG_INFO("Both keys are P-256 format, ECDH compatible"); - return true; - } - - // For P-384, size should be 97 bytes (1 + 48 + 48) - if (pubKey1.size() == 97 && pubKey2.size() == 97) { - LOG_INFO("Both keys are P-384 format, ECDH compatible"); - return true; - } - - LOG_ERROR("Key sizes don't match or unsupported curve"); - return false; - } catch (const std::exception& e) { - LOG_ERROR("verifyECDHCompatible failed: " + std::string(e.what())); - return false; - } - } - - static std::vector<uint8_t> convertBSI_TR03111_to_DER(const std::vector<uint8_t>& bsi_signature) { - if (bsi_signature.size() != 64) { - std::cerr << "Invalid BSI TR-03111 signature size: " << bsi_signature.size() << std::endl; - return {}; - } - - BIGNUM* r = BN_new(); - BIGNUM* s = BN_new(); - - if (!r || !s) { - if (r) - BN_free(r); - if (s) - BN_free(s); - return {}; - } - - BN_bin2bn(bsi_signature.data(), 32, r); - BN_bin2bn(bsi_signature.data() + 32, 32, s); - - ECDSA_SIG* ecdsa_sig = ECDSA_SIG_new(); - if (!ecdsa_sig) { - BN_free(r); - BN_free(s); - return {}; - } - - ECDSA_SIG_set0(ecdsa_sig, r, s); - - int der_len = i2d_ECDSA_SIG(ecdsa_sig, nullptr); - if (der_len <= 0) { - ECDSA_SIG_free(ecdsa_sig); - return {}; - } - - std::vector<uint8_t> der_signature(der_len); - unsigned char* der_ptr = der_signature.data(); - i2d_ECDSA_SIG(ecdsa_sig, &der_ptr); - - ECDSA_SIG_free(ecdsa_sig); - - return der_signature; - } - - static std::vector<uint8_t> convertDER_to_BSI_TR03111(const std::vector<uint8_t>& der_signature) { - if (der_signature.empty()) { - std::cerr << "Empty DER signature" << std::endl; - return {}; - } - - const unsigned char* der_ptr = der_signature.data(); - ECDSA_SIG* ecdsa_sig = d2i_ECDSA_SIG(nullptr, &der_ptr, der_signature.size()); - - if (!ecdsa_sig) { - std::cerr << "Failed to parse DER signature" << std::endl; - return {}; - } - - const BIGNUM *r, *s; - ECDSA_SIG_get0(ecdsa_sig, &r, &s); - - if (!r || !s) { - ECDSA_SIG_free(ecdsa_sig); - return {}; - } - - std::vector<uint8_t> bsi_signature(64, 0); - - int r_len = BN_num_bytes(r); - int s_len = BN_num_bytes(s); - - if (r_len > 32 || s_len > 32) { - std::cerr << "BIGNUM values too large for BSI TR-03111 format" << std::endl; - ECDSA_SIG_free(ecdsa_sig); - return {}; - } - - // Convert r to bytes (with leading zeros padding) - int r_offset = 32 - r_len; - BN_bn2bin(r, bsi_signature.data() + r_offset); - - // Convert s to bytes (with leading zeros padding) - int s_offset = 64 - s_len; - BN_bn2bin(s, bsi_signature.data() + s_offset); - - ECDSA_SIG_free(ecdsa_sig); - - return bsi_signature; - } - - static bool verify_TR031111(const std::vector<uint8_t>& message, const std::vector<uint8_t>& bsi_signature, EVP_PKEY* publicKey) { - std::vector<uint8_t> der_signature = convertBSI_TR03111_to_DER(bsi_signature); - if (der_signature.empty()) { - std::cerr << "Failed to convert signature to DER format" << std::endl; - return false; - } - - std::unique_ptr<EVP_MD_CTX, EVP_MD_CTX_Deleter> mdctx(EVP_MD_CTX_new()); - if (!mdctx.get()) { - std::cerr << "Failed to create MD context" << std::endl; - return false; - } - - if (EVP_DigestVerifyInit(mdctx.get(), nullptr, EVP_sha256(), nullptr, publicKey) != 1) { - std::cerr << "Failed to initialize digest verify" << std::endl; - return false; - } - - if (EVP_DigestVerifyUpdate(mdctx.get(), message.data(), message.size()) != 1) { - std::cerr << "Failed to update digest verify" << std::endl; - return false; - } - - int result = EVP_DigestVerifyFinal(mdctx.get(), der_signature.data(), der_signature.size()); - - return result == 1; - } -}; - -class HttpClient { - public: - HttpClient() { - // should be called once per application - curl_global_init(CURL_GLOBAL_DEFAULT); - } - - ~HttpClient() { - curl_global_cleanup(); - } - - struct ResponseData { - std::string body; - long statusCode; - std::string headers; - }; - - struct PostConfig { - bool useMutualTLS = false; - std::string clientCertPath; - std::string clientKeyPath; - bool includeAdminProtocolHeader = false; - bool verboseOutput = false; - std::string contentType = "application/json"; - }; - - ResponseData postJson(const std::string& url, unsigned int port, const std::string& jsonData, X509_STORE* store, std::vector<X509*>& certPool, - const PostConfig& config); - - private: - static size_t writeCallback(void* contents, size_t size, size_t nmemb, void* userp) { - size_t realSize = size * nmemb; - auto* response = static_cast<std::string*>(userp); - response->append(static_cast<char*>(contents), realSize); - return realSize; - } - - static size_t headerCallback(void* contents, size_t size, size_t nmemb, void* userp) { - size_t realSize = size * nmemb; - auto* headers = static_cast<std::string*>(userp); - headers->append(static_cast<char*>(contents), realSize); - return realSize; - } - - struct SslCtxData { - X509_STORE* store; - std::vector<X509*>* certPool; - bool verifyResult; - std::string errorMessage; - }; - - static std::string get_cn_name(X509_NAME* const name) { - int idx = -1; - unsigned char* utf8 = NULL; - X509_NAME_ENTRY* entry; - ASN1_STRING* data; - - if (!name) - return {}; - - idx = X509_NAME_get_index_by_NID(name, NID_commonName, -1); - if (idx < 0) - return {}; - - entry = X509_NAME_get_entry(name, idx); - if (!entry) - return {}; - - data = X509_NAME_ENTRY_get_data(entry); - if (!data) - return {}; - - if (!ASN1_STRING_to_UTF8(&utf8, data) || !utf8) - return {}; - - std::string retstr((char*)utf8); - OPENSSL_free(utf8); - return retstr; - } - - static CURLcode sslCtxFunction(CURL* curl, SSL_CTX* sslCtx, void* arg) { - SslCtxData* ctxData = static_cast<SslCtxData*>(arg); - SSL_CTX* ctx = (SSL_CTX*)sslCtx; - X509_STORE* store = SSL_CTX_get_cert_store(ctx); - - for (auto cert : *ctxData->certPool) { - LOG_DEBUG("ADDED Issuer: " + get_cn_name(X509_get_issuer_name(cert)) + " Subject: " + get_cn_name(X509_get_subject_name(cert)) + - " SKI: " + HexUtil::bytesToHex(CertificateUtil::getSubjectKeyIdentifier(cert)) + - " AKI: " + HexUtil::bytesToHex(CertificateUtil::getAuthorityKeyIdentifier(cert))); - - auto result = X509_STORE_add_cert(store, cert); - if (result != 1) { - printf("Warning: Could not add certificate\n"); - - unsigned long err = ERR_get_error(); - if (ERR_GET_REASON(err) != X509_R_CERT_ALREADY_IN_HASH_TABLE) { - ERR_print_errors_fp(stderr); - } - } - } - - SSL_CTX_set_verify(sslCtx, SSL_VERIFY_PEER, nullptr); - - return CURLE_OK; - } -}; - -inline HttpClient::ResponseData HttpClient::postJson(const std::string& url, unsigned int port, const std::string& jsonData, X509_STORE* store, - std::vector<X509*>& certPool, const PostConfig& config) { - ResponseData response; - CURL* curl = curl_easy_init(); - - if (!curl) { - throw std::runtime_error("Failed to initialize CURL"); - } - - struct curl_slist* headers = nullptr; - std::string contentTypeHeader = "Content-Type: " + config.contentType; - if (config.useMutualTLS) { - if (config.contentType == "application/json") { - contentTypeHeader += ";charset=UTF-8"; - } - headers = curl_slist_append(headers, contentTypeHeader.c_str()); - headers = curl_slist_append(headers, "Accept: application/json"); - if (config.includeAdminProtocolHeader) { - headers = curl_slist_append(headers, "X-Admin-Protocol: gsma/rsp/v2.5.0"); - } - } else { - headers = curl_slist_append(headers, contentTypeHeader.c_str()); - // For ASN.1, accept the same content type - std::string acceptHeader = "Accept: " + (config.contentType == "application/x-gsma-rsp-asn1" ? config.contentType : "application/json"); - headers = curl_slist_append(headers, acceptHeader.c_str()); - // Always add X-Admin-Protocol for ES9+ regardless of content type - headers = curl_slist_append(headers, "X-Admin-Protocol: gsma/rsp/v2.5.0"); - } - - SslCtxData ctxData = { .store = store, .certPool = &certPool, .verifyResult = false, .errorMessage = "" }; - - try { - // Basic CURL setup - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_PORT, port); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonData.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, jsonData.size()); // Important for binary data - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body); - curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback); - curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response.headers); - - // Enable SSL verification - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); - - // Set reasonable timeouts - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); - curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); - - if (config.useMutualTLS) { - if (!config.clientCertPath.empty()) { - curl_easy_setopt(curl, CURLOPT_SSLCERT, config.clientCertPath.c_str()); - - std::string certType = "PEM"; - if (config.clientCertPath.find(".der") != std::string::npos || config.clientCertPath.find(".DER") != std::string::npos) { - certType = "DER"; - } - curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, certType.c_str()); - LOG_DEBUG("Set client certificate: " + config.clientCertPath + " (type: " + certType + ")"); - } - - if (!config.clientKeyPath.empty()) { - curl_easy_setopt(curl, CURLOPT_SSLKEY, config.clientKeyPath.c_str()); - curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "PEM"); - LOG_DEBUG("Set client private key: " + config.clientKeyPath); - } - } - - // Use our custom SSL context function for server cert verification - curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, sslCtxFunction); - curl_easy_setopt(curl, CURLOPT_SSL_CTX_DATA, &ctxData); - - curl_easy_setopt(curl, CURLOPT_VERBOSE, config.verboseOutput ? 1L : 0L); - - CURLcode res = curl_easy_perform(curl); - - if (res != CURLE_OK) { - throw std::runtime_error(std::string("CURL request failed: ") + curl_easy_strerror(res)); - } - - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.statusCode); - - curl_slist_free_all(headers); - curl_easy_cleanup(curl); - - return response; - } catch (...) { - curl_slist_free_all(headers); - curl_easy_cleanup(curl); - throw; - } -} - -// Hashed Confirmation Code = SHA256(SHA256(Confirmation Code) | TransactionID) -static std::vector<uint8_t> computeConfirmationCodeHash(const std::string& confirmationCode, const std::vector<uint8_t>& transactionId) { - // Convert confirmation code from hex string to bytes (like EID handling) - std::vector<uint8_t> ccBytes; - for (size_t i = 0; i < confirmationCode.length(); i += 2) { - if (i + 1 < confirmationCode.length()) { - std::string byteString = confirmationCode.substr(i, 2); - uint8_t byte = static_cast<uint8_t>(std::stoi(byteString, nullptr, 16)); - ccBytes.push_back(byte); - } - } - - unsigned char firstHash[SHA256_DIGEST_LENGTH]; - SHA256(ccBytes.data(), ccBytes.size(), firstHash); - - std::vector<uint8_t> concatenated; - concatenated.insert(concatenated.end(), firstHash, firstHash + SHA256_DIGEST_LENGTH); - concatenated.insert(concatenated.end(), transactionId.begin(), transactionId.end()); - - unsigned char finalHash[SHA256_DIGEST_LENGTH]; - SHA256(concatenated.data(), concatenated.size(), finalHash); - - return std::vector<uint8_t>(finalHash, finalHash + SHA256_DIGEST_LENGTH); -} - -class RSPClient { - public: - RSPClient(const std::string& serverUrl, const unsigned int serverPort, const std::vector<std::string>& certPath, - const std::vector<std::string>& name_filters = {}) - : m_serverUrl(serverUrl) { - // OpenSSL 3.0+ initializes automatically - - for (auto cpath : certPath) { - bool isDirectory = std::filesystem::is_directory(cpath); - try { - if (isDirectory) { - LOG_DEBUG("Loading certificates from directory: " + cpath); - - auto certs = CertificateUtil::loadCertificatesFromDirectory(cpath, name_filters); - LOG_DEBUG("Loaded " + std::to_string(certs.size()) + " certificates"); - - // Separate root CAs and intermediates - for (auto& cert : certs) { - // Identify root certificates (self-signed) - if (X509_check_issued(cert.get(), cert.get()) == X509_V_OK) { - LOG_DEBUG("Found root CA: " + CertificateUtil::getSubjectName(cert.get())); - - // Store the first root CA as our primary root - if (!m_rootCA) { - m_rootCA = std::move(cert); - } else { - // Store additional roots for chain verification - m_certPool.push_back(std::move(cert)); - } - } else { - // Store intermediate certificates - m_certPool.push_back(std::move(cert)); - } - } - - if (!m_rootCA) { - LOG_WARNING("No root CA found in directory"); - } - } else { - // load certificate chain from single file - LOG_INFO("Loading certificates from file: " + cpath); - auto caChain = CertificateUtil::loadCertificateChain(cpath); - - if (!caChain.empty()) { - m_rootCA = std::move(caChain[0]); - - for (size_t i = 1; i < caChain.size(); ++i) { - m_intermediateCA.push_back(std::move(caChain[i])); - } - - LOG_INFO("Loaded root CA: " + CertificateUtil::getSubjectName(m_rootCA.get())); - - for (const auto& cert : m_intermediateCA) { - LOG_INFO("Loaded intermediate CA: " + CertificateUtil::getSubjectName(cert.get())); - - // add intermediates to certificate pool for compatibility - m_certPool.push_back(std::unique_ptr<X509, X509Deleter>(X509_dup(cert.get()))); - } - } - } - } catch (const std::exception& e) { - LOG_ERROR("Failed to load certificates: " + std::string(e.what())); - throw; - } - } - } - - ~RSPClient() { - // OpenSSL 3.0+ handles cleanup automatically - } - - void setCACertPath(const std::string& path) { - m_caCertPath = path; - } - - void loadEUICCCertificate(const std::string& euiccCertPath) { - loadCertificate(euiccCertPath, "eUICC", m_euiccCert); - - try { - m_EID = CertificateUtil::getEID(m_euiccCert.get()); - LOG_DEBUG("Using EID from certificate: " + m_EID); - - auto ski = CertificateUtil::getSubjectKeyIdentifier(m_euiccCert.get()); - m_euiccSKI = ski; - LOG_DEBUG("Using eUICC SKI as PKID: " + HexUtil::bytesToHex(m_euiccSKI)); - } catch (const std::exception& e) { - LOG_ERROR("Failed to extract EUICC-specific data: " + std::string(e.what())); - throw; - } - } - - void loadEUICCKeyPair(const std::string& euiccPrivateKeyPath, const std::string& euiccPublicKeyPath = "") { - loadKeyPair(euiccPrivateKeyPath, euiccPublicKeyPath, "eUICC", m_euiccPrivateKey, m_euiccPublicKeyData); - } - - void loadEUMCertificate(const std::string& eumCertPath) { - loadCertificate(eumCertPath, "EUM", m_eumCert); - } - - void loadEUMKeyPair(const std::string& eumPrivateKeyPath, const std::string& eumPublicKeyPath = "") { - loadKeyPair(eumPrivateKeyPath, eumPublicKeyPath, "EUM", m_eumPrivateKey, m_eumPublicKeyData); - } - - std::vector<uint8_t> generateChallenge() { - std::vector<uint8_t> challenge(16); - if (RAND_bytes(challenge.data(), challenge.size()) != 1) { - throw OpenSSLError("Failed to generate random challenge"); - } - return challenge; - } - - int getCertificateCurveNID(X509* cert) { - if (!cert) { - throw std::runtime_error("Certificate is null"); - } - - std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> pubkey(X509_get_pubkey(cert)); - if (!pubkey) { - throw OpenSSLError("Failed to extract public key from certificate"); - } - - char curve_name[80]; - size_t curve_name_len = sizeof(curve_name); - if (EVP_PKEY_get_utf8_string_param(pubkey.get(), OSSL_PKEY_PARAM_GROUP_NAME, curve_name, sizeof(curve_name), &curve_name_len) != 1) { - throw OpenSSLError("Failed to get curve name from certificate"); - } - - std::string curve_str(curve_name); - int curve_nid; - - if (curve_str == "prime256v1" || curve_str == "P-256") { - curve_nid = NID_X9_62_prime256v1; - } else if (curve_str == "brainpoolP256r1") { - curve_nid = NID_brainpoolP256r1; - } else { - throw std::runtime_error("Unsupported curve in certificate: " + curve_str); - } - - return curve_nid; - } - - int getEUICCCurveNID() { - if (!m_euiccCert) { - throw std::runtime_error("eUICC certificate not loaded"); - } - - int curve_nid = getCertificateCurveNID(m_euiccCert.get()); - - if (curve_nid == NID_X9_62_prime256v1) { - LOG_INFO("eUICC certificate uses P-256 curve"); - } else if (curve_nid == NID_brainpoolP256r1) { - LOG_INFO("eUICC certificate uses brainpoolP256r1 curve"); - } - - return curve_nid; - } - - void generateEUICCOtpk() { - int curve_nid = getEUICCCurveNID(); - std::string curve_name = (curve_nid == NID_X9_62_prime256v1) ? "P-256" : "brainpoolP256r1"; - - std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> pctx(EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr), EVP_PKEY_CTX_free); - if (!pctx) { - throw OpenSSLError("Failed to create EVP_PKEY_CTX"); - } - - if (EVP_PKEY_keygen_init(pctx.get()) <= 0) { - throw OpenSSLError("Failed to initialize key generation"); - } - - if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx.get(), curve_nid) <= 0) { - throw OpenSSLError("Failed to set " + curve_name + " curve"); - } - - EVP_PKEY* pkey_raw = nullptr; - if (EVP_PKEY_keygen(pctx.get(), &pkey_raw) <= 0) { - throw OpenSSLError("Failed to generate EC key pair"); - } - - m_euicc_ot_PrivateKey.reset(pkey_raw); - - size_t pub_len = 0; - - if (EVP_PKEY_get_octet_string_param(m_euicc_ot_PrivateKey.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &pub_len) != 1) { - throw OpenSSLError("Failed to get public key size"); - } - - // Both P-256 and brainpoolP256r1 have 256-bit (32 byte) coordinates - // Uncompressed format: 0x04 + 32 bytes X + 32 bytes Y = 65 bytes - if (pub_len != 65) { - throw OpenSSLError("Unexpected public key size for " + curve_name); - } - - m_euiccOtpk.resize(pub_len); - if (EVP_PKEY_get_octet_string_param(m_euicc_ot_PrivateKey.get(), OSSL_PKEY_PARAM_PUB_KEY, m_euiccOtpk.data(), pub_len, &pub_len) != 1) { - throw OpenSSLError("Failed to extract public key"); - } - - LOG_INFO("Generated eUICC OtPK (" + curve_name + "): " + HexUtil::bytesToHex(m_euiccOtpk)); - - if (m_euiccOtpk[0] != 0x04) { - throw std::runtime_error("Invalid public key format - expected uncompressed point"); - } - - LOG_DEBUG("Public key format verified - uncompressed EC point"); - } - - std::vector<uint8_t> computeECDHSharedSecret(const std::vector<uint8_t>& otherPublicKey) { - if (!m_euicc_ot_PrivateKey) { - throw std::runtime_error("eUICC ephemeral private key not available"); - } - - int curve_nid = getEUICCCurveNID(); - const char* curve_param_name = (curve_nid == NID_X9_62_prime256v1) ? "P-256" : "brainpoolP256r1"; - - std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> pctx(EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr), EVP_PKEY_CTX_free); - if (!pctx) { - throw OpenSSLError("Failed to create EVP_PKEY_CTX"); - } - - if (EVP_PKEY_fromdata_init(pctx.get()) <= 0) { - throw OpenSSLError("Failed to initialize fromdata"); - } - - OSSL_PARAM_BLD* param_bld = OSSL_PARAM_BLD_new(); - if (!param_bld) { - throw OpenSSLError("Failed to create OSSL_PARAM_BLD"); - } - - if (!OSSL_PARAM_BLD_push_utf8_string(param_bld, OSSL_PKEY_PARAM_GROUP_NAME, curve_param_name, 0)) { - OSSL_PARAM_BLD_free(param_bld); - throw OpenSSLError("Failed to set curve name"); - } - - if (!OSSL_PARAM_BLD_push_octet_string(param_bld, OSSL_PKEY_PARAM_PUB_KEY, otherPublicKey.data(), otherPublicKey.size())) { - OSSL_PARAM_BLD_free(param_bld); - throw OpenSSLError("Failed to set public key"); - } - - OSSL_PARAM* params = OSSL_PARAM_BLD_to_param(param_bld); - if (!params) { - OSSL_PARAM_BLD_free(param_bld); - throw OpenSSLError("Failed to build params"); - } - - EVP_PKEY* other_pkey_raw = nullptr; - if (EVP_PKEY_fromdata(pctx.get(), &other_pkey_raw, EVP_PKEY_PUBLIC_KEY, params) <= 0) { - OSSL_PARAM_free(params); - OSSL_PARAM_BLD_free(param_bld); - throw OpenSSLError("Failed to create public key from data"); - } - - OSSL_PARAM_free(params); - OSSL_PARAM_BLD_free(param_bld); - - std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> other_pkey(other_pkey_raw); - - std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> derive_ctx(EVP_PKEY_CTX_new(m_euicc_ot_PrivateKey.get(), nullptr), - EVP_PKEY_CTX_free); - if (!derive_ctx) { - throw OpenSSLError("Failed to create derive context"); - } - - if (EVP_PKEY_derive_init(derive_ctx.get()) <= 0) { - throw OpenSSLError("Failed to initialize key derivation"); - } - - if (EVP_PKEY_derive_set_peer(derive_ctx.get(), other_pkey.get()) <= 0) { - throw OpenSSLError("Failed to set peer key"); - } - - size_t secret_len = 0; - if (EVP_PKEY_derive(derive_ctx.get(), nullptr, &secret_len) <= 0) { - throw OpenSSLError("Failed to get shared secret length"); - } - - std::vector<uint8_t> shared_secret(secret_len); - if (EVP_PKEY_derive(derive_ctx.get(), shared_secret.data(), &secret_len) <= 0) { - throw OpenSSLError("ECDH computation failed"); - } - - shared_secret.resize(secret_len); - - LOG_INFO("Computed ECDH shared secret: " + HexUtil::bytesToHex(shared_secret)); - return shared_secret; - } - - std::vector<uint8_t> signDataWithEUICC(const std::vector<uint8_t>& dataToSign) { - if (!m_euiccPrivateKey) { - throw std::runtime_error("eUICC private key not available for signing"); - } - auto rv = signDataWithKey(dataToSign, m_euiccPrivateKey.get()); - return CertificateUtil::convertDER_to_BSI_TR03111(rv); - } - - bool verifyServerSignature(const std::vector<uint8_t>& serverSigned1, const std::vector<uint8_t>& serverSignature1, - const std::vector<uint8_t>& derDataServerCert) { - auto serverCert = CertificateUtil::loadCertFromDER(derDataServerCert); - return verifyServerSignature(serverSigned1, serverSignature1, serverCert.get()); - } - - void setConfirmationCode(const std::string& confirmationCode) { - m_confirmationCode = confirmationCode; - if (!m_transactionId.empty()) { - m_confirmationCodeHash = computeConfirmationCodeHash(m_confirmationCode, m_transactionId); - LOG_INFO("Set confirmation code, computed hash: " + HexUtil::bytesToHex(m_confirmationCodeHash)); - } else { - LOG_ERROR("Cannot compute confirmation code hash - transaction ID not set"); - } - } - - void setTransactionId(const std::vector<uint8_t>& transactionId) { - m_transactionId = transactionId; - LOG_INFO("Set transaction ID: " + HexUtil::bytesToHex(transactionId)); - // If confirmation code was already set, recalculate hash - if (!m_confirmationCode.empty()) { - m_confirmationCodeHash = computeConfirmationCodeHash(m_confirmationCode, m_transactionId); - LOG_INFO("Recalculated confirmation code hash: " + HexUtil::bytesToHex(m_confirmationCodeHash)); - } - } - - std::vector<uint8_t> getConfirmationCodeHash() const { - return m_confirmationCodeHash; - } - - void setConfirmationCodeHash(const std::vector<uint8_t>& hash) { - m_confirmationCodeHash = hash; - LOG_INFO("Set confirmation code hash directly: " + HexUtil::bytesToHex(hash)); - } - - std::vector<uint8_t> getEUMCertificate() { - return CertificateUtil::certToDER(m_eumCert.get()); - } - - std::vector<uint8_t> getEUICCCertificate() { - return CertificateUtil::certToDER(m_euiccCert.get()); - } - - std::vector<uint8_t> getCICertificate() { - // If no eUICC certificate is loaded, return the primary root CA - if (!m_euiccCert) { - if (m_rootCA) { - return CertificateUtil::certToDER(m_rootCA.get()); - } - return std::vector<uint8_t>(); - } - - int euicc_curve_nid = getEUICCCurveNID(); - - if (m_rootCA) { - try { - int root_curve_nid = getCertificateCurveNID(m_rootCA.get()); - if (root_curve_nid == euicc_curve_nid) { - LOG_DEBUG("Primary root CA matches eUICC curve type"); - return CertificateUtil::certToDER(m_rootCA.get()); - } - } catch (const std::exception& e) { - LOG_WARNING("Failed to get curve type from primary root CA: " + std::string(e.what())); - } - } - - // Search for a matching root CA in the certificate pool - for (const auto& cert : m_certPool) { - // Check if this is a self-signed certificate (root CA) - if (X509_check_issued(cert.get(), cert.get()) == X509_V_OK) { - try { - int cert_curve_nid = getCertificateCurveNID(cert.get()); - if (cert_curve_nid == euicc_curve_nid) { - LOG_DEBUG("Found matching root CA in certificate pool for curve type"); - return CertificateUtil::certToDER(cert.get()); - } - } catch (const std::exception& e) { - LOG_WARNING("Failed to get curve type from certificate: " + std::string(e.what())); - } - } - } - - // If no matching root CA found, return the primary root CA as fallback - LOG_WARNING("No root CA found matching eUICC curve type, returning primary " - "root CA"); - if (m_rootCA) { - return CertificateUtil::certToDER(m_rootCA.get()); - } - return std::vector<uint8_t>(); - } - - std::vector<uint8_t> getEUICCOtpk() { - return m_euiccOtpk; - } - - private: - void loadCertificate(const std::string& certPath, const std::string& certType, std::unique_ptr<X509, X509Deleter>& certStorage) { - CertificateUtil::xx_loadCertificate(certPath, certType, certStorage); - } - - void loadKeyPair(const std::string& privateKeyPath, const std::string& publicKeyPath, const std::string& keyType, - std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter>& privateKeyStorage, std::vector<uint8_t>& publicKeyStorage) { - CertificateUtil::xx_loadPrivateKey(privateKeyPath, keyType, privateKeyStorage); - - if (!publicKeyPath.empty() && CertificateUtil::xx_loadPublicKey(publicKeyPath, keyType, publicKeyStorage)) { - return; - } - - CertificateUtil::xx_generatePublicKeyFromPrivate(privateKeyStorage, keyType, publicKeyStorage); - } - - std::vector<uint8_t> signDataWithKey(const std::vector<uint8_t>& dataToSign, EVP_PKEY* pkey) { - if (dataToSign.empty()) { - throw std::runtime_error("Data to sign is empty"); - } - - std::unique_ptr<EVP_MD_CTX, EVP_MD_CTX_Deleter> mdctx(EVP_MD_CTX_new()); - if (!mdctx) { - throw OpenSSLError("Failed to create signature context"); - } - - if (EVP_DigestSignInit(mdctx.get(), nullptr, EVP_sha256(), nullptr, pkey) != 1) { - throw OpenSSLError("Failed to initialize signature operation"); - } - - if (EVP_DigestSignUpdate(mdctx.get(), dataToSign.data(), dataToSign.size()) != 1) { - throw OpenSSLError("Failed to update signature"); - } - - size_t sigLen; - if (EVP_DigestSignFinal(mdctx.get(), nullptr, &sigLen) != 1) { - throw OpenSSLError("Failed to determine signature length"); - } - - if (sigLen == 0) { - throw std::runtime_error("signature length is zero"); - } - - std::vector<uint8_t> signature(sigLen); - if (EVP_DigestSignFinal(mdctx.get(), signature.data(), &sigLen) != 1) { - throw OpenSSLError("Failed to create signature"); - } - - signature.resize(sigLen); - LOG_DEBUG("Created signature (" + std::to_string(sigLen) + " bytes) over " + std::to_string(dataToSign.size()) + " bytes of data"); - - return signature; - } - - bool verifyServerSignature(const std::vector<uint8_t>& signedData, const std::vector<uint8_t>& signature, X509* scertdata) { - try { - // Handle prefix in serverSignature1 based on SGP.22 spec - const unsigned char* signatureData = signature.data(); - size_t signatureLen = signature.size(); - - if (signatureLen >= 3 && signatureData[0] == 0x5f && signatureData[1] == 0x37 && signatureData[2] == 0x40) { - LOG_DEBUG("Detected 5F3740 prefix in serverSignature1, skipping for " - "verification"); - signatureData += 3; - signatureLen -= 3; - } - - std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> pubkey(X509_get_pubkey(scertdata)); - if (!pubkey) { - throw OpenSSLError("Failed to extract public key from certificate"); - } - - auto ski = CertificateUtil::getSubjectKeyIdentifier(scertdata); - LOG_DEBUG(" using cert with this SKI to verify sig: " + HexUtil::bytesToHex(ski)); - - auto verifyResult = CertificateUtil::verify_TR031111( - signedData, std::vector<unsigned char>(signatureData, signatureData + signatureLen), pubkey.get()); - - if (verifyResult) { - LOG_INFO("Signature verification successful!"); - return true; - } else { - LOG_ERROR("Signature verification failed - invalid signature"); - return false; - } - } catch (const std::exception& e) { - LOG_ERROR("Error verifying signature: " + std::string(e.what())); - return false; - } - } - - bool verifyServerSignature(const std::vector<uint8_t>& serverSigned1, const std::vector<uint8_t>& serverSignature1) { - return verifyServerSignature(serverSigned1, serverSignature1, m_serverCert.get()); - } - - public: - void configureHttpClient(bool useCustomTlsCert, const std::string& customTlsCertPath = "") { - m_useCustomTlsCert = useCustomTlsCert; - m_customTlsCertPath = customTlsCertPath; - } - - // Set authentication parameters for mutual TLS auth - void setAuthParams(bool useMutualTLS, const std::string& clientCertPath, const std::string& clientKeyPath) { - m_useMutualTLS = useMutualTLS; - m_clientCertPath = clientCertPath; - m_clientKeyPath = clientKeyPath; - LOG_DEBUG("Auth params set: mutualTLS=" + std::to_string(useMutualTLS) + ", cert=" + clientCertPath + ", key=" + clientKeyPath); - } - - // Send HTTPS POST without client authentication (for ES9+) - std::string sendHttpsPost(const std::string& endpoint, const std::string& body, int& httpStatusCode, unsigned int portOverride) { - return sendHttpsPostUnified(endpoint, body, httpStatusCode, portOverride); - } - - // Send HTTPS POST with pre-configured client authentication (for ES2+) - std::string sendHttpsPostWithAuth(const std::string& endpoint, const std::string& body, int& httpStatusCode, unsigned int portOverride) { - if (!m_useMutualTLS) { - LOG_WARNING("sendHttpsPostWithAuth called but mutual TLS not configured"); - } - return sendHttpsPostUnified(endpoint, body, httpStatusCode, portOverride, m_useMutualTLS, m_clientCertPath, m_clientKeyPath); - } - - std::string sendHttpsPostWithContentType(const std::string& endpoint, const std::string& body, const std::string& contentType, int& httpStatusCode, - unsigned int portOverride) { - LOG_DEBUG("sendHttpsPostWithContentType: endpoint=" + endpoint + ", contentType=" + contentType + ", bodySize=" + std::to_string(body.size()) + - ", port=" + std::to_string(portOverride)); - return sendHttpsPostUnified(endpoint, body, httpStatusCode, portOverride, false, "", "", contentType); - } - - std::string sendHttpsPostUnified(const std::string& endpoint, const std::string& body, int& httpStatusCode, unsigned int portOverride, - bool useMutualTLS = false, const std::string& clientCertPath = "", const std::string& clientKeyPath = "", - const std::string& contentType = "application/json") { - if (!m_httpClient) { - m_httpClient = std::make_unique<HttpClient>(); - } - - // full URL without port (port set via CURLOPT_PORT) - std::string url = "https://" + m_serverUrl + endpoint; - - // Build certificate pool for server verification - std::vector<X509*> rawCertPool; - for (const auto& cert : m_certPool) { - rawCertPool.push_back(cert.get()); - } - - // Add custom TLS server certificate if configured globally - if (m_useCustomTlsCert && !m_customTlsCertPath.empty()) { - try { - auto tlsCerts = CertificateUtil::loadCertificateChain(m_customTlsCertPath); - for (auto& cert : tlsCerts) { - rawCertPool.push_back(cert.get()); - } - LOG_DEBUG("Added custom TLS server certificate from: " + m_customTlsCertPath); - } catch (const std::exception& e) { - LOG_WARNING("Failed to load custom TLS server certificate: " + std::string(e.what())); - } - } - - HttpClient::PostConfig config; - config.useMutualTLS = useMutualTLS; - config.clientCertPath = clientCertPath; - config.clientKeyPath = clientKeyPath; - config.includeAdminProtocolHeader = useMutualTLS; // ES2+ requires this header - config.verboseOutput = false; - config.contentType = contentType; - - LOG_DEBUG("Sending " + std::string(useMutualTLS ? "ES2+ request with mutual TLS" : "HTTPS request") + " to: " + endpoint + - " (port: " + std::to_string(portOverride) + ")"); - - HttpClient::ResponseData response = m_httpClient->postJson(url, portOverride, body, nullptr, rawCertPool, config); - - httpStatusCode = response.statusCode; - - LOG_DEBUG("HTTP " + std::to_string(httpStatusCode) + " response, body length: " + std::to_string(response.body.length())); - - return response.body; - } - - std::string m_serverUrl; - - std::unique_ptr<X509, X509Deleter> m_rootCA; - std::vector<std::unique_ptr<X509, X509Deleter>> m_intermediateCA; - std::unique_ptr<X509, X509Deleter> m_serverCert; - std::vector<std::unique_ptr<X509, X509Deleter>> m_certPool; - - std::vector<uint8_t> m_euiccSKI; // eUICC SKI for PKIDs - std::string m_EID; // eUICC ID - std::unique_ptr<X509, X509Deleter> m_euiccCert; // eUICC certificate - std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> m_euiccPrivateKey; // eUICC private key for signing - std::vector<uint8_t> m_euiccPublicKeyData; // eUICC public key data - std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> m_euicc_ot_PrivateKey; // One-time private key - std::vector<uint8_t> m_euiccOtpk; // One-time public key from eUICC - - std::unique_ptr<X509, X509Deleter> m_eumCert; // EUM certificate - std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> m_eumPrivateKey; // EUM private key for signing - std::vector<uint8_t> m_eumPublicKeyData; // EUM public key data - - std::string m_confirmationCode; // Confirmation code (PIN) - std::vector<uint8_t> m_confirmationCodeHash; // Computed confirmation code hash - std::vector<uint8_t> m_transactionId; // Transaction ID for hash computation - std::string m_caCertPath = ""; // Path to system CA certificates - - std::unique_ptr<HttpClient> m_httpClient; - bool m_useCustomTlsCert = true; // Flag for custom vs public CA - std::string m_customTlsCertPath; // Path to custom TLS certificate - - // Authentication parameters for mutual TLS auth - bool m_useMutualTLS = false; - std::string m_clientCertPath = ""; - std::string m_clientKeyPath = ""; -}; - -} // namespace RspCrypto \ No newline at end of file diff --git a/smdpp/rsp_client.h b/smdpp/rsp_client.h new file mode 100644 index 0000000..0154986 --- /dev/null +++ b/smdpp/rsp_client.h @@ -0,0 +1,159 @@ +/* RSP Cryptographic Operations Header + * + * Author: Eric Wild <ewild(a)sysmocom.de> / sysmocom - s.f.m.c. GmbH + * + * Released under the terms of GNU General Public License, Version 2 or + * (at your option) any later version. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef RSP_CLIENT_H +#define RSP_CLIENT_H + +#include <memory> +#include <string> +#include <vector> +#include "helpers.h" // For deleter functors + +// Forward declarations for OpenSSL types +typedef struct x509_st X509; +typedef struct evp_pkey_st EVP_PKEY; +typedef struct x509_store_st X509_STORE; + +namespace RspCrypto { + +// Forward declarations for internal types +class HttpClient; + +/** + * RSPClient - Remote SIM Provisioning Client + * + * This class implements the cryptographic operations and certificate management + * required for RSP (Remote SIM Provisioning) protocol operations. + */ +class RSPClient { + public: + /** + * Constructor + * @param serverUrl The server URL + * @param serverPort The server port + * @param certPath Vector of certificate paths (files or directories) + * @param name_filters Vector of name filters for certificate selection + */ + RSPClient(const std::string& serverUrl, const unsigned int serverPort, const std::vector<std::string>& certPath, + const std::vector<std::string>& name_filters = {}); + + /** + * Destructor + */ + ~RSPClient(); + + // Certificate and key management + void setCACertPath(const std::string& path); + void loadEUICCCertificate(const std::string& euiccCertPath); + void loadEUICCKeyPair(const std::string& euiccPrivateKeyPath, const std::string& euiccPublicKeyPath = ""); + void loadEUMCertificate(const std::string& eumCertPath); + void loadEUMKeyPair(const std::string& eumPrivateKeyPath, const std::string& eumPublicKeyPath = ""); + + // Certificate retrieval + std::vector<uint8_t> getEUMCertificate(); + std::vector<uint8_t> getEUICCCertificate(); + std::vector<uint8_t> getCICertificate(); + + // Cryptographic operations + std::vector<uint8_t> generateChallenge(); + void generateEUICCOtpk(); + std::vector<uint8_t> getEUICCOtpk(); + std::vector<uint8_t> signDataWithEUICC(const std::vector<uint8_t>& dataToSign); + bool verifyServerSignature(const std::vector<uint8_t>& serverSigned1, const std::vector<uint8_t>& signature); + bool verifyServerSignature(const std::vector<uint8_t>& serverSigned1, const std::vector<uint8_t>& serverSignature1, + const std::vector<uint8_t>& derDataServerCert); + std::vector<uint8_t> computeECDHSharedSecret(const std::vector<uint8_t>& otherPublicKey); + + // Confirmation code and transaction handling + void setConfirmationCode(const std::string& confirmationCode); + void setTransactionId(const std::vector<uint8_t>& transactionId); + std::vector<uint8_t> getConfirmationCodeHash() const; + void setConfirmationCodeHash(const std::vector<uint8_t>& hash); + + // HTTP client configuration + void configureHttpClient(bool useCustomTlsCert, const std::string& customTlsCertPath = ""); + void setAuthParams(bool useMutualTLS, const std::string& clientCertPath, const std::string& clientKeyPath); + + // HTTP operations + std::string sendHttpsPost(const std::string& endpoint, const std::string& body, int& httpStatusCode, unsigned int portOverride); + std::string sendHttpsPostWithAuth(const std::string& endpoint, const std::string& body, int& httpStatusCode, unsigned int portOverride); + std::string sendHttpsPostWithContentType(const std::string& endpoint, const std::string& body, const std::string& contentType, int& httpStatusCode, + unsigned int portOverride); + std::string sendHttpsPostUnified(const std::string& endpoint, const std::string& body, int& httpStatusCode, unsigned int portOverride, + bool useMutualTLS = false, const std::string& clientCertPath = "", const std::string& clientKeyPath = "", + const std::string& contentType = "application/json"); + + private: + // Private helper methods + void loadCertificate(const std::string& certPath, const std::string& certType, std::unique_ptr<X509, X509Deleter>& certStorage); + void loadKeyPair(const std::string& privateKeyPath, const std::string& publicKeyPath, const std::string& keyType, + std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter>& privateKeyStorage, std::vector<uint8_t>& publicKeyStorage); + std::vector<uint8_t> signDataWithKey(const std::vector<uint8_t>& dataToSign, EVP_PKEY* pkey); + bool verifyServerSignature(const std::vector<uint8_t>& signedData, const std::vector<uint8_t>& signature, X509* serverCert); + bool verifyServerSignature(const std::vector<uint8_t>& serverSigned1, const std::vector<uint8_t>& signature, X509* serverCert, + const std::string& certSource); + int getEUICCCurveNID(); + + // Member variables + std::string m_serverUrl; + + std::unique_ptr<X509, X509Deleter> m_rootCA; + std::vector<std::unique_ptr<X509, X509Deleter>> m_intermediateCA; + std::unique_ptr<X509, X509Deleter> m_serverCert; + std::vector<std::unique_ptr<X509, X509Deleter>> m_certPool; + + std::vector<uint8_t> m_euiccSKI; + std::string m_EID; + std::unique_ptr<X509, X509Deleter> m_euiccCert; + std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> m_euiccPrivateKey; + std::vector<uint8_t> m_euiccPublicKeyData; + std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> m_euicc_ot_PrivateKey; + std::vector<uint8_t> m_euiccOtpk; + + std::unique_ptr<X509, X509Deleter> m_eumCert; + std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter> m_eumPrivateKey; + std::vector<uint8_t> m_eumPublicKeyData; + + std::string m_confirmationCode; + std::vector<uint8_t> m_confirmationCodeHash; + std::vector<uint8_t> m_transactionId; + std::string m_caCertPath; + + std::unique_ptr<HttpClient> m_httpClient; + bool m_useCustomTlsCert; + std::string m_customTlsCertPath; + + bool m_useMutualTLS; + std::string m_clientCertPath; + std::string m_clientKeyPath; +}; + +// Utility classes used by RSPClient (exposed for use in external functions) +class CertificateUtil { + public: + static std::unique_ptr<X509, X509Deleter> loadCertFromDER(const std::vector<uint8_t>& derData); + static std::vector<std::unique_ptr<X509, X509Deleter>> loadCertificateChain(const std::string& filename); + static std::vector<std::unique_ptr<X509, X509Deleter>> loadCertificatesFromDirectory(const std::string& directory, + const std::vector<std::string>& name_filters); + static std::string getEID(X509* cert); + static std::string getSubjectName(X509* cert); + static std::vector<uint8_t> getSubjectKeyIdentifier(X509* cert); + static std::vector<uint8_t> getAuthorityKeyIdentifier(X509* cert); + static bool validateEIDRange(const std::string& eid, const std::vector<uint8_t>& eumCertData); + static bool hasRSPRole(const std::vector<uint8_t>& certData, const std::string& roleOid); + static std::string getPermittedEINs(const std::vector<uint8_t>& eumCertData); + static bool verifyECDHCompatible(const std::vector<uint8_t>& pubKey1, const std::vector<uint8_t>& pubKey2); + static std::string getCurveOID(const std::vector<uint8_t>& certData); + static bool verifyCertificateChainDynamic(X509* cert, const std::vector<X509*>& certPool, X509* rootCA, bool verbose); +}; + +} // namespace RspCrypto + +#endif // RSP_CLIENT_H \ No newline at end of file diff --git a/smdpp/smdpp_Tests_Functions.cc b/smdpp/smdpp_Tests_Functions.cc index fb9b504..0fac22e 100644 --- a/smdpp/smdpp_Tests_Functions.cc +++ b/smdpp/smdpp_Tests_Functions.cc @@ -13,8 +13,9 @@ #include <TTCN3.hh> #include "smdpp_Tests.hh" -#include "rsp_client.cpp" -#include "bsp_crypto.cpp" +#include "rsp_client.h" +#include "bsp_crypto.h" +#include "logger.h" using namespace RspCrypto; -- To view, visit https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41122?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: merged Gerrit-Project: osmo-ttcn3-hacks Gerrit-Branch: master Gerrit-Change-Id: I9dcb3249fc712e2ddb2c7cf48d4fc54425848bc6 Gerrit-Change-Number: 41122 Gerrit-PatchSet: 7 Gerrit-Owner: Hoernchen <ewild(a)sysmocom.de> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: dexter <pmaier(a)sysmocom.de> Gerrit-Reviewer: laforge <laforge(a)osmocom.org> Gerrit-Reviewer: osmith <osmith(a)sysmocom.de>
1 0
0 0
[XL] Change in osmo-ttcn3-hacks[master]: smdpp: es9p testsuite
by osmith 08 Oct '25

08 Oct '25
osmith has submitted this change. ( https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41119?usp=email ) Change subject: smdpp: es9p testsuite ...................................................................... smdpp: es9p testsuite Implemented parts conform to sgp 23 v1.13-v1.15 Certs are from https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uplo… the invalid ones are generated using smdpp/generate_all_test_certificates.py Change-Id: I3ba163d9155a3b019214dbccc3c8031dfd7deb6b --- M .checkpatch.conf M Makefile A smdpp/.clang-format A smdpp/InvalidTestCases/CERT_EUICC_ECDSA_NIST_EXPIRED.der A smdpp/InvalidTestCases/CERT_EUICC_ECDSA_NIST_INVALID.der A smdpp/InvalidTestCases/CERT_EUICC_ECDSA_NIST_INVALID_EID.der A smdpp/InvalidTestCases/CERT_EUICC_ECDSA_NIST_INVALID_EID.pem A smdpp/InvalidTestCases/CERT_EUICC_ECDSA_NIST_UNMATCHED_EID.der A smdpp/InvalidTestCases/CERT_EUICC_ECDSA_NIST_UNMATCHED_EID.pem A smdpp/InvalidTestCases/CERT_EUM_ECDSA_NIST_EXPIRED.der A smdpp/InvalidTestCases/CERT_EUM_ECDSA_NIST_INVALID.der A smdpp/InvalidTestCases/CERT_EUM_ECDSA_NIST_UNKNOWN_CI.der A smdpp/InvalidTestCases/CERT_UNKNOWN_CI_ECDSA_NIST.der A smdpp/bsp_crypto.cpp A smdpp/bsp_crypto.h A smdpp/gen_links.sh A smdpp/generate_all_test_certificates.py A smdpp/helpers.h A smdpp/logger.h A smdpp/package.yml A smdpp/regen_makefile.sh A smdpp/rsp_client.cpp A smdpp/sgp26/CertificateIssuer/CERT_CI_ECDSA_BRP.der A smdpp/sgp26/CertificateIssuer/CERT_CI_ECDSA_BRP.pem A smdpp/sgp26/CertificateIssuer/CERT_CI_ECDSA_NIST.der A smdpp/sgp26/CertificateIssuer/CERT_CI_ECDSA_NIST.pem A smdpp/sgp26/CertificateIssuer/CI-csr.cnf A smdpp/sgp26/CertificateIssuer/SK_CI_ECDSA_BRP.pem A smdpp/sgp26/CertificateIssuer/SK_CI_ECDSA_NIST.pem A smdpp/sgp26/DPauth/CERT_S_SM_DP2auth_ECDSA_BRP.der A smdpp/sgp26/DPauth/CERT_S_SM_DP2auth_ECDSA_NIST.der A smdpp/sgp26/DPauth/CERT_S_SM_DPauth_ECDSA_BRP.der A smdpp/sgp26/DPauth/CERT_S_SM_DPauth_ECDSA_NIST.der A smdpp/sgp26/DPauth/PK_S_SM_DP2auth_ECDSA_BRP.pem A smdpp/sgp26/DPauth/PK_S_SM_DP2auth_ECDSA_NIST.pem A smdpp/sgp26/DPauth/PK_S_SM_DPauth_ECDSA_BRP.pem A smdpp/sgp26/DPauth/PK_S_SM_DPauth_ECDSA_NIST.pem A smdpp/sgp26/DPauth/SK_S_SM_DP2auth_ECDSA_BRP.pem A smdpp/sgp26/DPauth/SK_S_SM_DP2auth_ECDSA_NIST.pem A smdpp/sgp26/DPauth/SK_S_SM_DPauth_ECDSA_BRP.pem A smdpp/sgp26/DPauth/SK_S_SM_DPauth_ECDSA_NIST.pem A smdpp/sgp26/DPpb/CERT_S_SM_DP2pb_ECDSA_BRP.der A smdpp/sgp26/DPpb/CERT_S_SM_DP2pb_ECDSA_NIST.der A smdpp/sgp26/DPpb/CERT_S_SM_DPpb_ECDSA_BRP.der A smdpp/sgp26/DPpb/CERT_S_SM_DPpb_ECDSA_NIST.der A smdpp/sgp26/DPpb/PK_S_SM_DP2pb_ECDSA_BRP.pem A smdpp/sgp26/DPpb/PK_S_SM_DP2pb_ECDSA_NIST.pem A smdpp/sgp26/DPpb/PK_S_SM_DPpb_ECDSA_BRP.pem A smdpp/sgp26/DPpb/PK_S_SM_DPpb_ECDSA_NIST.pem A smdpp/sgp26/DPpb/SK_S_SM_DP2pb_ECDSA_BRP.pem A smdpp/sgp26/DPpb/SK_S_SM_DP2pb_ECDSA_NIST.pem A smdpp/sgp26/DPpb/SK_S_SM_DPpb_ECDSA_BRP.pem A smdpp/sgp26/DPpb/SK_S_SM_DPpb_ECDSA_NIST.pem A smdpp/sgp26/DPtls/CERT_S_SM_DP2_TLS.csr.cnf A smdpp/sgp26/DPtls/CERT_S_SM_DP2_TLS.der A smdpp/sgp26/DPtls/CERT_S_SM_DP2_TLS.ext.cnf A smdpp/sgp26/DPtls/CERT_S_SM_DP4_TLS.csr.cnf A smdpp/sgp26/DPtls/CERT_S_SM_DP4_TLS.der A smdpp/sgp26/DPtls/CERT_S_SM_DP4_TLS.ext.cnf A smdpp/sgp26/DPtls/CERT_S_SM_DP8_TLS.csr.cnf A smdpp/sgp26/DPtls/CERT_S_SM_DP8_TLS.der A smdpp/sgp26/DPtls/CERT_S_SM_DP8_TLS.ext.cnf A smdpp/sgp26/DPtls/CERT_S_SM_DP_TLS.csr.cnf A smdpp/sgp26/DPtls/CERT_S_SM_DP_TLS.ext.cnf A smdpp/sgp26/DPtls/CERT_S_SM_DP_TLS_BRP.der A smdpp/sgp26/DPtls/CERT_S_SM_DP_TLS_NIST.der A smdpp/sgp26/DPtls/PK_S_SM_DP2_TLS_NIST.pem A smdpp/sgp26/DPtls/PK_S_SM_DP4_TLS.pem A smdpp/sgp26/DPtls/PK_S_SM_DP8_TLS.pem A smdpp/sgp26/DPtls/PK_S_SM_DP_TLS_BRP.pem A smdpp/sgp26/DPtls/PK_S_SM_DP_TLS_NIST.pem A smdpp/sgp26/DPtls/SK_S_SM_DP2_TLS_NIST.pem A smdpp/sgp26/DPtls/SK_S_SM_DP4_TLS.pem A smdpp/sgp26/DPtls/SK_S_SM_DP8_TLS.pem A smdpp/sgp26/DPtls/SK_S_SM_DP_TLS_BRP.pem A smdpp/sgp26/DPtls/SK_S_SM_DP_TLS_NIST.pem A smdpp/sgp26/EUM/CERT_EUM_ECDSA_BRP.der A smdpp/sgp26/EUM/CERT_EUM_ECDSA_NIST.der A smdpp/sgp26/EUM/EUM-csr.cnf A smdpp/sgp26/EUM/EUM-ext.cnf A smdpp/sgp26/EUM/PK_EUM_ECDSA_BRP.pem A smdpp/sgp26/EUM/PK_EUM_ECDSA_NIST.pem A smdpp/sgp26/EUM/SK_EUM_ECDSA_BRP.pem A smdpp/sgp26/EUM/SK_EUM_ECDSA_NIST.pem A smdpp/sgp26/eUICC/CERT_EUICC_ECDSA_BRP.der A smdpp/sgp26/eUICC/CERT_EUICC_ECDSA_NIST.der A smdpp/sgp26/eUICC/PK_EUICC_ECDSA_BRP.pem A smdpp/sgp26/eUICC/PK_EUICC_ECDSA_NIST.pem A smdpp/sgp26/eUICC/SK_EUICC_ECDSA_BRP.pem A smdpp/sgp26/eUICC/SK_EUICC_ECDSA_NIST.pem A smdpp/sgp26/eUICC/eUICC-csr.cnf A smdpp/sgp26/eUICC/eUICC-ext.cnf A smdpp/smdpp_Tests.cfg A smdpp/smdpp_Tests.default A smdpp/smdpp_Tests.ttcn A smdpp/smdpp_Tests_Functions.cc 96 files changed, 10,896 insertions(+), 1 deletion(-) Approvals: dexter: Looks good to me, approved; Verified Jenkins Builder: Verified -- To view, visit https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41119?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: merged Gerrit-Project: osmo-ttcn3-hacks Gerrit-Branch: master Gerrit-Change-Id: I3ba163d9155a3b019214dbccc3c8031dfd7deb6b Gerrit-Change-Number: 41119 Gerrit-PatchSet: 7 Gerrit-Owner: Hoernchen <ewild(a)sysmocom.de> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: dexter <pmaier(a)sysmocom.de> Gerrit-Reviewer: laforge <laforge(a)osmocom.org> Gerrit-Reviewer: osmith <osmith(a)sysmocom.de>
1 0
0 0
[L] Change in osmo-ttcn3-hacks[master]: smdpp: es9p pure asn1 support
by osmith 08 Oct '25

08 Oct '25
osmith has submitted this change. ( https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41121?usp=email ) Change subject: smdpp: es9p pure asn1 support ...................................................................... smdpp: es9p pure asn1 support Can be used instead of the json layer. Change-Id: I1d824931bd6513d2320ba30df0f8193cd8352863 --- M smdpp/rsp_client.cpp M smdpp/smdpp_Tests.ttcn M smdpp/smdpp_Tests_Functions.cc 3 files changed, 377 insertions(+), 15 deletions(-) Approvals: dexter: Looks good to me, approved laforge: Looks good to me, but someone else must approve Jenkins Builder: Verified diff --git a/smdpp/rsp_client.cpp b/smdpp/rsp_client.cpp index d03a64e..97f5a5a 100644 --- a/smdpp/rsp_client.cpp +++ b/smdpp/rsp_client.cpp @@ -928,6 +928,7 @@ std::string clientKeyPath; bool includeAdminProtocolHeader = false; bool verboseOutput = false; + std::string contentType = "application/json"; }; ResponseData postJson(const std::string& url, unsigned int port, const std::string& jsonData, X509_STORE* store, std::vector<X509*>& certPool, @@ -1021,15 +1022,23 @@ } struct curl_slist* headers = nullptr; + std::string contentTypeHeader = "Content-Type: " + config.contentType; if (config.useMutualTLS) { - headers = curl_slist_append(headers, "Content-Type: application/json;charset=UTF-8"); + if (config.contentType == "application/json") { + contentTypeHeader += ";charset=UTF-8"; + } + headers = curl_slist_append(headers, contentTypeHeader.c_str()); headers = curl_slist_append(headers, "Accept: application/json"); if (config.includeAdminProtocolHeader) { headers = curl_slist_append(headers, "X-Admin-Protocol: gsma/rsp/v2.5.0"); } } else { - headers = curl_slist_append(headers, "Content-Type: application/json"); - headers = curl_slist_append(headers, "Accept: application/json"); + headers = curl_slist_append(headers, contentTypeHeader.c_str()); + // For ASN.1, accept the same content type + std::string acceptHeader = "Accept: " + (config.contentType == "application/x-gsma-rsp-asn1" ? config.contentType : "application/json"); + headers = curl_slist_append(headers, acceptHeader.c_str()); + // Always add X-Admin-Protocol for ES9+ regardless of content type + headers = curl_slist_append(headers, "X-Admin-Protocol: gsma/rsp/v2.5.0"); } SslCtxData ctxData = { .store = store, .certPool = &certPool, .verifyResult = false, .errorMessage = "" }; @@ -1040,6 +1049,7 @@ curl_easy_setopt(curl, CURLOPT_PORT, port); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonData.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, jsonData.size()); // Important for binary data curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body); curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback); @@ -1049,6 +1059,10 @@ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + // Set reasonable timeouts + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); + if (config.useMutualTLS) { if (!config.clientCertPath.empty()) { curl_easy_setopt(curl, CURLOPT_SSLCERT, config.clientCertPath.c_str()); @@ -1630,8 +1644,16 @@ return sendHttpsPostUnified(endpoint, body, httpStatusCode, portOverride, m_useMutualTLS, m_clientCertPath, m_clientKeyPath); } + std::string sendHttpsPostWithContentType(const std::string& endpoint, const std::string& body, const std::string& contentType, int& httpStatusCode, + unsigned int portOverride) { + LOG_DEBUG("sendHttpsPostWithContentType: endpoint=" + endpoint + ", contentType=" + contentType + ", bodySize=" + std::to_string(body.size()) + + ", port=" + std::to_string(portOverride)); + return sendHttpsPostUnified(endpoint, body, httpStatusCode, portOverride, false, "", "", contentType); + } + std::string sendHttpsPostUnified(const std::string& endpoint, const std::string& body, int& httpStatusCode, unsigned int portOverride, - bool useMutualTLS = false, const std::string& clientCertPath = "", const std::string& clientKeyPath = "") { + bool useMutualTLS = false, const std::string& clientCertPath = "", const std::string& clientKeyPath = "", + const std::string& contentType = "application/json") { if (!m_httpClient) { m_httpClient = std::make_unique<HttpClient>(); } @@ -1664,6 +1686,7 @@ config.clientKeyPath = clientKeyPath; config.includeAdminProtocolHeader = useMutualTLS; // ES2+ requires this header config.verboseOutput = false; + config.contentType = contentType; LOG_DEBUG("Sending " + std::string(useMutualTLS ? "ES2+ request with mutual TLS" : "HTTPS request") + " to: " + endpoint + " (port: " + std::to_string(portOverride) + ")"); diff --git a/smdpp/smdpp_Tests.ttcn b/smdpp/smdpp_Tests.ttcn index b2842a1..79af08c 100644 --- a/smdpp/smdpp_Tests.ttcn +++ b/smdpp/smdpp_Tests.ttcn @@ -234,6 +234,24 @@ out integer statusCode ) return charstring; +external function ext_RSPClient_sendHttpsPostWithContentType( + integer clientHandle, + charstring endpoint, + charstring body, + integer dport, + charstring contentType, + out integer statusCode +) return charstring; + +external function ext_RSPClient_sendHttpsPostBinary( + integer clientHandle, + charstring endpoint, + octetstring body, + integer dport, + charstring contentType, + out integer statusCode +) return octetstring; + /* RSP Protocol Constants */ const charstring c_oid_rspRole_dp_auth := "2.23.146.1.2.1.4"; const charstring c_oid_rspRole_dp_pb := "2.23.146.1.2.1.5"; @@ -337,6 +355,11 @@ var charstring g_last_es9p_request := ""; }; +type enumerated ES9EncodingMode { + ES9_JSON, + ES9_ASN1 +} + type record smdpp_ConnHdlrPars { charstring smdp_server_url, integer smdp_es9p_server_port, @@ -353,7 +376,8 @@ GetBppErrorInjection gbpp_err_injection optional, boolean cc_required optional, boolean use_ppk optional, - integer metadata_segments optional + integer metadata_segments optional, + ES9EncodingMode es9_encoding_mode optional }; private function f_init_pars() runs on MTC_CT return smdpp_ConnHdlrPars { @@ -373,7 +397,8 @@ gbpp_err_injection := omit, cc_required := false, use_ppk := false, - metadata_segments := 1 + metadata_segments := 1, + es9_encoding_mode := omit /* Default to JSON mode */ }; return pars; } @@ -921,16 +946,62 @@ private function f_es9p_transceive_wrap(RemoteProfileProvisioningRequest request) runs on smdpp_ConnHdlr return DecodedRPPReponse_Wrap { - f_es9p_send_new(request); + + var ES9EncodingMode encoding_mode := ES9_JSON; + if (ispresent(g_pars_smdpp.es9_encoding_mode)) { + encoding_mode := g_pars_smdpp.es9_encoding_mode; + } + + if (encoding_mode == ES9_ASN1) { + return f_es9p_transceive_wrap_asn1(request); + } else { + f_es9p_send_new(request); + + var integer http_status; + var charstring response_body := ext_RSPClient_sendHttpsPost( + g_rsp_client_handle, + g_last_es9p_endpoint, + g_last_es9p_request, + g_pars_smdpp.smdp_es9p_server_port, + http_status + ); + + if (http_status != 200) { + setverdict(fail, "HTTP error response: " & int2str(http_status)); + var DecodedRPPReponse_Wrap empty_response; + return empty_response; + } + + // Decode to wrapper type that handles both success and error + var DecodedRPPReponse_Wrap response := {omit, omit}; + dec_RemoteProfileProvisioningResponse_from_JSON(response_body, response); + + return response; + } +} + +/* Pure ASN.1 mode transceive function */ +private function f_es9p_transceive_wrap_asn1(RemoteProfileProvisioningRequest request) +runs on smdpp_ConnHdlr +return DecodedRPPReponse_Wrap { + /* Encode the request as pure ASN.1 (already includes A2 tag) */ + var octetstring asn1_request := enc_RemoteProfileProvisioningRequest(request); + + ext_logInfo("ASN.1 request size: " & int2str(lengthof(asn1_request)) & " bytes"); + ext_logInfo("ASN.1 request first 50 bytes: " & oct2str(substr(asn1_request, 0, 50))); + ext_logInfo("ASN.1 request full: " & oct2str(asn1_request)); var integer http_status; - var charstring response_body := ext_RSPClient_sendHttpsPost( + ext_logInfo("Sending ASN.1 request to /gsma/rsp2/asn1 on port " & int2str(g_pars_smdpp.smdp_es9p_server_port)); + var octetstring response_body := ext_RSPClient_sendHttpsPostBinary( g_rsp_client_handle, - g_last_es9p_endpoint, - g_last_es9p_request, - g_pars_smdpp.smdp_es9p_server_port, + "/gsma/rsp2/asn1", + asn1_request, + g_pars_smdpp.smdp_es9p_server_port, + "application/x-gsma-rsp-asn1", http_status ); + ext_logInfo("Received HTTP status: " & int2str(http_status)); if (http_status != 200) { setverdict(fail, "HTTP error response: " & int2str(http_status)); @@ -938,11 +1009,129 @@ return empty_response; } - // Decode to wrapper type that handles both success and error - var DecodedRPPReponse_Wrap response := {omit, omit}; - dec_RemoteProfileProvisioningResponse_from_JSON(response_body, response); + /* Decode pure ASN.1 response (already includes A3 tag) */ + var RemoteProfileProvisioningResponse asn1_response := dec_RemoteProfileProvisioningResponse(response_body); - return response; + /* Convert to DecodedRPPReponse_Wrap format for compatibility */ + var DecodedRPPReponse_Wrap wrap := {omit, omit}; + + /* ASN.1 errors are encoded as specific CHOICE values */ + if (f_is_asn1_error_response(asn1_response)) { + ext_logInfo("ASN.1 response detected as error, converting..."); + wrap.err := f_convert_asn1_error_to_json(asn1_response); + ext_logInfo("Converted error: subject=" & wrap.err.subjectCode & ", reason=" & wrap.err.reasonCode); + } else { + /* Success response */ + wrap.asn1_pdu := asn1_response; + } + + return wrap; +} + +/* Helper to check if ASN.1 response is an error */ +private function f_is_asn1_error_response(RemoteProfileProvisioningResponse resp) return boolean { + if (ischosen(resp.authenticateClientResponseEs9)) { + ext_logInfo("Response has authenticateClientResponseEs9"); + /* AuthenticateClientResponseEs9 is a CHOICE between authenticateClientOk and authenticateClientError */ + if (ischosen(resp.authenticateClientResponseEs9.authenticateClientError)) { + return true; + } + } else if (ischosen(resp.getBoundProfilePackageResponse)) { + /* GetBoundProfilePackageResponse is a CHOICE between getBoundProfilePackageOk and getBoundProfilePackageError */ + if (ischosen(resp.getBoundProfilePackageResponse.getBoundProfilePackageError)) { + return true; + } + } else if (ischosen(resp.initiateAuthenticationResponse)) { + /* InitiateAuthenticationResponse is a CHOICE between initiateAuthenticationOk and initiateAuthenticationError */ + if (ischosen(resp.initiateAuthenticationResponse.initiateAuthenticationError)) { + return true; + } + } else if (ischosen(resp.cancelSessionResponseEs9)) { + /* CancelSessionResponseEs9 is a CHOICE between cancelSessionOk and cancelSessionError */ + if (ischosen(resp.cancelSessionResponseEs9.cancelSessionError)) { + return true; + } + } + return false; +} + +/* Helper to convert ASN.1 error to JSON error format */ +private function f_convert_asn1_error_to_json(RemoteProfileProvisioningResponse resp) +return JSON_ESx_FunctionExecutionStatusCodeData { + var JSON_ESx_FunctionExecutionStatusCodeData err := { + subjectCode := "", + reasonCode := "", + subjectIdentifier := omit, + message_ := "" + }; + + if (ischosen(resp.authenticateClientResponseEs9)) { + if (ischosen(resp.authenticateClientResponseEs9.authenticateClientError)) { + var integer errorCode := resp.authenticateClientResponseEs9.authenticateClientError; + /* Map AuthenticateClient error codes per SGP.22 */ + if (errorCode == 1) { /* eumCertificateInvalid */ + err.subjectCode := "8.1.1"; + err.reasonCode := "3.1"; + err.message_ := "EUM Certificate Invalid"; + } else if (errorCode == 2) { /* eumCertificateExpired */ + err.subjectCode := "8.1.1"; + err.reasonCode := "3.2"; + err.message_ := "EUM Certificate Expired"; + } else if (errorCode == 3) { /* euiccCertificateInvalid */ + err.subjectCode := "8.1.2"; + err.reasonCode := "3.1"; + err.message_ := "eUICC Certificate Invalid"; + } else if (errorCode == 4) { /* euiccCertificateExpired */ + err.subjectCode := "8.1.2"; + err.reasonCode := "3.2"; + err.message_ := "eUICC Certificate Expired"; + } else if (errorCode == 5) { /* euiccSignatureInvalid */ + err.subjectCode := "8.1.3"; + err.reasonCode := "3.1"; + err.message_ := "eUICC Signature Invalid"; + } else if (errorCode == 8) { /* matchingIdRefused */ + err.subjectCode := "8.2.6"; + err.reasonCode := "3.1"; + err.message_ := "Matching ID Refused"; + } else if (errorCode == 127) { /* undefinedError */ + err.subjectCode := "8.8.1"; + err.reasonCode := "5"; + err.message_ := "Undefined Error"; + } + } + } else if (ischosen(resp.getBoundProfilePackageResponse)) { + if (ischosen(resp.getBoundProfilePackageResponse.getBoundProfilePackageError)) { + var integer errorCode := resp.getBoundProfilePackageResponse.getBoundProfilePackageError; + /* Map GetBoundProfilePackage error codes */ + if (errorCode == 1) { /* euiccSignatureInvalid */ + err.subjectCode := "8.1.3"; + err.reasonCode := "3.1"; + err.message_ := "eUICC Signature Invalid"; + } else if (errorCode == 2) { /* confirmationCodeMissing */ + err.subjectCode := "8.2.7"; + err.reasonCode := "3.7"; + err.message_ := "Confirmation Code Missing"; + } else if (errorCode == 3) { /* confirmationCodeRefused */ + err.subjectCode := "8.2.7"; + err.reasonCode := "3.8"; + err.message_ := "Confirmation Code Refused"; + } else if (errorCode == 127) { /* undefinedError */ + err.subjectCode := "8.8.1"; + err.reasonCode := "5"; + err.message_ := "Undefined Error"; + } + } + } + + /* Fallback for undefined or empty errors, tbd */ + if (err.subjectCode == "" or err.reasonCode == "") { + err.subjectCode := "8.8.1"; + err.reasonCode := "5"; + err.message_ := "Undefined Error (Server Internal Error)"; + ext_logInfo("Using fallback error codes for undefined error"); + } + + return err; } private function f_es9p_transceive_success(RemoteProfileProvisioningRequest request) @@ -3056,6 +3245,12 @@ ext_logInfo("=== Step 2: AuthenticateClient ==="); var RemoteProfileProvisioningRequest authClientReq := f_create_authenticate_client_request(); var RemoteProfileProvisioningResponse authClientResp := f_es9p_transceive_success(authClientReq); + + if (not ischosen(authClientResp.authenticateClientResponseEs9.authenticateClientOk)) { + setverdict(fail, "AuthenticateClient did not return success response"); + f_rsp_client_cleanup(); + return; + } var AuthenticateClientOk authClientOk := authClientResp.authenticateClientResponseEs9.authenticateClientOk; ext_logInfo("=== Step 3: CancelSession - " & reason_name & " ==="); @@ -5639,6 +5834,81 @@ setverdict(pass); } +/* ======================================================================== + * ASN.1 Mode Test Variants + * These tests run the same logic as the NIST tests but use pure ASN.1 + * encoding instead of JSON for the ES9+ interface + * ======================================================================== */ + +testcase TC_SM_DP_ES9_InitiateAuthenticationASN1_01_Nominal() runs on MTC_CT { + var smdpp_ConnHdlrPars pars := f_init_pars(); + pars.es9_encoding_mode := ES9_ASN1; + var smdpp_ConnHdlr vc_conn; + f_init(testcasename()); + vc_conn := f_start_handler(refers(f_TC_InitiateAuth_01_Nominal), pars); + vc_conn.done; + setverdict(pass); +} + +testcase TC_SM_DP_ES9_AuthenticateClientASN1_01_Nominal() runs on MTC_CT { + var smdpp_ConnHdlrPars pars := f_init_pars(); + pars.es9_encoding_mode := ES9_ASN1; + var smdpp_ConnHdlr vc_conn; + f_init(testcasename()); + vc_conn := f_start_handler(refers(f_TC_AuthenticateClient_01_Nominal), pars); + vc_conn.done; + setverdict(pass); +} + +testcase TC_SM_DP_ES9_GetBoundProfilePackageASN1_01_Nominal() runs on MTC_CT { + var smdpp_ConnHdlrPars pars := f_init_pars(); + pars.es9_encoding_mode := ES9_ASN1; + var smdpp_ConnHdlr vc_conn; + f_init(testcasename()); + vc_conn := f_start_handler(refers(f_TC_GetBoundProfilePackage_01_Nominal), pars); + vc_conn.done; + setverdict(pass); +} + +testcase TC_SM_DP_ES9_CancelSession_After_AuthenticateClientASN1_01_Nominal() runs on MTC_CT { + var smdpp_ConnHdlrPars pars := f_init_pars(); + pars.es9_encoding_mode := ES9_ASN1; + var smdpp_ConnHdlr vc_conn; + f_init(testcasename()); + vc_conn := f_start_handler(refers(f_TC_CancelSession_After_AuthenticateClient_01_End_User_Rejection), pars); + vc_conn.done; + setverdict(pass); +} + +testcase TC_SM_DP_ES9_HandleNotificationASN1_01_Nominal() runs on MTC_CT { + var smdpp_ConnHdlrPars pars := f_init_pars(); + pars.es9_encoding_mode := ES9_ASN1; + var smdpp_ConnHdlr vc_conn; + f_init(testcasename()); + vc_conn := f_start_handler(refers(f_TC_HandleNotification_01_Nominal), pars); + vc_conn.done; + setverdict(pass); +} + +/* quick comparison */ +testcase TC_ES9_Mode_Comparison() runs on MTC_CT { + var smdpp_ConnHdlrPars pars_json := f_init_pars(); + pars_json.es9_encoding_mode := ES9_JSON; + var smdpp_ConnHdlr vc_json; + f_init(testcasename() & "_JSON"); + vc_json := f_start_handler(refers(f_TC_InitiateAuth_01_Nominal), pars_json); + vc_json.done; + + var smdpp_ConnHdlrPars pars_asn1 := f_init_pars(); + pars_asn1.es9_encoding_mode := ES9_ASN1; + var smdpp_ConnHdlr vc_asn1; + f_init(testcasename() & "_ASN1"); + vc_asn1 := f_start_handler(refers(f_TC_InitiateAuth_01_Nominal), pars_asn1); + vc_asn1.done; + + setverdict(pass); +} + control { execute(TC_rsp_complete_flow()); diff --git a/smdpp/smdpp_Tests_Functions.cc b/smdpp/smdpp_Tests_Functions.cc index 7bd08e3..fb9b504 100644 --- a/smdpp/smdpp_Tests_Functions.cc +++ b/smdpp/smdpp_Tests_Functions.cc @@ -640,4 +640,73 @@ }); } +CHARSTRING ext__RSPClient__sendHttpsPostWithContentType(const INTEGER& clientHandle, const CHARSTRING& endpoint, + const CHARSTRING& body, const INTEGER& port, const CHARSTRING& contentType, INTEGER& statusCode) { + statusCode = INTEGER(0); + + return with_client(clientHandle, "ext__RSPClient__sendHttpsPostWithContentType", CHARSTRING(""), [&](RSPClient* client) { + int httpStatus = 0; + int dstport = static_cast<int>(port); + + std::string response = client->sendHttpsPostWithContentType( + charstring_to_string(endpoint), + charstring_to_string(body), + charstring_to_string(contentType), + httpStatus, + dstport + ); + + statusCode = INTEGER(httpStatus); + return string_to_charstring(response); + }); +} + +OCTETSTRING ext__RSPClient__sendHttpsPostBinary(const INTEGER& clientHandle, const CHARSTRING& endpoint, + const OCTETSTRING& body, const INTEGER& port, const CHARSTRING& contentType, INTEGER& statusCode) { + statusCode = INTEGER(0); + + std::string endpointStr = charstring_to_string(endpoint); + std::string contentTypeStr = charstring_to_string(contentType); + int dstport = static_cast<int>(port); + + LOG_INFO("ext__RSPClient__sendHttpsPostBinary: endpoint=" + endpointStr + + ", port=" + std::to_string(dstport) + + ", contentType=" + contentTypeStr + + ", bodySize=" + std::to_string(body.lengthof())); + + return with_client(clientHandle, "ext__RSPClient__sendHttpsPostBinary", OCTETSTRING(), [&](RSPClient* client) { + int httpStatus = 0; + + std::string binaryBody; + binaryBody.reserve(body.lengthof()); + for (int i = 0; i < body.lengthof(); i++) { + binaryBody.push_back(static_cast<char>(body[i].get_octet())); + } + + LOG_INFO("Calling sendHttpsPostWithContentType..."); + + std::string response = client->sendHttpsPostWithContentType( + endpointStr, + binaryBody, + contentTypeStr, + httpStatus, + dstport + ); + + LOG_INFO("sendHttpsPostWithContentType returned, status=" + std::to_string(httpStatus) + + ", responseSize=" + std::to_string(response.size())); + + if (httpStatus == 0 || response.empty()) { + LOG_ERROR("HTTP request failed with status " + std::to_string(httpStatus)); + statusCode = INTEGER(0); + return OCTETSTRING(); // Return empty octetstring on failure + } + + OCTETSTRING result(response.size(), (const unsigned char*)response.data()); + + statusCode = INTEGER(httpStatus); + return result; + }); +} + } // namespace smdpp__Tests \ No newline at end of file -- To view, visit https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41121?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: merged Gerrit-Project: osmo-ttcn3-hacks Gerrit-Branch: master Gerrit-Change-Id: I1d824931bd6513d2320ba30df0f8193cd8352863 Gerrit-Change-Number: 41121 Gerrit-PatchSet: 7 Gerrit-Owner: Hoernchen <ewild(a)sysmocom.de> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: dexter <pmaier(a)sysmocom.de> Gerrit-Reviewer: laforge <laforge(a)osmocom.org> Gerrit-Reviewer: osmith <osmith(a)sysmocom.de>
1 0
0 0
[XL] Change in osmo-ttcn3-hacks[master]: smdpp: add es2p test suite
by osmith 08 Oct '25

08 Oct '25
osmith has submitted this change. ( https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41120?usp=email ) Change subject: smdpp: add es2p test suite ...................................................................... smdpp: add es2p test suite Integrated with es9p Change-Id: I2865e016974d7d7a03e00a7795a42f573b147a4b --- A smdpp/ES2Plus_Tests.cfg A smdpp/ES2Plus_Tests.ttcn M smdpp/rsp_client.cpp M smdpp/smdpp_Tests.ttcn M smdpp/smdpp_Tests_Functions.cc A smdpp/test_certs/CERT_MNO_ECDSA_NIST.pem A smdpp/test_certs/SK_MNO_ECDSA_NIST.pem 7 files changed, 1,695 insertions(+), 128 deletions(-) Approvals: laforge: Looks good to me, but someone else must approve Jenkins Builder: Verified dexter: Looks good to me, approved diff --git a/smdpp/ES2Plus_Tests.cfg b/smdpp/ES2Plus_Tests.cfg new file mode 100644 index 0000000..f98a65d --- /dev/null +++ b/smdpp/ES2Plus_Tests.cfg @@ -0,0 +1,17 @@ +[LOGGING] +FileMask := LOG_ALL | TTCN_DEBUG | TTCN_MATCHING | DEBUG_ENCDEC; +SourceInfoFormat := Single; +LogFile := "ES2Plus_Tests.%e.%h.%r.%s" +LogSourceInfo := Stack + +[MODULE_PARAMETERS] +# ES2+ specific parameters +ES2Plus_Tests.mp_es2plus_server_host := "127.0.0.1" +ES2Plus_Tests.mp_es2plus_server_port := 8000 # NIST tests use port 8000 +ES2Plus_Tests.mp_operator_id := "test.operator.com" + +[TESTPORT_PARAMETERS] + +[EXECUTE] +# Execute all ES2+ tests +ES2Plus_Tests.control \ No newline at end of file diff --git a/smdpp/ES2Plus_Tests.ttcn b/smdpp/ES2Plus_Tests.ttcn new file mode 100644 index 0000000..c6e84cd --- /dev/null +++ b/smdpp/ES2Plus_Tests.ttcn @@ -0,0 +1,1031 @@ +module ES2Plus_Tests { + +/* ES2+ Interface Test Suite + * Based on SGP.22 v2.5 Section 5.3 and SGP.23 v1.5 Section 4.3.1-4.3.5 + */ + +import from General_Types all; +import from Osmocom_Types all; +import from Native_Functions all; +import from es2p_Types_JSON all; +import from esx_header_Types_JSON all; +import from TCCConversion_Functions all; +import from smdpp_Tests all; // For RSPClient functions and types + +// ES2+ specific constants +const charstring c_es2plus_base_path := "/gsma/rsp2/es2plus"; +const charstring c_path_download_order := c_es2plus_base_path & "/downloadOrder"; +const charstring c_path_confirm_order := c_es2plus_base_path & "/confirmOrder"; +const charstring c_path_cancel_order := c_es2plus_base_path & "/cancelOrder"; +const charstring c_path_release_profile := c_es2plus_base_path & "/releaseProfile"; + +// Test ICCIDs that exist in Python server test mode +const charstring c_iccid_test_1 := "89000000000000001010"; +const charstring c_iccid_test_2 := "89000000000000001028"; +const charstring c_iccid_test_3 := "89000000000000001036"; +const charstring c_iccid_test_4 := "89000000000000001044"; +const charstring c_iccid_test_5 := "89000000000000001051"; + +// Test EIDs from test specification +const charstring c_eid1 := "89049032123451234512345678901235"; +const charstring c_eid2 := "89049032123451234512345678901236"; + +// Client certificate paths +const charstring c_cert_path := "./test_certs/CERT_MNO_ECDSA_NIST.pem"; +const charstring c_key_path := "./test_certs/SK_MNO_ECDSA_NIST.pem"; + +// Wrong certificate for testing +const charstring c_wrong_cert_path := "./sgp26/eUICC/CERT_EUICC_ECDSA_NIST.der"; +const charstring c_wrong_key_path := "./sgp26/eUICC/SK_EUICC_ECDSA_NIST.pem"; + +// Module parameters +modulepar { + charstring mp_es2plus_server_host := "testsmdpplus1.example.com"; + integer mp_es9_server_port := 8000; + integer mp_es2plus_server_port := 8000; // NIST tests use port 8000 + charstring mp_operator_id := "test.operator.com"; + charstring mp_nist_rsp_cert_path := "./sgp26/"; + boolean mp_use_ssl := true; // SSL with mutual TLS authentication enabled +} + +// Test component - extend smdpp_ConnHdlr to reuse RSPClient functionality +type component ES2Plus_ConnHdlr extends smdpp_ConnHdlr { + // Inherits g_rsp_client_handle from smdpp_ConnHdlr +} + +// Parameter structures for test cases +type record DownloadOrderTestParams { + charstring testName, + charstring eid optional, + charstring iccid optional, + charstring profileType optional, + boolean expectError, + charstring expectedSubjectCode optional, + charstring expectedReasonCode optional +} + +type record ConfirmOrderTestParams { + charstring testName, + charstring iccid, + charstring eid optional, + charstring matchingId optional, + charstring confirmationCode optional, + charstring smdsAddress optional, + boolean releaseFlag, + boolean expectError, + charstring expectedSubjectCode optional, + charstring expectedReasonCode optional +} + +type record CancelOrderTestParams { + charstring testName, + charstring iccid, + charstring eid optional, + charstring matchingId optional, + charstring finalProfileStatusIndicator, + boolean expectError, + charstring expectedSubjectCode optional, + charstring expectedReasonCode optional +} + +// Helper function to generate unique function call identifier +function f_gen_es2plus_uuid() return charstring { + // Simple UUID based on random octets + var octetstring rnd_oct := f_rnd_octstring(4); + return "TTCN3-" & oct2str(rnd_oct); +} + +// Helper function to build ES2+ request header +function f_es2plus_request_header() return JSON_ESx_RequestHeader { + return { + functionRequesterIdentifier := mp_operator_id, + functionCallIdentifier := f_gen_es2plus_uuid() + }; +} + +// Helper function for ES2+ HTTP requests +function f_es2plus_http_request(charstring path, charstring functionName, JSON_ES2p_Request request) runs on ES2Plus_ConnHdlr return JSON_ES2p_Response { + + var integer status_code; + var charstring response; + + // Encode request + var octetstring req_enc := enc_JSON_ES2p_Request(request); + + // Build full URL + var charstring url := path; // URL without host/port - the C++ function will add it + + // Send request using pre-configured authentication + response := smdpp_Tests.ext_RSPClient_sendHttpsPostWithAuth( + g_rsp_client_handle, + url, + oct2char(req_enc), + mp_es2plus_server_port, + status_code + ); + + if (status_code != 200) { + setverdict(fail, "HTTP request failed with status ", status_code); + mtc.stop; + } + + // Decode response based on function name + return dec_JSON_ES2p_Response(char2oct(response), functionName); +} + +// Check if response indicates success +function f_es2plus_check_success(JSON_ESx_ResponseHeader header) return boolean { + if (header.functionExecutionStatus.status == "Executed-Success") { + return true; + } + return false; +} + +// Helper function to verify error response +function f_es2plus_check_error(JSON_ESx_ResponseHeader header, charstring expected_subject_code, charstring expected_reason_code) return boolean { + if (header.functionExecutionStatus.status != "Failed") { + log("Expected Failed status, got: ", header.functionExecutionStatus.status); + return false; + } + + if (not ispresent(header.functionExecutionStatus.statusCodeData)) { + log("Missing statusCodeData in error response"); + return false; + } + + var JSON_ESx_FunctionExecutionStatusCodeData statusData := header.functionExecutionStatus.statusCodeData; + + if (statusData.subjectCode != expected_subject_code) { + log("Expected subject code ", expected_subject_code, ", got: ", statusData.subjectCode); + return false; + } + + if (statusData.reasonCode != expected_reason_code) { + log("Expected reason code ", expected_reason_code, ", got: ", statusData.reasonCode); + return false; + } + + log("Got expected error: ", expected_subject_code, "/", expected_reason_code); + return true; +} + +// Initialize RSP client for ES2+ +function f_init_es2plus() runs on ES2Plus_ConnHdlr { + // Initialize RSP client using imported function from smdpp_Tests + g_rsp_client_handle := smdpp_Tests.ext_RSPClient_create( + mp_es2plus_server_host, + mp_es2plus_server_port, + mp_nist_rsp_cert_path, + "NIST" + ); + + if (g_rsp_client_handle < 0) { + setverdict(fail, "Failed to initialize RSP client"); + mtc.stop; + } + + // Set authentication parameters once for all ES2+ operations + var integer result := smdpp_Tests.ext_RSPClient_setAuthParams( + g_rsp_client_handle, + true, // useMutualTLS + c_cert_path, + c_key_path + ); + + if (result != 0) { + setverdict(fail, "Failed to set authentication parameters"); + mtc.stop; + } +} + +/*********************************************************************** + * Generic Test Functions + ***********************************************************************/ + +// Generic DownloadOrder test function +private function f_TC_DownloadOrder_Generic( + DownloadOrderTestParams params +) runs on ES2Plus_ConnHdlr { + + log("=== Test Case: ", params.testName, " ==="); + + // Initialize + f_init_es2plus(); + + // Build request + var JSON_ES2p_Request req := { + downloadOrderRequest := { + header := f_es2plus_request_header(), + eid := params.eid, + iccid := params.iccid, + profileType := params.profileType + } + }; + + // Send and receive + var JSON_ES2p_Response resp := f_es2plus_http_request(c_path_download_order, "downloadOrder", req); + + // Check result + if (params.expectError) { + if (not f_es2plus_check_error(resp.downloadOrderResponse.header, + params.expectedSubjectCode, + params.expectedReasonCode)) { + setverdict(fail, "Expected error ", params.expectedSubjectCode, "/", + params.expectedReasonCode, " not received"); + mtc.stop; + } + } else { + if (not f_es2plus_check_success(resp.downloadOrderResponse.header)) { + setverdict(fail, "DownloadOrder failed unexpectedly"); + mtc.stop; + } + + // Verify ICCID returned + if (not ispresent(resp.downloadOrderResponse.iccid)) { + setverdict(fail, "No ICCID in response"); + mtc.stop; + } + + log("Reserved ICCID: ", resp.downloadOrderResponse.iccid); + } + + setverdict(pass); +} + +// Generic ConfirmOrder test function +private function f_TC_ConfirmOrder_Generic( + ConfirmOrderTestParams params +) runs on ES2Plus_ConnHdlr { + + log("=== Test Case: ", params.testName, " ==="); + + // Initialize + f_init_es2plus(); + + // Build request + var JSON_ES2p_Request req := { + confirmOrderRequest := { + header := f_es2plus_request_header(), + iccid := params.iccid, + eid := params.eid, + matchingId := params.matchingId, + confirmationCode := params.confirmationCode, + smdsAddress := params.smdsAddress, + releaseFlag := params.releaseFlag + } + }; + + // Send and receive + var JSON_ES2p_Response resp := f_es2plus_http_request(c_path_confirm_order, "confirmOrder", req); + + // Check result + if (params.expectError) { + if (not f_es2plus_check_error(resp.confirmOrderResponse.header, + params.expectedSubjectCode, + params.expectedReasonCode)) { + setverdict(fail, "Expected error ", params.expectedSubjectCode, "/", + params.expectedReasonCode, " not received"); + mtc.stop; + } + } else { + if (not f_es2plus_check_success(resp.confirmOrderResponse.header)) { + setverdict(fail, "ConfirmOrder failed unexpectedly"); + mtc.stop; + } + + // Verify matchingId returned + if (not ispresent(resp.confirmOrderResponse.matchingId)) { + setverdict(fail, "No matchingId in response"); + mtc.stop; + } + + log("Got matchingId: ", resp.confirmOrderResponse.matchingId); + } + + setverdict(pass); +} + +// Generic CancelOrder test function +private function f_TC_CancelOrder_Generic( + CancelOrderTestParams params +) runs on ES2Plus_ConnHdlr { + + log("=== Test Case: ", params.testName, " ==="); + + // Initialize + f_init_es2plus(); + + // Build request + var JSON_ES2p_Request req := { + cancelOrderRequest := { + header := f_es2plus_request_header(), + iccid := params.iccid, + eid := params.eid, + matchingId := params.matchingId, + finalProfileStatusIndicator := params.finalProfileStatusIndicator + } + }; + + // Send and receive + var JSON_ES2p_Response resp := f_es2plus_http_request(c_path_cancel_order, "cancelOrder", req); + + // Check result + if (params.expectError) { + if (not f_es2plus_check_error(resp.cancelOrderResponse.header, + params.expectedSubjectCode, + params.expectedReasonCode)) { + setverdict(fail, "Expected error ", params.expectedSubjectCode, "/", + params.expectedReasonCode, " not received"); + mtc.stop; + } + } else { + if (not f_es2plus_check_success(resp.cancelOrderResponse.header)) { + setverdict(fail, "CancelOrder failed unexpectedly"); + mtc.stop; + } + } + + setverdict(pass); +} + +/*********************************************************************** + * DownloadOrder Test Cases + ***********************************************************************/ + +// Test: DownloadOrder with available ICCID (nominal) +testcase TC_ES2Plus_DownloadOrder_01_Nominal_ICCID() runs on ES2Plus_ConnHdlr { + f_TC_DownloadOrder_Generic({ + testName := "DownloadOrder - Nominal with ICCID", + eid := omit, + iccid := c_iccid_test_1, + profileType := omit, + expectError := false + }); +} + +// Test: DownloadOrder with profileType (nominal) +testcase TC_ES2Plus_DownloadOrder_02_Nominal_ProfileType() runs on ES2Plus_ConnHdlr { + f_TC_DownloadOrder_Generic({ + testName := "DownloadOrder - Nominal with ProfileType", + eid := c_eid1, + iccid := omit, + profileType := "Test", + expectError := false + }); +} + +// Test: DownloadOrder Error - ICCID already in use +testcase TC_ES2Plus_DownloadOrder_Error_01_ICCID_AlreadyInUse() runs on ES2Plus_ConnHdlr { + // This test requires two operations - first reserve, then try again + log("=== Test Case: DownloadOrder - Error: ICCID Already in Use ==="); + + // Initialize + f_init_es2plus(); + + // First reserve an ICCID and tie this ICCID to an EID immediately. + var JSON_ES2p_Request req := { + downloadOrderRequest := { + header := f_es2plus_request_header(), + eid := c_eid1, + iccid := c_iccid_test_3, + profileType := omit + } + }; + + var JSON_ES2p_Response resp := f_es2plus_http_request(c_path_download_order, "downloadOrder", req); + + // Verify first request succeeds + if (not f_es2plus_check_success(resp.downloadOrderResponse.header)) { + setverdict(fail, "Initial DownloadOrder failed"); + mtc.stop; + } + + // Now try to reserve the same ICCID again, but withot an EID. Since the EID is missing, the SM-DP+ will now + // recognize this request as a new request and reject with the expeced error. (Two requests with the same + // input parameters will be interpreted as a resent function call as long as the outcome is the same) + var JSON_ES2p_Request req2 := { + downloadOrderRequest := { + header := f_es2plus_request_header(), + eid := omit, + iccid := c_iccid_test_3, + profileType := omit + } + }; + resp := f_es2plus_http_request(c_path_download_order, "downloadOrder", req2); + + // Should get error 8.2.1/3.3 + if (not f_es2plus_check_error(resp.downloadOrderResponse.header, "8.2.1", "3.3")) { + setverdict(fail, "Expected error 8.2.1/3.3 for ICCID already in use"); + mtc.stop; + } + + setverdict(pass); +} + +// Test: DownloadOrder Error - Unknown ICCID +testcase TC_ES2Plus_DownloadOrder_Error_02_Unknown_ICCID() runs on ES2Plus_ConnHdlr { + f_TC_DownloadOrder_Generic({ + testName := "DownloadOrder - Error: Unknown ICCID", + eid := omit, + iccid := "8900000000000099999", + profileType := omit, + expectError := true, + expectedSubjectCode := "8.2.1", + expectedReasonCode := "3.9" + }); +} + +/*********************************************************************** + * ConfirmOrder Test Cases + ***********************************************************************/ + +// Test: ConfirmOrder nominal case +testcase TC_ES2Plus_ConfirmOrder_01_Nominal() runs on ES2Plus_ConnHdlr { + log("=== Test Case: ConfirmOrder - Nominal ==="); + + // Initialize + f_init_es2plus(); + + // First, do a DownloadOrder to get an ICCID + var JSON_ES2p_Request dl_req := { + downloadOrderRequest := { + header := f_es2plus_request_header(), + eid := omit, + iccid := omit, + profileType := "Test" + } + }; + + var JSON_ES2p_Response dl_resp := f_es2plus_http_request(c_path_download_order, "downloadOrder", dl_req); + + if (not f_es2plus_check_success(dl_resp.downloadOrderResponse.header)) { + setverdict(fail, "DownloadOrder failed"); + mtc.stop; + } + + var charstring reserved_iccid := dl_resp.downloadOrderResponse.iccid; + log("Got ICCID from DownloadOrder: ", reserved_iccid); + + // Now confirm the order + f_TC_ConfirmOrder_Generic({ + testName := "ConfirmOrder - Nominal", + iccid := reserved_iccid, + eid := omit, + matchingId := omit, + confirmationCode := omit, + smdsAddress := omit, + releaseFlag := true, + expectError := false + }); +} + +// Test: ConfirmOrder Error - Unknown Profile +testcase TC_ES2Plus_ConfirmOrder_Error_01_Unknown_Profile() runs on ES2Plus_ConnHdlr { + f_TC_ConfirmOrder_Generic({ + testName := "ConfirmOrder - Error: Unknown Profile", + iccid := "8900000000000099999", + eid := omit, + matchingId := omit, + confirmationCode := omit, + smdsAddress := omit, + releaseFlag := true, + expectError := true, + expectedSubjectCode := "8.2.1", + expectedReasonCode := "3.9" + }); +} + +// Test: ConfirmOrder Error - Profile in Available state +testcase TC_ES2Plus_ConfirmOrder_Error_02_Profile_Available() runs on ES2Plus_ConnHdlr { + log("=== Test Case: ConfirmOrder - Error: Profile in Available State ==="); + + // Initialize + f_init_es2plus(); + + // Try to confirm an ICCID that hasn't been ordered + var JSON_ES2p_Request req := { + confirmOrderRequest := { + header := f_es2plus_request_header(), + iccid := c_iccid_test_4, + eid := omit, + matchingId := omit, + confirmationCode := omit, + smdsAddress := omit, + releaseFlag := true + } + }; + + var JSON_ES2p_Response resp := f_es2plus_http_request(c_path_confirm_order, "confirmOrder", req); + + // Should get an error (exact code may vary) + if (resp.confirmOrderResponse.header.functionExecutionStatus.status != "Failed") { + setverdict(fail, "Expected error for profile in Available state"); + mtc.stop; + } + + setverdict(pass); +} + +// Test: ConfirmOrder Error - Conflicting matchingID +testcase TC_ES2Plus_ConfirmOrder_Error_03_Conflicting_MatchingID() runs on ES2Plus_ConnHdlr { + log("=== Test Case: ConfirmOrder - Error: Conflicting MatchingID ==="); + + // Initialize + f_init_es2plus(); + + // First, create a profile with a specific matchingID + var JSON_ES2p_Request dl_req := { + downloadOrderRequest := { + header := f_es2plus_request_header(), + eid := omit, + iccid := omit, + profileType := "Test" + } + }; + + var JSON_ES2p_Response dl_resp := f_es2plus_http_request(c_path_download_order, "downloadOrder", dl_req); + + if (not f_es2plus_check_success(dl_resp.downloadOrderResponse.header)) { + setverdict(fail, "Setup DownloadOrder failed"); + mtc.stop; + } + + var charstring iccid1 := dl_resp.downloadOrderResponse.iccid; + + // Confirm with a specific matchingID + var JSON_ES2p_Request conf_req := { + confirmOrderRequest := { + header := f_es2plus_request_header(), + iccid := iccid1, + eid := omit, + matchingId := "MATCHING_ID_CONFLICT_TEST", + confirmationCode := omit, + smdsAddress := omit, + releaseFlag := true + } + }; + + var JSON_ES2p_Response conf_resp := f_es2plus_http_request(c_path_confirm_order, "confirmOrder", conf_req); + + if (not f_es2plus_check_success(conf_resp.confirmOrderResponse.header)) { + setverdict(fail, "First ConfirmOrder failed"); + mtc.stop; + } + + // Now create another profile and try to use the same matchingID + dl_req.downloadOrderRequest.header := f_es2plus_request_header(); + dl_resp := f_es2plus_http_request(c_path_download_order, "downloadOrder", dl_req); + + if (not f_es2plus_check_success(dl_resp.downloadOrderResponse.header)) { + setverdict(fail, "Second DownloadOrder failed"); + mtc.stop; + } + + var charstring iccid2 := dl_resp.downloadOrderResponse.iccid; + + // Try to confirm with the same matchingID + f_TC_ConfirmOrder_Generic({ + testName := "ConfirmOrder - Error: Conflicting MatchingID", + iccid := iccid2, + eid := omit, + matchingId := "MATCHING_ID_CONFLICT_TEST", + confirmationCode := omit, + smdsAddress := omit, + releaseFlag := true, + expectError := true, + expectedSubjectCode := "8.2.6", + expectedReasonCode := "3.3" + }); +} + +// Test: ConfirmOrder Error - Incorrect smdsAddress +testcase TC_ES2Plus_ConfirmOrder_Error_04_Incorrect_SmdsAddress() runs on ES2Plus_ConnHdlr { + log("=== Test Case: ConfirmOrder - Error: Incorrect smdsAddress ==="); + + // Initialize + f_init_es2plus(); + + // First create a profile + var JSON_ES2p_Request dl_req := { + downloadOrderRequest := { + header := f_es2plus_request_header(), + eid := omit, + iccid := omit, + profileType := "Test" + } + }; + + var JSON_ES2p_Response dl_resp := f_es2plus_http_request(c_path_download_order, "downloadOrder", dl_req); + + if (not f_es2plus_check_success(dl_resp.downloadOrderResponse.header)) { + setverdict(fail, "DownloadOrder failed"); + mtc.stop; + } + + var charstring iccid := dl_resp.downloadOrderResponse.iccid; + + // Try to confirm with invalid smdsAddress + f_TC_ConfirmOrder_Generic({ + testName := "ConfirmOrder - Error: Incorrect smdsAddress", + iccid := iccid, + eid := c_eid1, + matchingId := omit, + confirmationCode := omit, + smdsAddress := "invalid.smds.address", + releaseFlag := true, + expectError := true, + expectedSubjectCode := "8.9", + expectedReasonCode := "5.1" + }); +} + +// Test: ConfirmOrder Error - Missing EID with empty matchingID +testcase TC_ES2Plus_ConfirmOrder_Error_05_Missing_EID_Empty_MatchingID() runs on ES2Plus_ConnHdlr { + log("=== Test Case: ConfirmOrder - Error: Missing EID with Empty MatchingID ==="); + + // Initialize + f_init_es2plus(); + + // First create a profile + var JSON_ES2p_Request dl_req := { + downloadOrderRequest := { + header := f_es2plus_request_header(), + eid := omit, + iccid := omit, + profileType := "Test" + } + }; + + var JSON_ES2p_Response dl_resp := f_es2plus_http_request(c_path_download_order, "downloadOrder", dl_req); + + if (not f_es2plus_check_success(dl_resp.downloadOrderResponse.header)) { + setverdict(fail, "DownloadOrder failed"); + mtc.stop; + } + + var charstring iccid := dl_resp.downloadOrderResponse.iccid; + + // Try to confirm without EID and with empty matchingID + f_TC_ConfirmOrder_Generic({ + testName := "ConfirmOrder - Error: Missing EID with Empty MatchingID", + iccid := iccid, + eid := omit, + matchingId := "", + confirmationCode := omit, + smdsAddress := omit, + releaseFlag := true, + expectError := true, + expectedSubjectCode := "8.1.1", + expectedReasonCode := "2.2" + }); +} + +// Test: ConfirmOrder Error - Different EID +testcase TC_ES2Plus_ConfirmOrder_Error_06_Different_EID() runs on ES2Plus_ConnHdlr { + log("=== Test Case: ConfirmOrder - Error: Different EID ==="); + + // Initialize + f_init_es2plus(); + + // Create a profile linked to a specific EID + var JSON_ES2p_Request dl_req := { + downloadOrderRequest := { + header := f_es2plus_request_header(), + eid := c_eid1, + iccid := omit, + profileType := "Test" + } + }; + + var JSON_ES2p_Response dl_resp := f_es2plus_http_request(c_path_download_order, "downloadOrder", dl_req); + + if (not f_es2plus_check_success(dl_resp.downloadOrderResponse.header)) { + setverdict(fail, "DownloadOrder failed"); + mtc.stop; + } + + var charstring iccid := dl_resp.downloadOrderResponse.iccid; + + // Try to confirm with a different EID + f_TC_ConfirmOrder_Generic({ + testName := "ConfirmOrder - Error: Different EID", + iccid := iccid, + eid := c_eid2, + matchingId := omit, + confirmationCode := omit, + smdsAddress := omit, + releaseFlag := true, + expectError := true, + expectedSubjectCode := "8.1.1", + expectedReasonCode := "3.10" + }); +} + +/*********************************************************************** + * CancelOrder Test Cases + ***********************************************************************/ + +// Test: CancelOrder nominal case +testcase TC_ES2Plus_CancelOrder_01_Nominal() runs on ES2Plus_ConnHdlr { + log("=== Test Case: CancelOrder - Nominal ==="); + + // Initialize + f_init_es2plus(); + + // First, create and confirm a profile + var JSON_ES2p_Request dl_req := { + downloadOrderRequest := { + header := f_es2plus_request_header(), + eid := c_eid1, + iccid := omit, + profileType := "Test" + } + }; + + var JSON_ES2p_Response dl_resp := f_es2plus_http_request(c_path_download_order, "downloadOrder", dl_req); + + if (not f_es2plus_check_success(dl_resp.downloadOrderResponse.header)) { + setverdict(fail, "DownloadOrder failed"); + mtc.stop; + } + + var charstring iccid := dl_resp.downloadOrderResponse.iccid; + log("Reserved ICCID: ", iccid); + + // Confirm the order + var JSON_ES2p_Request conf_req := { + confirmOrderRequest := { + header := f_es2plus_request_header(), + iccid := iccid, + eid := omit, + matchingId := omit, + confirmationCode := omit, + smdsAddress := omit, + releaseFlag := false // Don't release yet + } + }; + + var JSON_ES2p_Response conf_resp := f_es2plus_http_request(c_path_confirm_order, "confirmOrder", conf_req); + + if (not f_es2plus_check_success(conf_resp.confirmOrderResponse.header)) { + setverdict(fail, "ConfirmOrder failed"); + mtc.stop; + } + + var charstring matching_id := conf_resp.confirmOrderResponse.matchingId; + log("Got matchingId: ", matching_id); + + // Now cancel the order + f_TC_CancelOrder_Generic({ + testName := "CancelOrder - Nominal", + iccid := iccid, + eid := c_eid1, + matchingId := matching_id, + finalProfileStatusIndicator := "Available", + expectError := false + }); +} + +// Test: CancelOrder Error - Unknown ICCID +testcase TC_ES2Plus_CancelOrder_Error_01_Unknown_ICCID() runs on ES2Plus_ConnHdlr { + f_TC_CancelOrder_Generic({ + testName := "CancelOrder - Error: Unknown ICCID", + iccid := "8900000000000099999", + eid := omit, + matchingId := omit, + finalProfileStatusIndicator := "Available", + expectError := true, + expectedSubjectCode := "8.2.1", + expectedReasonCode := "3.9" + }); +} + +// Test: CancelOrder Error - Missing EID +testcase TC_ES2Plus_CancelOrder_Error_02_Missing_EID() runs on ES2Plus_ConnHdlr { + log("=== Test Case: CancelOrder - Error: Missing EID ==="); + + // Initialize + f_init_es2plus(); + + // First create a profile linked to an EID + var JSON_ES2p_Request dl_req := { + downloadOrderRequest := { + header := f_es2plus_request_header(), + eid := c_eid1, + iccid := omit, + profileType := "Test" + } + }; + + var JSON_ES2p_Response dl_resp := f_es2plus_http_request(c_path_download_order, "downloadOrder", dl_req); + + if (not f_es2plus_check_success(dl_resp.downloadOrderResponse.header)) { + setverdict(fail, "Setup DownloadOrder failed"); + mtc.stop; + } + + var charstring iccid := dl_resp.downloadOrderResponse.iccid; + + // Try to cancel without providing the EID + var JSON_ES2p_Request req := { + cancelOrderRequest := { + header := f_es2plus_request_header(), + iccid := iccid, + eid := omit, // Missing required EID + matchingId := omit, + finalProfileStatusIndicator := "Available" + } + }; + + var JSON_ES2p_Response resp := f_es2plus_http_request(c_path_cancel_order, "cancelOrder", req); + + // Should get an error + if (resp.cancelOrderResponse.header.functionExecutionStatus.status != "Failed") { + setverdict(fail, "Expected error for missing EID"); + mtc.stop; + } + + setverdict(pass); +} + +// Test: CancelOrder Error - Incorrect matchingID +testcase TC_ES2Plus_CancelOrder_Error_03_Incorrect_MatchingID() runs on ES2Plus_ConnHdlr { + log("=== Test Case: CancelOrder - Error: Incorrect MatchingID ==="); + + // Initialize + f_init_es2plus(); + + // Create and confirm a profile + var JSON_ES2p_Request dl_req := { + downloadOrderRequest := { + header := f_es2plus_request_header(), + eid := c_eid1, + iccid := omit, + profileType := "Test" + } + }; + + var JSON_ES2p_Response dl_resp := f_es2plus_http_request(c_path_download_order, "downloadOrder", dl_req); + + if (not f_es2plus_check_success(dl_resp.downloadOrderResponse.header)) { + setverdict(fail, "DownloadOrder failed"); + mtc.stop; + } + + var charstring iccid := dl_resp.downloadOrderResponse.iccid; + + // Confirm to get a matchingID + var JSON_ES2p_Request conf_req := { + confirmOrderRequest := { + header := f_es2plus_request_header(), + iccid := iccid, + eid := omit, + matchingId := omit, + confirmationCode := omit, + smdsAddress := omit, + releaseFlag := false + } + }; + + var JSON_ES2p_Response conf_resp := f_es2plus_http_request(c_path_confirm_order, "confirmOrder", conf_req); + + if (not f_es2plus_check_success(conf_resp.confirmOrderResponse.header)) { + setverdict(fail, "ConfirmOrder failed"); + mtc.stop; + } + + // Try to cancel with wrong matchingID + f_TC_CancelOrder_Generic({ + testName := "CancelOrder - Error: Incorrect MatchingID", + iccid := iccid, + eid := c_eid1, + matchingId := "WRONG_MATCHING_ID", + finalProfileStatusIndicator := "Available", + expectError := true, + expectedSubjectCode := "8.2.6", + expectedReasonCode := "3.10" + }); +} + +// Test: CancelOrder Error - Profile in Available state +testcase TC_ES2Plus_CancelOrder_Error_04_Profile_Available() runs on ES2Plus_ConnHdlr { + log("=== Test Case: CancelOrder - Error: Profile in Available State ==="); + + // Initialize + f_init_es2plus(); + + // Try to cancel an ICCID that hasn't been ordered + var JSON_ES2p_Request req := { + cancelOrderRequest := { + header := f_es2plus_request_header(), + iccid := c_iccid_test_5, + eid := omit, + matchingId := omit, + finalProfileStatusIndicator := "Available" + } + }; + + var JSON_ES2p_Response resp := f_es2plus_http_request(c_path_cancel_order, "cancelOrder", req); + + // Should get an error + if (resp.cancelOrderResponse.header.functionExecutionStatus.status != "Failed") { + setverdict(fail, "Expected error for profile in Available state"); + mtc.stop; + } + + setverdict(pass); +} + +/*********************************************************************** + * Special Test Cases + ***********************************************************************/ + +// Test with wrong client certificate - should fail +testcase TC_ES2Plus_Wrong_Client_Cert() runs on ES2Plus_ConnHdlr { + // Initialize + f_init_es2plus(); + + // Build request + var JSON_ES2p_DownloadOrderRequest req := { + header := f_es2plus_request_header(), + iccid := c_iccid_test_1 + }; + + var JSON_ES2p_Request es2p_req := { + downloadOrderRequest := req + }; + + var integer status_code; + var charstring response; + + // Temporarily set wrong authentication parameters + var integer result := smdpp_Tests.ext_RSPClient_setAuthParams( + g_rsp_client_handle, + true, // useMutualTLS + c_wrong_cert_path, // Using wrong certificate + c_wrong_key_path // Using wrong key + ); + + if (result != 0) { + setverdict(fail, "Failed to set wrong auth parameters"); + mtc.stop; + } + + // Send request using WRONG client certificate + var octetstring req_enc := enc_JSON_ES2p_Request(es2p_req); + var charstring url := c_path_download_order; + + log("ES2+ Request with WRONG certificate to ", url); + response := smdpp_Tests.ext_RSPClient_sendHttpsPostWithAuth( + g_rsp_client_handle, + url, + oct2char(req_enc), + mp_es2plus_server_port, + status_code + ); + + // Should get 401 Unauthorized or error + if (status_code == 401 or status_code == 403 or status_code == 0) { + setverdict(pass, "Server correctly rejected wrong client certificate"); + } else { + setverdict(fail, "Server accepted wrong client certificate! Status: ", status_code); + } + + // Restore correct auth params for cleanup + smdpp_Tests.ext_RSPClient_setAuthParams( + g_rsp_client_handle, + true, + c_cert_path, + c_key_path + ); +} + +// Test control section +control { + // Wrong certificate test + execute(TC_ES2Plus_Wrong_Client_Cert()); + + // DownloadOrder tests + execute(TC_ES2Plus_DownloadOrder_01_Nominal_ICCID()); + execute(TC_ES2Plus_DownloadOrder_02_Nominal_ProfileType()); + execute(TC_ES2Plus_DownloadOrder_Error_01_ICCID_AlreadyInUse()); + execute(TC_ES2Plus_DownloadOrder_Error_02_Unknown_ICCID()); + + // ConfirmOrder tests + execute(TC_ES2Plus_ConfirmOrder_01_Nominal()); + execute(TC_ES2Plus_ConfirmOrder_Error_01_Unknown_Profile()); + execute(TC_ES2Plus_ConfirmOrder_Error_02_Profile_Available()); + execute(TC_ES2Plus_ConfirmOrder_Error_03_Conflicting_MatchingID()); + execute(TC_ES2Plus_ConfirmOrder_Error_04_Incorrect_SmdsAddress()); + execute(TC_ES2Plus_ConfirmOrder_Error_05_Missing_EID_Empty_MatchingID()); + execute(TC_ES2Plus_ConfirmOrder_Error_06_Different_EID()); + + // CancelOrder tests + execute(TC_ES2Plus_CancelOrder_01_Nominal()); + execute(TC_ES2Plus_CancelOrder_Error_01_Unknown_ICCID()); + execute(TC_ES2Plus_CancelOrder_Error_02_Missing_EID()); + execute(TC_ES2Plus_CancelOrder_Error_03_Incorrect_MatchingID()); + execute(TC_ES2Plus_CancelOrder_Error_04_Profile_Available()); +} + +} \ No newline at end of file diff --git a/smdpp/rsp_client.cpp b/smdpp/rsp_client.cpp index fc4f223..d03a64e 100644 --- a/smdpp/rsp_client.cpp +++ b/smdpp/rsp_client.cpp @@ -922,6 +922,17 @@ std::string headers; }; + struct PostConfig { + bool useMutualTLS = false; + std::string clientCertPath; + std::string clientKeyPath; + bool includeAdminProtocolHeader = false; + bool verboseOutput = false; + }; + + ResponseData postJson(const std::string& url, unsigned int port, const std::string& jsonData, X509_STORE* store, std::vector<X509*>& certPool, + const PostConfig& config); + private: static size_t writeCallback(void* contents, size_t size, size_t nmemb, void* userp) { size_t realSize = size * nmemb; @@ -998,67 +1009,89 @@ return CURLE_OK; } +}; - public: - ResponseData postJsonWithCustomVerification(const std::string& url, unsigned int port, const std::string& jsonData, X509_STORE* store, - std::vector<X509*>& certPool) { - ResponseData response; - CURL* curl = curl_easy_init(); +inline HttpClient::ResponseData HttpClient::postJson(const std::string& url, unsigned int port, const std::string& jsonData, X509_STORE* store, + std::vector<X509*>& certPool, const PostConfig& config) { + ResponseData response; + CURL* curl = curl_easy_init(); - if (!curl) { - throw std::runtime_error("Failed to initialize CURL"); + if (!curl) { + throw std::runtime_error("Failed to initialize CURL"); + } + + struct curl_slist* headers = nullptr; + if (config.useMutualTLS) { + headers = curl_slist_append(headers, "Content-Type: application/json;charset=UTF-8"); + headers = curl_slist_append(headers, "Accept: application/json"); + if (config.includeAdminProtocolHeader) { + headers = curl_slist_append(headers, "X-Admin-Protocol: gsma/rsp/v2.5.0"); } - - struct curl_slist* headers = nullptr; + } else { headers = curl_slist_append(headers, "Content-Type: application/json"); headers = curl_slist_append(headers, "Accept: application/json"); + } - SslCtxData ctxData = { .store = store, .certPool = &certPool, .verifyResult = false, .errorMessage = "" }; + SslCtxData ctxData = { .store = store, .certPool = &certPool, .verifyResult = false, .errorMessage = "" }; - try { - // Basic CURL setup - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_PORT, port); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonData.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body); - curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback); - curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response.headers); + try { + // Basic CURL setup + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_PORT, port); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonData.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response.headers); - // Enable SSL verification - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + // Enable SSL verification + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); - // Use our custom SSL context function and data - curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, sslCtxFunction); - curl_easy_setopt(curl, CURLOPT_SSL_CTX_DATA, &ctxData); + if (config.useMutualTLS) { + if (!config.clientCertPath.empty()) { + curl_easy_setopt(curl, CURLOPT_SSLCERT, config.clientCertPath.c_str()); - // Don't disable the default CA bundle - custom certs will be added to it - // This allows both system CAs and custom test certificates to work - // together - - // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); - - CURLcode res = curl_easy_perform(curl); - - if (res != CURLE_OK) { - throw std::runtime_error(std::string("CURL request failed: ") + curl_easy_strerror(res)); + std::string certType = "PEM"; + if (config.clientCertPath.find(".der") != std::string::npos || config.clientCertPath.find(".DER") != std::string::npos) { + certType = "DER"; + } + curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, certType.c_str()); + LOG_DEBUG("Set client certificate: " + config.clientCertPath + " (type: " + certType + ")"); } - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.statusCode); - - curl_slist_free_all(headers); - curl_easy_cleanup(curl); - - return response; - } catch (...) { - curl_slist_free_all(headers); - curl_easy_cleanup(curl); - throw; + if (!config.clientKeyPath.empty()) { + curl_easy_setopt(curl, CURLOPT_SSLKEY, config.clientKeyPath.c_str()); + curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "PEM"); + LOG_DEBUG("Set client private key: " + config.clientKeyPath); + } } + + // Use our custom SSL context function for server cert verification + curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, sslCtxFunction); + curl_easy_setopt(curl, CURLOPT_SSL_CTX_DATA, &ctxData); + + curl_easy_setopt(curl, CURLOPT_VERBOSE, config.verboseOutput ? 1L : 0L); + + CURLcode res = curl_easy_perform(curl); + + if (res != CURLE_OK) { + throw std::runtime_error(std::string("CURL request failed: ") + curl_easy_strerror(res)); + } + + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.statusCode); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + return response; + } catch (...) { + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + throw; } -}; +} // Hashed Confirmation Code = SHA256(SHA256(Confirmation Code) | TransactionID) static std::vector<uint8_t> computeConfirmationCodeHash(const std::string& confirmationCode, const std::vector<uint8_t>& transactionId) { @@ -1089,7 +1122,7 @@ public: RSPClient(const std::string& serverUrl, const unsigned int serverPort, const std::vector<std::string>& certPath, const std::vector<std::string>& name_filters = {}) - : m_serverUrl(serverUrl), m_serverPort(serverPort) { + : m_serverUrl(serverUrl) { // OpenSSL 3.0+ initializes automatically for (auto cpath : certPath) { @@ -1165,11 +1198,11 @@ try { m_EID = CertificateUtil::getEID(m_euiccCert.get()); - LOG_INFO("Using EID from certificate: " + m_EID); + LOG_DEBUG("Using EID from certificate: " + m_EID); auto ski = CertificateUtil::getSubjectKeyIdentifier(m_euiccCert.get()); m_euiccSKI = ski; - LOG_INFO("Using eUICC SKI as PKID: " + HexUtil::bytesToHex(m_euiccSKI)); + LOG_DEBUG("Using eUICC SKI as PKID: " + HexUtil::bytesToHex(m_euiccSKI)); } catch (const std::exception& e) { LOG_ERROR("Failed to extract EUICC-specific data: " + std::string(e.what())); throw; @@ -1576,7 +1609,29 @@ m_customTlsCertPath = customTlsCertPath; } - std::string sendHttpsPost(const std::string& endpoint, const std::string& body, int& httpStatusCode) { + // Set authentication parameters for mutual TLS auth + void setAuthParams(bool useMutualTLS, const std::string& clientCertPath, const std::string& clientKeyPath) { + m_useMutualTLS = useMutualTLS; + m_clientCertPath = clientCertPath; + m_clientKeyPath = clientKeyPath; + LOG_DEBUG("Auth params set: mutualTLS=" + std::to_string(useMutualTLS) + ", cert=" + clientCertPath + ", key=" + clientKeyPath); + } + + // Send HTTPS POST without client authentication (for ES9+) + std::string sendHttpsPost(const std::string& endpoint, const std::string& body, int& httpStatusCode, unsigned int portOverride) { + return sendHttpsPostUnified(endpoint, body, httpStatusCode, portOverride); + } + + // Send HTTPS POST with pre-configured client authentication (for ES2+) + std::string sendHttpsPostWithAuth(const std::string& endpoint, const std::string& body, int& httpStatusCode, unsigned int portOverride) { + if (!m_useMutualTLS) { + LOG_WARNING("sendHttpsPostWithAuth called but mutual TLS not configured"); + } + return sendHttpsPostUnified(endpoint, body, httpStatusCode, portOverride, m_useMutualTLS, m_clientCertPath, m_clientKeyPath); + } + + std::string sendHttpsPostUnified(const std::string& endpoint, const std::string& body, int& httpStatusCode, unsigned int portOverride, + bool useMutualTLS = false, const std::string& clientCertPath = "", const std::string& clientKeyPath = "") { if (!m_httpClient) { m_httpClient = std::make_unique<HttpClient>(); } @@ -1584,33 +1639,37 @@ // full URL without port (port set via CURLOPT_PORT) std::string url = "https://" + m_serverUrl + endpoint; - HttpClient::ResponseData response; - - if (m_useCustomTlsCert) { - if (!m_customTlsCertPath.empty()) { - try { - auto tlsCerts = CertificateUtil::loadCertificateChain(m_customTlsCertPath); - for (auto& cert : tlsCerts) { - m_certPool.push_back(std::move(cert)); - } - LOG_DEBUG("Loaded custom TLS certificate from: " + m_customTlsCertPath); - } catch (const std::exception& e) { - LOG_WARNING("Failed to load custom TLS certificate: " + std::string(e.what())); - } - } - - std::vector<X509*> rawCertPool; - for (const auto& cert : m_certPool) { - rawCertPool.push_back(cert.get()); - } - - response = m_httpClient->postJsonWithCustomVerification(url, m_serverPort, body, nullptr, rawCertPool); - } else { - std::vector<X509*> emptyCertPool; - response = m_httpClient->postJsonWithCustomVerification(url, m_serverPort, body, nullptr, emptyCertPool); - LOG_INFO("Using standard CA bundle for TLS verification"); + // Build certificate pool for server verification + std::vector<X509*> rawCertPool; + for (const auto& cert : m_certPool) { + rawCertPool.push_back(cert.get()); } + // Add custom TLS server certificate if configured globally + if (m_useCustomTlsCert && !m_customTlsCertPath.empty()) { + try { + auto tlsCerts = CertificateUtil::loadCertificateChain(m_customTlsCertPath); + for (auto& cert : tlsCerts) { + rawCertPool.push_back(cert.get()); + } + LOG_DEBUG("Added custom TLS server certificate from: " + m_customTlsCertPath); + } catch (const std::exception& e) { + LOG_WARNING("Failed to load custom TLS server certificate: " + std::string(e.what())); + } + } + + HttpClient::PostConfig config; + config.useMutualTLS = useMutualTLS; + config.clientCertPath = clientCertPath; + config.clientKeyPath = clientKeyPath; + config.includeAdminProtocolHeader = useMutualTLS; // ES2+ requires this header + config.verboseOutput = false; + + LOG_DEBUG("Sending " + std::string(useMutualTLS ? "ES2+ request with mutual TLS" : "HTTPS request") + " to: " + endpoint + + " (port: " + std::to_string(portOverride) + ")"); + + HttpClient::ResponseData response = m_httpClient->postJson(url, portOverride, body, nullptr, rawCertPool, config); + httpStatusCode = response.statusCode; LOG_DEBUG("HTTP " + std::to_string(httpStatusCode) + " response, body length: " + std::to_string(response.body.length())); @@ -1619,7 +1678,6 @@ } std::string m_serverUrl; - unsigned int m_serverPort; std::unique_ptr<X509, X509Deleter> m_rootCA; std::vector<std::unique_ptr<X509, X509Deleter>> m_intermediateCA; @@ -1646,6 +1704,11 @@ std::unique_ptr<HttpClient> m_httpClient; bool m_useCustomTlsCert = true; // Flag for custom vs public CA std::string m_customTlsCertPath; // Path to custom TLS certificate + + // Authentication parameters for mutual TLS auth + bool m_useMutualTLS = false; + std::string m_clientCertPath = ""; + std::string m_clientKeyPath = ""; }; } // namespace RspCrypto \ No newline at end of file diff --git a/smdpp/smdpp_Tests.ttcn b/smdpp/smdpp_Tests.ttcn index 40d663e..b2842a1 100644 --- a/smdpp/smdpp_Tests.ttcn +++ b/smdpp/smdpp_Tests.ttcn @@ -33,6 +33,7 @@ import from es9p_Types_JSON all; import from es2p_Types_JSON all; import from esx_header_Types_JSON all; +import from ES2Plus_Tests all; /* C++ handles only crypto, TTCN-3 handles ASN.1 encoding/decoding most of the time */ @@ -208,6 +209,7 @@ integer clientHandle, charstring endpoint, charstring body, + integer dport, out integer statusCode ) return charstring; @@ -217,6 +219,21 @@ charstring customTlsCertPath ) return integer; +external function ext_RSPClient_setAuthParams( + integer clientHandle, + boolean useMutualTLS, + charstring clientCertPath, + charstring clientKeyPath +) return integer; + +external function ext_RSPClient_sendHttpsPostWithAuth( + integer clientHandle, + charstring endpoint, + charstring body, + integer dport, + out integer statusCode +) return charstring; + /* RSP Protocol Constants */ const charstring c_oid_rspRole_dp_auth := "2.23.146.1.2.1.4"; const charstring c_oid_rspRole_dp_pb := "2.23.146.1.2.1.5"; @@ -234,6 +251,12 @@ const integer c_hostId_max_length := 16; const integer c_min_segment_length := 2; +// ensure canonical DER encoding, using a width 4 bitstring here leads to violations +const NotificationEvent c_notificationInstall := '1'B; // Bit 0 +const NotificationEvent c_notificationEnable := '10'B; // Bit 1 +const NotificationEvent c_notificationDisable := '100'B; // Bit 2 +const NotificationEvent c_notificationDelete := '1000'B; // Bit 3 + /* Error injection types for AuthenticateClient testing */ type union AuthClientErrorInjection { CertificateError cert_error, @@ -316,7 +339,8 @@ type record smdpp_ConnHdlrPars { charstring smdp_server_url, - integer smdp_server_port, + integer smdp_es9p_server_port, + integer smdp_es2p_server_port, charstring cert_path, charstring cert_name_filter, charstring euicc_cert_path, @@ -335,7 +359,8 @@ private function f_init_pars() runs on MTC_CT return smdpp_ConnHdlrPars { var smdpp_ConnHdlrPars pars := { smdp_server_url := "testsmdpplus1.example.com", - smdp_server_port := 8000, + smdp_es9p_server_port := 8000, + smdp_es2p_server_port := 8000, cert_path := "./sgp26/CertificateIssuer", cert_name_filter := "", // Load all certificates (both NIST and BRP) or "NIST" or "BRP" euicc_cert_path := "./sgp26/eUICC/CERT_EUICC_ECDSA_NIST.der", @@ -396,7 +421,7 @@ g_rsp_client_handle := ext_RSPClient_create( g_pars_smdpp.smdp_server_url, - g_pars_smdpp.smdp_server_port, + g_pars_smdpp.smdp_es9p_server_port, g_pars_smdpp.cert_path, g_pars_smdpp.cert_name_filter ); @@ -448,6 +473,137 @@ } } +/* HELPER FUNCTIONS FOR ES2+ DYNAMIC PROFILE PROVISIONING */ + +// Main ES2+ provisioning function - provisions a profile dynamically via ES2+ +private function f_provision_profile_via_es2plus( + charstring eid := "89049032123451234512345678901235", + boolean with_confirmation_code := false, + charstring profile_type := "Test", + boolean release_flag := true +) runs on smdpp_ConnHdlr return charstring { + + // ES2+ constants + const charstring c_es2plus_base_path := "/gsma/rsp2/es2plus"; + const charstring c_path_download_order := c_es2plus_base_path & "/downloadOrder"; + const charstring c_path_confirm_order := c_es2plus_base_path & "/confirmOrder"; + const charstring c_cert_path := "./test_certs/CERT_MNO_ECDSA_NIST.pem"; + const charstring c_key_path := "./test_certs/SK_MNO_ECDSA_NIST.pem"; + + // Generate unique function call identifier + var octetstring rnd_oct := f_rnd_octstring(4); + var charstring func_call_id := "TTCN3-" & oct2str(rnd_oct); + + var integer result := smdpp_Tests.ext_RSPClient_setAuthParams( + g_rsp_client_handle, + true, // useMutualTLS + c_cert_path, + c_key_path + ); + + if (result != 0) { + setverdict(fail, "Failed to set authentication parameters"); + mtc.stop; + } + + // Step 1: DownloadOrder to reserve ICCID + var JSON_ES2p_Request dl_req := { + downloadOrderRequest := { + header := { + functionRequesterIdentifier := "test.operator.com", + functionCallIdentifier := func_call_id & "-DL" + }, + eid := eid, + iccid := omit, + profileType := profile_type + } + }; + + var octetstring req_enc := enc_JSON_ES2p_Request(dl_req); + var integer status_code; + var charstring response := ext_RSPClient_sendHttpsPostWithAuth( + g_rsp_client_handle, + c_path_download_order, + oct2char(req_enc), + g_pars_smdpp.smdp_es2p_server_port, + status_code + ); + + if (status_code != 200) { + ext_logError("ES2+ DownloadOrder failed with status: " & int2str(status_code)); + return ""; + } + + var JSON_ES2p_Response dl_resp := dec_JSON_ES2p_Response(char2oct(response), "downloadOrder"); + if (dl_resp.downloadOrderResponse.header.functionExecutionStatus.status != "Executed-Success") { + ext_logError("ES2+ DownloadOrder returned error"); + return ""; + } + + var charstring new_iccid := dl_resp.downloadOrderResponse.iccid; + ext_logInfo("ES2+ provisioned ICCID: " & new_iccid); + + // Step 2: ConfirmOrder to create profile with matchingId + var JSON_ES2p_Request conf_req := { + confirmOrderRequest := { + header := { + functionRequesterIdentifier := "test.operator.com", + functionCallIdentifier := func_call_id & "-CF" + }, + iccid := new_iccid, + eid := omit, + matchingId := omit, // Let server generate + confirmationCode := omit, + smdsAddress := omit, + releaseFlag := release_flag + } + }; + + if (with_confirmation_code) { + conf_req.confirmOrderRequest.confirmationCode := "12345678"; + } + + req_enc := enc_JSON_ES2p_Request(conf_req); + response := ext_RSPClient_sendHttpsPostWithAuth( + g_rsp_client_handle, + c_path_confirm_order, + oct2char(req_enc), + g_pars_smdpp.smdp_es2p_server_port, + status_code + ); + + if (status_code != 200) { + ext_logError("ES2+ ConfirmOrder failed with status: " & int2str(status_code)); + return ""; + } + + var JSON_ES2p_Response conf_resp := dec_JSON_ES2p_Response(char2oct(response), "confirmOrder"); + if (conf_resp.confirmOrderResponse.header.functionExecutionStatus.status != "Executed-Success") { + ext_logError("ES2+ ConfirmOrder returned error"); + return ""; + } + + var charstring matching_id := conf_resp.confirmOrderResponse.matchingId; + ext_logInfo("ES2+ provisioned profile with matchingId: " & matching_id); + + // Store confirmation code if requested + if (with_confirmation_code) { + ext_logInfo("Profile provisioned with confirmation code required"); + } + + return matching_id; +} + +// Simplified variant for basic profile provisioning +private function f_provision_simple_profile() runs on smdpp_ConnHdlr return charstring { + return f_provision_profile_via_es2plus(); +} + +// Variant for profile with confirmation code +private function f_provision_profile_with_cc(charstring eid := "89049032123451234512345678901235") runs on smdpp_ConnHdlr return charstring { + return f_provision_profile_via_es2plus(eid, true); +} + /* HELPER FUNCTIONS FOR CERTIFICATE PKID MANAGEMENT */ private function f_get_ci_pkids_list() runs on smdpp_ConnHdlr return RSPDefinitions.EUICCInfo2.euiccCiPKIdListForVerification { if (g_pars_smdpp.cert_name_filter == "BRP") { @@ -722,6 +878,7 @@ g_rsp_client_handle, g_last_es9p_endpoint, g_last_es9p_request, + g_pars_smdpp.smdp_es9p_server_port, http_status ); @@ -771,6 +928,7 @@ g_rsp_client_handle, g_last_es9p_endpoint, g_last_es9p_request, + g_pars_smdpp.smdp_es9p_server_port, http_status ); @@ -904,8 +1062,13 @@ } private function f_perform_authenticate_client_standard( - charstring matchingId := "TS48v4_SAIP2.3_BERTLV" + charstring matchingId := "" /* Empty string will trigger dynamic provisioning */ ) runs on smdpp_ConnHdlr return AuthenticateClientOk { + /* Provision profile dynamically if matchingId not provided */ + if (matchingId == "") { + matchingId := f_provision_simple_profile(); + } + var EUICCInfo2 euiccInfo2 := valueof(ts_EUICCInfo2); f_set_euicc_pkids(euiccInfo2); @@ -940,6 +1103,11 @@ ) return octetstring with { extension "prototype(convert) encode(BER:BER_ENCODE_DER)" }; +external function encode_DER_profileInstallationResultData( + in ProfileInstallationResultData pdu +) return octetstring +with { extension "prototype(convert) encode(BER:BER_ENCODE_DER)" }; + /* VALIDATION HELPERS */ private function f_validate_initialise_secure_channel_request(InitialiseSecureChannelRequest iscReq) @@ -1110,6 +1278,7 @@ g_rsp_client_handle, g_last_es9p_endpoint, g_last_es9p_request, + g_pars_smdpp.smdp_es9p_server_port, http_status ); @@ -1182,7 +1351,7 @@ runs on smdpp_ConnHdlr return ProfileInstallationResult { var NotificationMetadata notifMeta := { seqNumber := 1, - profileManagementOperation := '00000001'B, /* notificationInstall - bit 0 set */ + profileManagementOperation := c_notificationInstall, notificationAddress := g_pars_smdpp.smdp_server_url, iccid := omit }; @@ -1214,11 +1383,7 @@ }; } - var ProfileInstallationResult temp_pir := { - profileInstallationResultData := pirData, - euiccSignPIR := ''O /* Empty signature for encoding */ - }; - var octetstring full_encoded := enc_ProfileInstallationResult(temp_pir); + var octetstring full_encoded := encode_DER_profileInstallationResultData(pirData); var octetstring euiccSignPIR := ext_RSPClient_signDataWithEUICC(g_rsp_client_handle, full_encoded); var ProfileInstallationResult pir := { @@ -1244,7 +1409,7 @@ private function f_buildAuthenticateClientRequest_unified( octetstring serverChallenge, - charstring matchingId := "TS48v4_SAIP2.3_BERTLV", + charstring matchingId := "", /* Empty string will trigger dynamic provisioning */ template (omit) EUICCInfo2 euiccInfo2 := omit, // omit = create default template (omit) DeviceInfo deviceInfo := omit, // omit = no device info template (omit) octetstring transactionIdOverride := omit, // omit = use correct ID @@ -1265,12 +1430,16 @@ var CtxParams1 ctxParams := valueof(ts_ctxParams1); /* matchingId: * - "OMIT" -> field omitted from ASN.1 structure - * - "" -> field is present but empty + * - "" -> triggers dynamic provisioning and uses the result * - any other value -> field contains that value */ if (matchingId == "OMIT") { ctxParams.ctxParamsForCommonAuthentication.matchingId := omit; } else { + /* Provision profile dynamically if matchingId empty */ + if (matchingId == "") { + matchingId := f_provision_simple_profile(); + } ctxParams.ctxParamsForCommonAuthentication.matchingId := matchingId; } @@ -1314,7 +1483,7 @@ private function f_buildAuthenticateClientRequest( octetstring serverChallenge, - charstring matchingId := "TS48v4_SAIP2.3_BERTLV", + charstring matchingId := "", /* Empty string will trigger dynamic provisioning */ template (omit) octetstring transactionIdOverride := omit // error injection - omit means correct ID ) runs on smdpp_ConnHdlr return RemoteProfileProvisioningRequest { return f_buildAuthenticateClientRequest_unified( @@ -1500,7 +1669,12 @@ } } -private function f_create_authenticate_client_request(charstring matchingId := "TS48v4_SAIP2.3_BERTLV") runs on smdpp_ConnHdlr return RemoteProfileProvisioningRequest { +private function f_create_authenticate_client_request(charstring matchingId := "") runs on smdpp_ConnHdlr return RemoteProfileProvisioningRequest { + /* Provision profile dynamically if matchingId not provided */ + if (matchingId == "") { + matchingId := f_provision_simple_profile(); + } + var EUICCInfo2 euiccInfo2 := valueof(ts_EUICCInfo2); f_set_euicc_pkids(euiccInfo2); @@ -1613,7 +1787,10 @@ /* Build CtxParams */ var CtxParams1 ctxParams := valueof(ts_ctxParams1); - ctxParams.ctxParamsForCommonAuthentication.matchingId := "TS48v4_SAIP2.3_BERTLV"; + + /* Provision profile dynamically unless error injection overrides it */ + var charstring provisionedMatchingId := f_provision_simple_profile(); + ctxParams.ctxParamsForCommonAuthentication.matchingId := provisionedMatchingId; /* Apply context parameter errors if specified */ if (ischosen(err_injection.ctx_error)) { @@ -2060,7 +2237,7 @@ var AuthClientErrorTestParams params := { testName := "AuthenticateClient - Mismatched Transaction ID", stepDescription := "AuthenticateClient with wrong transaction ID in signed data", - matchingId := "TS48v4_SAIP2.3_BERTLV", + matchingId := omit, /* Will be provisioned dynamically */ transactionIdOverride := wrongTransactionId, expectedSubjectCode := "8.10.1", expectedReasonCode := "3.9", @@ -2103,8 +2280,11 @@ var EUICCInfo2 euiccInfo2 := valueof(ts_EUICCInfo2); f_set_euicc_pkids(euiccInfo2); + /* Provision profile dynamically */ + var charstring provisionedMatchingId := f_provision_simple_profile(); + var CtxParams1 ctxParams := valueof(ts_ctxParams1); - ctxParams.ctxParamsForCommonAuthentication.matchingId := "TS48v4_SAIP2.3_BERTLV"; + ctxParams.ctxParamsForCommonAuthentication.matchingId := provisionedMatchingId; var EuiccSigned1 euiccSigned1 := f_create_euicc_signed1(euiccInfo2, ctxParams); var octetstring euiccSigned1_raw := enc_EuiccSigned1(euiccSigned1); @@ -2508,11 +2688,16 @@ } private function f_perform_session_through_getBoundProfilePackage( - charstring matchingId := "TS48v4_SAIP2.3_BERTLV" + charstring matchingId := "" /* Empty string will trigger dynamic provisioning */ ) runs on smdpp_ConnHdlr return GetBoundProfilePackageOk { + /* Provision profile dynamically if matchingId not provided */ + if (matchingId == "") { + matchingId := f_provision_simple_profile(); + } + var InitiateAuthenticationOkEs9 authOk := f_initiate_authentication_and_validate(); - var RemoteProfileProvisioningRequest authClientReq := f_create_authenticate_client_request(); + var RemoteProfileProvisioningRequest authClientReq := f_create_authenticate_client_request(matchingId); var RemoteProfileProvisioningResponse authClientResp := f_es9p_transceive_success(authClientReq); var AuthenticateClientOk authClientOk := authClientResp.authenticateClientResponseEs9.authenticateClientOk; @@ -3349,8 +3534,11 @@ euic2.euiccCiPKIdListForVerification := f_get_ci_pkids_for_verification(); euic2.euiccCiPKIdListForSigning := f_get_ci_pkids_for_signing(); + /* Provision profile dynamically */ + var charstring provisionedMatchingId := f_provision_simple_profile(); + var CtxParams1 ctxpar := valueof(ts_ctxParams1); - ctxpar.ctxParamsForCommonAuthentication.matchingId := "TS48v4_SAIP2.3_BERTLV"; + ctxpar.ctxParamsForCommonAuthentication.matchingId := provisionedMatchingId; var EuiccSigned1 euiccSig := { transactionId := g_transactionId, @@ -3718,7 +3906,7 @@ var NotificationMetadata notifMeta := { seqNumber := 2, /* Different sequence number from PIR */ - profileManagementOperation := '00000010'B, /* notificationEnable */ + profileManagementOperation := c_notificationEnable, notificationAddress := g_pars_smdpp.smdp_server_url, iccid := omit /* No specific ICCID for this test */ }; @@ -4447,14 +4635,40 @@ /* Test Case: TC_SM-DP+_ES9+.AuthenticateClientNIST - Error Test Sequence #15 // SGP.23 v1.15 section 4.3.14.2.2 Test Sequence #15 */ private function f_TC_AuthenticateClient_Error_15_NoEligibleProfile(charstring id) runs on smdpp_ConnHdlr { - f_test_authenticateClient_error({ - testName := "AuthenticateClient - Error: No Eligible Profile", - stepDescription := "AuthenticateClient when no profile matches device capabilities", - matchingId := "AC_NO_ELIGIBLE", /* Profile with incompatible requirements */ - expectedSubjectCode := "8.2.5", - expectedReasonCode := "4.3", - errorDescription := "No Eligible Profile test" - }, id); + /* SGP.23 Section 4.3.14.2.2 Test Sequence #15 + * The profile has PPR1 set (disabling not allowed) but the eUICC reports PPR1 as forbidden + * because an operational profile is already installed */ + + if (not f_rsp_client_init()) { + setverdict(fail, "RSP client initialization failed"); + return; + } + + var InitiateAuthenticationOkEs9 authOk := f_initiate_authentication_and_validate(); + + ext_logInfo("Step 2: AuthenticateClient with eUICC reporting forbidden PPR1"); + + var EUICCInfo2 euiccInfo2 := valueof(ts_EUICCInfo2); + f_set_euicc_pkids(euiccInfo2); + /* Set forbiddenProfilePolicyRules to indicate PPR1 is forbidden + * PPR1 (bit 1) = "Disabling of this Profile is not allowed" + * This is forbidden when an operational profile is already installed */ + euiccInfo2.forbiddenProfilePolicyRules := '0100'B; /* PPR1 is bit 1 */ + + var CtxParams1 ctxParams := valueof(ts_ctxParams1); + /* Use AC_PPR1 which has PPR1 set in the profile */ + ctxParams.ctxParamsForCommonAuthentication.matchingId := "AC_PPR1"; + + var EuiccSigned1 euiccSigned1 := f_create_euicc_signed1(euiccInfo2, ctxParams); + var octetstring euiccSignature1 := f_sign_euicc_signed1(euiccSigned1); + + var RemoteProfileProvisioningRequest authReq := f_build_authenticate_client_request(euiccSigned1, euiccSignature1); + var JSON_ESx_FunctionExecutionStatusCodeData err := f_es9p_transceive_error(authReq); + + f_validate_error_response(err, "8.2.5", "4.3", "No eligible Profile for this eUICC/Device"); + setverdict(pass); + + f_rsp_client_cleanup(); } /* Test Case: TC_SM-DP+_ES9+.AuthenticateClientNIST - Error Test Sequence #16 // SGP.23 v1.15 section 4.3.14.2.2 Test Sequence #16 */ @@ -5157,7 +5371,7 @@ testcase TC_SM_DP_ES9_InitiateAuthenticationBRP_01_Nominal() runs on MTC_CT { var smdpp_ConnHdlrPars pars := f_init_pars(); - pars.smdp_server_port := 8001; /* BRP server port */ + pars.smdp_es9p_server_port := 8001; /* BRP server port */ pars.cert_name_filter := "BRP"; /* Use Brainpool certificates */ pars.euicc_cert_path := "./sgp26/eUICC/CERT_EUICC_ECDSA_BRP.der"; pars.euicc_key_path := "./sgp26/eUICC/SK_EUICC_ECDSA_BRP.pem"; @@ -5170,7 +5384,7 @@ testcase TC_SM_DP_ES9_AuthenticateClientBRP_01_Nominal() runs on MTC_CT { var smdpp_ConnHdlrPars pars := f_init_pars(); - pars.smdp_server_port := 8001; /* BRP server port */ + pars.smdp_es9p_server_port := 8001; /* BRP server port */ pars.cert_name_filter := "BRP"; /* Use Brainpool certificates */ pars.euicc_cert_path := "./sgp26/eUICC/CERT_EUICC_ECDSA_BRP.der"; pars.euicc_key_path := "./sgp26/eUICC/SK_EUICC_ECDSA_BRP.pem"; @@ -5183,7 +5397,7 @@ testcase TC_SM_DP_ES9_GetBoundProfilePackageBRP_01_Nominal() runs on MTC_CT { var smdpp_ConnHdlrPars pars := f_init_pars(); - pars.smdp_server_port := 8001; /* BRP server port */ + pars.smdp_es9p_server_port := 8001; /* BRP server port */ pars.cert_name_filter := "BRP"; /* Use Brainpool certificates */ pars.euicc_cert_path := "./sgp26/eUICC/CERT_EUICC_ECDSA_BRP.der"; pars.euicc_key_path := "./sgp26/eUICC/SK_EUICC_ECDSA_BRP.pem"; @@ -5196,7 +5410,7 @@ testcase TC_rsp_complete_flow_BRP() runs on MTC_CT { var smdpp_ConnHdlrPars pars := f_init_pars(); - pars.smdp_server_port := 8001; /* BRP server port */ + pars.smdp_es9p_server_port := 8001; /* BRP server port */ pars.cert_name_filter := "BRP"; /* Use Brainpool certificates */ pars.euicc_cert_path := "./sgp26/eUICC/CERT_EUICC_ECDSA_BRP.der"; pars.euicc_key_path := "./sgp26/eUICC/SK_EUICC_ECDSA_BRP.pem"; @@ -5209,7 +5423,7 @@ testcase TC_SM_DP_ES9_HandleNotificationBRP() runs on MTC_CT { var smdpp_ConnHdlrPars pars := f_init_pars(); - pars.smdp_server_port := 8001; /* BRP server port */ + pars.smdp_es9p_server_port := 8001; /* BRP server port */ pars.cert_name_filter := "BRP"; /* Use Brainpool certificates */ pars.euicc_cert_path := "./sgp26/eUICC/CERT_EUICC_ECDSA_BRP.der"; pars.euicc_key_path := "./sgp26/eUICC/SK_EUICC_ECDSA_BRP.pem"; @@ -5222,7 +5436,7 @@ testcase TC_SM_DP_ES9_CancelSession_After_AuthenticateClientBRP() runs on MTC_CT { var smdpp_ConnHdlrPars pars := f_init_pars(); - pars.smdp_server_port := 8001; /* BRP server port */ + pars.smdp_es9p_server_port := 8001; /* BRP server port */ pars.cert_name_filter := "BRP"; /* Use Brainpool certificates */ pars.euicc_cert_path := "./sgp26/eUICC/CERT_EUICC_ECDSA_BRP.der"; pars.euicc_key_path := "./sgp26/eUICC/SK_EUICC_ECDSA_BRP.pem"; @@ -5235,7 +5449,7 @@ testcase TC_SM_DP_ES9_CancelSession_After_GetBoundProfilePackageBRP() runs on MTC_CT { var smdpp_ConnHdlrPars pars := f_init_pars(); - pars.smdp_server_port := 8001; /* BRP server port */ + pars.smdp_es9p_server_port := 8001; /* BRP server port */ pars.cert_name_filter := "BRP"; /* Use Brainpool certificates */ pars.euicc_cert_path := "./sgp26/eUICC/CERT_EUICC_ECDSA_BRP.der"; pars.euicc_key_path := "./sgp26/eUICC/SK_EUICC_ECDSA_BRP.pem"; @@ -5246,6 +5460,185 @@ setverdict(pass); } +/* ES2+/ES9+ Integration Test Case */ +testcase TC_ES2Plus_ES9Plus_FullFlow() runs on MTC_CT { + var smdpp_ConnHdlrPars pars := f_init_pars(); + var smdpp_ConnHdlr vc_conn; + f_init(testcasename()); + vc_conn := f_start_handler(refers(f_TC_ES2Plus_ES9Plus_FullFlow), pars); + vc_conn.done; + setverdict(pass); +} + +/* ES2+/ES9+ Integration Test Implementation */ +private function f_TC_ES2Plus_ES9Plus_FullFlow(charstring id) runs on smdpp_ConnHdlr { + // ES2+ constants + const charstring c_es2plus_base_path := "/gsma/rsp2/es2plus"; + const charstring c_path_download_order := c_es2plus_base_path & "/downloadOrder"; + const charstring c_path_confirm_order := c_es2plus_base_path & "/confirmOrder"; + const charstring c_eid1 := "89049032123451234512345678901235"; + const charstring c_cert_path := "./test_certs/CERT_MNO_ECDSA_NIST.pem"; + const charstring c_key_path := "./test_certs/SK_MNO_ECDSA_NIST.pem"; + + if (not f_rsp_client_init()) { + setverdict(fail, "RSP client initialization failed"); + return; + } + + var integer result := smdpp_Tests.ext_RSPClient_setAuthParams( + g_rsp_client_handle, + true, // useMutualTLS + c_cert_path, + c_key_path + ); + + if (result != 0) { + setverdict(fail, "Failed to set authentication parameters"); + mtc.stop; + } + + // Step 1: Order profile via ES2+ + + // 1.1: dl req + var JSON_ES2p_Request dl_req := { + downloadOrderRequest := { + header := { + functionRequesterIdentifier := "test.operator.com", + functionCallIdentifier := "01234567890123456789012345678901" + }, + eid := c_eid1, + iccid := omit, + profileType := "Test" + } + }; + + var octetstring req_enc := enc_JSON_ES2p_Request(dl_req); + var integer status_code; + var charstring response := ext_RSPClient_sendHttpsPostWithAuth( + g_rsp_client_handle, + c_path_download_order, + oct2char(req_enc), + g_pars_smdpp.smdp_es2p_server_port, + status_code + ); + + if (status_code != 200) { + setverdict(fail, "DownloadOrder failed with status: ", status_code); + f_rsp_client_cleanup(); + return; + } + + var JSON_ES2p_Response dl_resp := dec_JSON_ES2p_Response(char2oct(response), "downloadOrder"); + if (dl_resp.downloadOrderResponse.header.functionExecutionStatus.status != "Executed-Success") { + setverdict(fail, "DownloadOrder returned error"); + f_rsp_client_cleanup(); + return; + } + + var charstring new_iccid := dl_resp.downloadOrderResponse.iccid; + + // 1.2: confirm + var JSON_ES2p_Request conf_req := { + confirmOrderRequest := { + header := { + functionRequesterIdentifier := "test.operator.com", + functionCallIdentifier := "02234567890123456789012345678901" + }, + iccid := new_iccid, + eid := omit, + matchingId := omit, + confirmationCode := omit, + smdsAddress := omit, + releaseFlag := true + } + }; + + req_enc := enc_JSON_ES2p_Request(conf_req); + response := ext_RSPClient_sendHttpsPostWithAuth( + g_rsp_client_handle, + c_path_confirm_order, + oct2char(req_enc), + g_pars_smdpp.smdp_es2p_server_port, + status_code + ); + + if (status_code != 200) { + setverdict(fail, "ConfirmOrder failed with status: ", status_code); + f_rsp_client_cleanup(); + return; + } + + var JSON_ES2p_Response conf_resp := dec_JSON_ES2p_Response(char2oct(response), "confirmOrder"); + if (conf_resp.confirmOrderResponse.header.functionExecutionStatus.status != "Executed-Success") { + setverdict(fail, "ConfirmOrder returned error"); + f_rsp_client_cleanup(); + return; + } + + var charstring matching_id := conf_resp.confirmOrderResponse.matchingId; + + // Step 2: Download profile via ES9+ + + // Step 2.1: InitiateAuthentication + var RemoteProfileProvisioningRequest initAuthReq := f_create_initiate_authentication_request(); + var RemoteProfileProvisioningResponse initAuthResp := f_es9p_transceive_success(initAuthReq); + + if (not f_validate_initiate_authentication_response(initAuthResp)) { + setverdict(fail, "InitiateAuthentication validation failed"); + f_rsp_client_cleanup(); + return; + } + + var InitiateAuthenticationOkEs9 initAuthOk := initAuthResp.initiateAuthenticationResponse.initiateAuthenticationOk; + + // Step 2.2: AuthenticateClient with ES2+ matchingId + + var RemoteProfileProvisioningRequest authClientReq := f_buildAuthenticateClientRequest( + initAuthOk.serverSigned1.serverChallenge, + matching_id // Using the matchingId from ES2+ ConfirmOrder + ); + + var RemoteProfileProvisioningResponse authClientResp := f_es9p_transceive_success(authClientReq); + var AuthenticateClientOk authClientOk := authClientResp.authenticateClientResponseEs9.authenticateClientOk; + + f_validateAuthenticateClientResponse(authClientOk); + + // Handle confirmation code if required + if (authClientOk.smdpSigned2.ccRequiredFlag == true) { + log("Confirmation code is required for this profile"); + result := ext_RSPClient_setTransactionId(g_rsp_client_handle, authClientOk.transactionId); + if (result != 0) { + setverdict(fail, "Failed to set transaction ID"); + f_rsp_client_cleanup(); + return; + } + + var charstring confirmationCode := "12345678"; // Default test code + result := ext_RSPClient_setConfirmationCode(g_rsp_client_handle, confirmationCode); + if (result != 0) { + setverdict(fail, "Failed to set confirmation code"); + f_rsp_client_cleanup(); + return; + } + log("Set confirmation code: ", confirmationCode); + } + + // Step 2.3: GetBoundProfilePackage + + var RemoteProfileProvisioningRequest packageReq := f_create_get_bound_profile_package_request( + authClientOk.smdpSignature2 + ); + + var RemoteProfileProvisioningResponse packageResp := f_es9p_transceive_success(packageReq); + var GetBoundProfilePackageOk packageOk := packageResp.getBoundProfilePackageResponse.getBoundProfilePackageOk; + + f_validate_get_bound_profile_package_response(packageResp); + + f_rsp_client_cleanup(); + + setverdict(pass); +} + control { execute(TC_rsp_complete_flow()); @@ -5525,6 +5918,9 @@ execute(TC_SM_DP_ES9_CancelSession_After_GetBoundProfilePackageBRP()); /* Complete flow with BRP certificates */ execute(TC_rsp_complete_flow_BRP()); + + /* ES2+/ES9+ Integration Test */ + execute(TC_ES2Plus_ES9Plus_FullFlow()); } } \ No newline at end of file diff --git a/smdpp/smdpp_Tests_Functions.cc b/smdpp/smdpp_Tests_Functions.cc index 52b6c6b..7bd08e3 100644 --- a/smdpp/smdpp_Tests_Functions.cc +++ b/smdpp/smdpp_Tests_Functions.cc @@ -534,12 +534,12 @@ // Derive BSP session keys using X9.63 KDF (from_kdf) LOG_DEBUG("BSP KDF inputs:"); LOG_DEBUG(" Shared secret: " + HexUtil::bytesToHex(sharedSecretVec)); - LOG_DEBUG(" Key type: " + std::to_string(keyType.get_long_long_val()) + + LOG_DEBUG(" Key type: " + std::to_string(keyType.get_long_long_val()) + " (0x" + HexUtil::bytesToHex({static_cast<uint8_t>(keyType.get_long_long_val())}) + ")"); LOG_DEBUG(" Key length: " + std::to_string(keyLength.get_long_long_val())); LOG_DEBUG(" Host ID: " + HexUtil::bytesToHex(hostIdVec)); LOG_DEBUG(" EID: " + HexUtil::bytesToHex(eidVec)); - + auto bsp = BspCryptoNS::BspCrypto::from_kdf(sharedSecretVec, static_cast<uint8_t>(keyType.get_long_long_val()), static_cast<uint8_t>(keyLength.get_long_long_val()), @@ -574,19 +574,6 @@ }); } -CHARSTRING ext__RSPClient__sendHttpsPost(const INTEGER& clientHandle, const CHARSTRING& endpoint, - const CHARSTRING& body, INTEGER& statusCode) { - statusCode = INTEGER(0); - - return with_client(clientHandle, "ext__RSPClient__sendHttpsPost", CHARSTRING(""), [&](RSPClient* client) { - int httpStatus = 0; - std::string response = - client->sendHttpsPost(charstring_to_string(endpoint), charstring_to_string(body), httpStatus); - statusCode = INTEGER(httpStatus); - return string_to_charstring(response); - }); -} - INTEGER ext__RSPClient__configureHttpClient(const INTEGER& clientHandle, const BOOLEAN& useCustomTlsCert, const CHARSTRING& customTlsCertPath) { return with_client(clientHandle, "ext__RSPClient__configureHttpClient", INTEGER(-1), [&](RSPClient* client) { @@ -599,4 +586,58 @@ }); } +INTEGER ext__RSPClient__setAuthParams(const INTEGER& clientHandle, const BOOLEAN& useMutualTLS, + const CHARSTRING& clientCertPath, const CHARSTRING& clientKeyPath) { + return with_client(clientHandle, "ext__RSPClient__setAuthParams", INTEGER(-1), [&](RSPClient* client) { + bool useMutual = static_cast<bool>(useMutualTLS); + std::string certPath = charstring_to_string(clientCertPath); + std::string keyPath = charstring_to_string(clientKeyPath); + + client->setAuthParams(useMutual, certPath, keyPath); + LOG_DEBUG("Set auth params - mutual TLS: " + std::string(useMutual ? "true" : "false") + + ", cert: " + certPath + ", key: " + keyPath); + return INTEGER(0); + }); +} + +CHARSTRING ext__RSPClient__sendHttpsPost(const INTEGER& clientHandle, const CHARSTRING& endpoint, + const CHARSTRING& body, const INTEGER& port, INTEGER& statusCode) { + statusCode = INTEGER(0); + + return with_client(clientHandle, "ext__RSPClient__sendHttpsPostSimple", CHARSTRING(""), [&](RSPClient* client) { + int httpStatus = 0; + int dstport = static_cast<int>(port); + + std::string response = client->sendHttpsPost( + charstring_to_string(endpoint), + charstring_to_string(body), + httpStatus, + dstport + ); + + statusCode = INTEGER(httpStatus); + return string_to_charstring(response); + }); +} + +CHARSTRING ext__RSPClient__sendHttpsPostWithAuth(const INTEGER& clientHandle, const CHARSTRING& endpoint, + const CHARSTRING& body, const INTEGER& port, INTEGER& statusCode) { + statusCode = INTEGER(0); + + return with_client(clientHandle, "ext__RSPClient__sendHttpsPostWithAuth", CHARSTRING(""), [&](RSPClient* client) { + int httpStatus = 0; + int dstport = static_cast<int>(port); + + std::string response = client->sendHttpsPostWithAuth( + charstring_to_string(endpoint), + charstring_to_string(body), + httpStatus, + dstport + ); + + statusCode = INTEGER(httpStatus); + return string_to_charstring(response); + }); +} + } // namespace smdpp__Tests \ No newline at end of file diff --git a/smdpp/test_certs/CERT_MNO_ECDSA_NIST.pem b/smdpp/test_certs/CERT_MNO_ECDSA_NIST.pem new file mode 100644 index 0000000..f626b2e --- /dev/null +++ b/smdpp/test_certs/CERT_MNO_ECDSA_NIST.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBpDCCAUugAwIBAgIUSeaTfsRONLZXImVBtISYmkx2cuYwCgYIKoZIzj0EAwIw +QTELMAkGA1UEBhMCVVMxFjAUBgNVBAoMDVRlc3QgT3BlcmF0b3IxGjAYBgNVBAMM +EXRlc3Qub3BlcmF0b3IuY29tMB4XDTI1MDgwNDE0MzU1OFoXDTI2MDgwNDE0MzU1 +OFowQTELMAkGA1UEBhMCVVMxFjAUBgNVBAoMDVRlc3QgT3BlcmF0b3IxGjAYBgNV +BAMMEXRlc3Qub3BlcmF0b3IuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +ml9gJuY6zXQS+lAjVTcRfCQpjfHqwFozg3NeyXjjf1DzKV7z+1OxI4wyjVEWDbXR +y3LuqA0IJn9G4OlpPsQx9qMhMB8wHQYDVR0OBBYEFPrtOcEs4Pqf0ehIWfOwEnqD +Z82OMAoGCCqGSM49BAMCA0cAMEQCIGGr25Njje34dwlkmRO1fEI1DfjzstTmBBCr +WruTnbbkAiBsMnKf7EZ937+WIz4Re+lyEtQjEERuT9AUd/yTHqEY0Q== +-----END CERTIFICATE----- diff --git a/smdpp/test_certs/SK_MNO_ECDSA_NIST.pem b/smdpp/test_certs/SK_MNO_ECDSA_NIST.pem new file mode 100644 index 0000000..3c5e5b8 --- /dev/null +++ b/smdpp/test_certs/SK_MNO_ECDSA_NIST.pem @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJKEROO3uDeVP5/iKmGuLK56kIdksG8PISUiCfq58HDfoAoGCCqGSM49 +AwEHoUQDQgAEml9gJuY6zXQS+lAjVTcRfCQpjfHqwFozg3NeyXjjf1DzKV7z+1Ox +I4wyjVEWDbXRy3LuqA0IJn9G4OlpPsQx9g== +-----END EC PRIVATE KEY----- -- To view, visit https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41120?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: merged Gerrit-Project: osmo-ttcn3-hacks Gerrit-Branch: master Gerrit-Change-Id: I2865e016974d7d7a03e00a7795a42f573b147a4b Gerrit-Change-Number: 41120 Gerrit-PatchSet: 7 Gerrit-Owner: Hoernchen <ewild(a)sysmocom.de> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: dexter <pmaier(a)sysmocom.de> Gerrit-Reviewer: laforge <laforge(a)osmocom.org> Gerrit-Reviewer: osmith <osmith(a)sysmocom.de>
1 0
0 0
[XL] Change in osmo-ttcn3-hacks[master]: smdpp: proper headers for native code
by osmith 08 Oct '25

08 Oct '25
Attention is currently required from: Hoernchen. osmith has posted comments on this change by Hoernchen. ( https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41122?usp=email ) Change subject: smdpp: proper headers for native code ...................................................................... Patch Set 7: (1 comment) Patchset: PS7: merging, as discussed yesterday with Harald and Philipp and today in chat with Eric -- To view, visit https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41122?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: comment Gerrit-Project: osmo-ttcn3-hacks Gerrit-Branch: master Gerrit-Change-Id: I9dcb3249fc712e2ddb2c7cf48d4fc54425848bc6 Gerrit-Change-Number: 41122 Gerrit-PatchSet: 7 Gerrit-Owner: Hoernchen <ewild(a)sysmocom.de> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: dexter <pmaier(a)sysmocom.de> Gerrit-Reviewer: laforge <laforge(a)osmocom.org> Gerrit-CC: osmith <osmith(a)sysmocom.de> Gerrit-Attention: Hoernchen <ewild(a)sysmocom.de> Gerrit-Comment-Date: Wed, 08 Oct 2025 07:20:31 +0000 Gerrit-HasComments: Yes Gerrit-Has-Labels: No
1 0
0 0
[XS] Change in libosmo-sigtran[master]: xua_msg_copy(): add missing check if xua_msg_alloc() fails
by lynxis lazus 07 Oct '25

07 Oct '25
lynxis lazus has uploaded this change for review. ( https://gerrit.osmocom.org/c/libosmo-sigtran/+/41205?usp=email ) Change subject: xua_msg_copy(): add missing check if xua_msg_alloc() fails ...................................................................... xua_msg_copy(): add missing check if xua_msg_alloc() fails Change-Id: I7f2e098dcf7790862eb509357f8d214c87212381 --- M src/xua_msg.c 1 file changed, 2 insertions(+), 0 deletions(-) git pull ssh://gerrit.osmocom.org:29418/libosmo-sigtran refs/changes/05/41205/1 diff --git a/src/xua_msg.c b/src/xua_msg.c index 0652277..1761fee 100644 --- a/src/xua_msg.c +++ b/src/xua_msg.c @@ -62,6 +62,8 @@ { struct xua_msg_part *part; struct xua_msg *xua_cpy = xua_msg_alloc(); + if (!xua_cpy) + return NULL; xua_cpy->hdr = xua_in->hdr; xua_cpy->mtp = xua_in->mtp; -- To view, visit https://gerrit.osmocom.org/c/libosmo-sigtran/+/41205?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: newchange Gerrit-Project: libosmo-sigtran Gerrit-Branch: master Gerrit-Change-Id: I7f2e098dcf7790862eb509357f8d214c87212381 Gerrit-Change-Number: 41205 Gerrit-PatchSet: 1 Gerrit-Owner: lynxis lazus <lynxis(a)fe80.eu>
1 0
0 0
[M] Change in osmo-ttcn3-hacks[master]: MME-Tests: Add test for emergency bearer setup
by jolly 06 Oct '25

06 Oct '25
Attention is currently required from: laforge. jolly has posted comments on this change by jolly. ( https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41189?usp=email ) Change subject: MME-Tests: Add test for emergency bearer setup ...................................................................... Patch Set 3: (1 comment) File mme/ConnHdlr.ttcn: https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41189/comment/b1cb2a4b_b930… : PS2, Line 397: char2oct(apn) > wouldn't it make sense to define "var octetstring apn := O'00000004' & char2oct("test") & O'00000003 […] I use f_enc_dns_hostname("test.sos") now. The result is already an octetstring. -- To view, visit https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/41189?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: comment Gerrit-Project: osmo-ttcn3-hacks Gerrit-Branch: master Gerrit-Change-Id: I2a81defc96bcebdf1a4ec765278ca502800cbe46 Gerrit-Change-Number: 41189 Gerrit-PatchSet: 3 Gerrit-Owner: jolly <andreas(a)eversberg.eu> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: fixeria <vyanitskiy(a)sysmocom.de> Gerrit-Reviewer: laforge <laforge(a)osmocom.org> Gerrit-Attention: laforge <laforge(a)osmocom.org> Gerrit-Comment-Date: Mon, 06 Oct 2025 08:23:16 +0000 Gerrit-HasComments: Yes Gerrit-Has-Labels: No Comment-In-Reply-To: laforge <laforge(a)osmocom.org>
1 0
0 0
[XS] Change in ...osmo-s1gw[master]: README.md, doc/osmo-s1gw-cli.md: fix broken links
by fixeria 06 Oct '25

06 Oct '25
fixeria has submitted this change. ( https://gerrit.osmocom.org/c/erlang/osmo-s1gw/+/41171?usp=email ) Change subject: README.md, doc/osmo-s1gw-cli.md: fix broken links ...................................................................... README.md, doc/osmo-s1gw-cli.md: fix broken links Change-Id: I2f035c6baf5ec19784bda915b802f28fd1063043 --- M README.md M doc/osmo-s1gw-cli.md 2 files changed, 2 insertions(+), 2 deletions(-) Approvals: Jenkins Builder: Verified osmith: Looks good to me, approved diff --git a/README.md b/README.md index a593ac0..5ff4589 100644 --- a/README.md +++ b/README.md @@ -190,4 +190,4 @@ OsmoS1GW comes with [`osmo-s1gw-cli.py`](contrib/osmo-s1gw-cli.py) - an interactive shell based on Python's **cmd2** library. This script serves as an alternative to the traditional telnet/VTY interface found in many Osmocom projects. For more details, -see (doc/osmo-s1gw-cli.md). +see the respective [documentation](doc/osmo-s1gw-cli.md). diff --git a/doc/osmo-s1gw-cli.md b/doc/osmo-s1gw-cli.md index b801b25..db29852 100644 --- a/doc/osmo-s1gw-cli.md +++ b/doc/osmo-s1gw-cli.md @@ -1,7 +1,7 @@ Interactive CLI =============== -[`osmo-s1gw-cli.py`](contrib/osmo-s1gw-cli.py) is an interactive shell based on +[`osmo-s1gw-cli.py`](../contrib/osmo-s1gw-cli.py) is an interactive shell based on Python's **cmd2** library. This script serves as an alternative to the traditional telnet/VTY interface found in many Osmocom projects. It communicates with the main process via the **REST interface**, allowing users to inspect and interact with -- To view, visit https://gerrit.osmocom.org/c/erlang/osmo-s1gw/+/41171?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: merged Gerrit-Project: erlang/osmo-s1gw Gerrit-Branch: master Gerrit-Change-Id: I2f035c6baf5ec19784bda915b802f28fd1063043 Gerrit-Change-Number: 41171 Gerrit-PatchSet: 5 Gerrit-Owner: fixeria <vyanitskiy(a)sysmocom.de> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: fixeria <vyanitskiy(a)sysmocom.de> Gerrit-Reviewer: jolly <andreas(a)eversberg.eu> Gerrit-Reviewer: osmith <osmith(a)sysmocom.de>
1 0
0 0
  • ← Newer
  • 1
  • ...
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • ...
  • 64
  • Older →

HyperKitty Powered by HyperKitty version 1.3.12.