gerrit-log
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
October 2025
- 1 participants
- 631 discussions
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
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
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
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
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
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
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
[XS] Change in libosmo-sigtran[master]: xua_msg_copy(): add missing check if xua_msg_alloc() fails
by lynxis lazus 07 Oct '25
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
[M] Change in osmo-ttcn3-hacks[master]: MME-Tests: Add test for emergency bearer setup
by jolly 06 Oct '25
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
[XS] Change in ...osmo-s1gw[master]: README.md, doc/osmo-s1gw-cli.md: fix broken links
by fixeria 06 Oct '25
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