From 9ff35c651fb278d376d2a63aa8200029582243e9 Mon Sep 17 00:00:00 2001 From: Eric Wild Date: Thu, 22 May 2025 16:27:14 +0200 Subject: [PATCH] c++ bpp verification code with pybind11 needs ext building: $ python3 setup.py build_ext --inplace --- .gitignore | 3 + bsp_python_bindings.cpp | 1076 +++++++++++++++++ bsp_test_integration.py | 188 +++ osmo-smdpp.py | 354 +++++- pySim/esim/bsp.py | 27 + pySim/esim/es8p.py | 4 + pyproject.toml | 2 +- requirements.txt | 5 + setup.py | 20 + smdpp-data/certs/DPauth/data_sig.der | 1 - smdpp-data/certs/DPtls/CERT_S_SM_DP2_TLS.der | Bin 647 -> 647 bytes smdpp-data/certs/DPtls/CERT_S_SM_DP4_TLS.der | Bin 647 -> 647 bytes smdpp-data/certs/DPtls/CERT_S_SM_DP8_TLS.der | Bin 647 -> 647 bytes .../certs/DPtls/CERT_S_SM_DP_TLS_BRP.der | Bin 646 -> 646 bytes .../certs/DPtls/CERT_S_SM_DP_TLS_NIST.der | Bin 647 -> 647 bytes .../certs/DPtls/CERT_S_SM_DP_TLS_NIST.pem | 16 - .../Expired 2021/CERT_S_SM_DP2_TLS.der | Bin 646 -> 0 bytes .../Expired 2021/CERT_S_SM_DP4_TLS.der | Bin 647 -> 0 bytes .../Expired 2021/CERT_S_SM_DP8_TLS.der | Bin 647 -> 0 bytes .../Expired 2021/CERT_S_SM_DP_TLS_BRP.der | Bin 646 -> 0 bytes .../Expired 2021/CERT_S_SM_DP_TLS_NIST.der | Bin 647 -> 0 bytes .../Expired 2022/CERT_S_SM_DP2_TLS.der | Bin 647 -> 0 bytes .../Expired 2022/CERT_S_SM_DP4_TLS.der | Bin 646 -> 0 bytes .../Expired 2022/CERT_S_SM_DP8_TLS.der | Bin 646 -> 0 bytes .../Expired 2022/CERT_S_SM_DP_TLS_BRP.der | Bin 646 -> 0 bytes .../Expired 2022/CERT_S_SM_DP_TLS_NIST.der | Bin 645 -> 0 bytes .../Expired 2023/CERT_S_SM_DP2_TLS.der | Bin 646 -> 0 bytes .../Expired 2023/CERT_S_SM_DP4_TLS.der | Bin 647 -> 0 bytes .../Expired 2023/CERT_S_SM_DP8_TLS.der | Bin 648 -> 0 bytes .../Expired 2023/CERT_S_SM_DP_TLS_BRP.der | Bin 648 -> 0 bytes .../Expired 2023/CERT_S_SM_DP_TLS_NIST.der | Bin 646 -> 0 bytes 31 files changed, 1626 insertions(+), 70 deletions(-) create mode 100644 bsp_python_bindings.cpp create mode 100644 bsp_test_integration.py delete mode 100644 smdpp-data/certs/DPauth/data_sig.der delete mode 100644 smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_NIST.pem delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP2_TLS.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP4_TLS.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP8_TLS.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP_TLS_BRP.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP_TLS_NIST.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP2_TLS.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP4_TLS.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP8_TLS.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP_TLS_BRP.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP_TLS_NIST.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP2_TLS.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP4_TLS.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP8_TLS.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_BRP.der delete mode 100644 smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_NIST.der diff --git a/.gitignore b/.gitignore index e9fa1d30..d7536f62 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ /smdpp-data/sm-dp-sessions dist tags +*.so +dhparam2048.pem +smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_NIST.pem diff --git a/bsp_python_bindings.cpp b/bsp_python_bindings.cpp new file mode 100644 index 00000000..8ea2a9e4 --- /dev/null +++ b/bsp_python_bindings.cpp @@ -0,0 +1,1076 @@ +/* + * (C) 2025 by sysmocom s.f.m.c. GmbH + * All Rights Reserved + * + * Author: Eric Wild + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +// Python bindings for BSP crypto using pybind11 +// Compile with: c++ -O3 -Wall -shared -std=c++17 -fPIC `python3 -m pybind11 --includes` bsp_python_bindings.cpp -o bsp_crypto`python3-config --extension-suffix` $(pkg-config --cflags --libs openssl) + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +} + +//straightforward way is to do it properly with openssl instead of manually messing with the DER +typedef struct ReplaceSessionKeysRequest_ASN1_st { + ASN1_OCTET_STRING* initialMacChainingValue; + ASN1_OCTET_STRING* ppkEnc; + ASN1_OCTET_STRING* ppkCmac; +} ReplaceSessionKeysRequest_ASN1; +DECLARE_ASN1_FUNCTIONS(ReplaceSessionKeysRequest_ASN1) + +ASN1_SEQUENCE(ReplaceSessionKeysRequest_ASN1) = { + ASN1_IMP(ReplaceSessionKeysRequest_ASN1, initialMacChainingValue, ASN1_OCTET_STRING, 0), + ASN1_IMP(ReplaceSessionKeysRequest_ASN1, ppkEnc, ASN1_OCTET_STRING, 1), + ASN1_IMP(ReplaceSessionKeysRequest_ASN1, ppkCmac, ASN1_OCTET_STRING, 2) +} ASN1_SEQUENCE_END(ReplaceSessionKeysRequest_ASN1) +IMPLEMENT_ASN1_FUNCTIONS(ReplaceSessionKeysRequest_ASN1) + +typedef ReplaceSessionKeysRequest_ASN1 ReplaceSessionKeysRequest_outer; +DECLARE_ASN1_FUNCTIONS(ReplaceSessionKeysRequest_outer) + +ASN1_ITEM_TEMPLATE(ReplaceSessionKeysRequest_outer) = + ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_IMPTAG | ASN1_TFLG_CONTEXT, 38, ReplaceSessionKeysRequest_outer, ReplaceSessionKeysRequest_ASN1) + ASN1_ITEM_TEMPLATE_END(ReplaceSessionKeysRequest_outer) +IMPLEMENT_ASN1_FUNCTIONS(ReplaceSessionKeysRequest_outer) + +// Define stack macros for ASN1_OCTET_STRING +DEFINE_STACK_OF(ASN1_OCTET_STRING) + +// RemoteOpId ::= [2] INTEGER +// This is a tagged INTEGER type, not just an INTEGER with a tag +typedef ASN1_INTEGER RemoteOpId; +DECLARE_ASN1_FUNCTIONS(RemoteOpId) + +ASN1_ITEM_TEMPLATE(RemoteOpId) = + ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_IMPTAG | ASN1_TFLG_CONTEXT, 2, RemoteOpId, ASN1_INTEGER) +ASN1_ITEM_TEMPLATE_END(RemoteOpId) + +IMPLEMENT_ASN1_FUNCTIONS(RemoteOpId) + +// ControlRefTemplate +typedef struct ControlRefTemplate_st { + ASN1_OCTET_STRING* keyType; + ASN1_OCTET_STRING* keyLen; + ASN1_OCTET_STRING* hostId; +} ControlRefTemplate; + +DECLARE_ASN1_FUNCTIONS(ControlRefTemplate) + +ASN1_SEQUENCE(ControlRefTemplate) = { + ASN1_IMP(ControlRefTemplate, keyType, ASN1_OCTET_STRING, 0), + ASN1_IMP(ControlRefTemplate, keyLen, ASN1_OCTET_STRING, 1), + ASN1_IMP(ControlRefTemplate, hostId, ASN1_OCTET_STRING, 4) +} ASN1_SEQUENCE_END(ControlRefTemplate) + +IMPLEMENT_ASN1_FUNCTIONS(ControlRefTemplate) + +// Custom types for APPLICATION tags > 30 +typedef ASN1_OCTET_STRING SmdpOtpk; +DECLARE_ASN1_FUNCTIONS(SmdpOtpk) + +ASN1_ITEM_TEMPLATE(SmdpOtpk) = + ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_IMPTAG | ASN1_TFLG_APPLICATION, 73, SmdpOtpk, ASN1_OCTET_STRING) +ASN1_ITEM_TEMPLATE_END(SmdpOtpk) + +IMPLEMENT_ASN1_FUNCTIONS(SmdpOtpk) + +typedef ASN1_OCTET_STRING SmdpSign; +DECLARE_ASN1_FUNCTIONS(SmdpSign) + +ASN1_ITEM_TEMPLATE(SmdpSign) = + ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_IMPTAG | ASN1_TFLG_APPLICATION, 55, SmdpSign, ASN1_OCTET_STRING) +ASN1_ITEM_TEMPLATE_END(SmdpSign) + +IMPLEMENT_ASN1_FUNCTIONS(SmdpSign) + +// InitialiseSecureChannelRequest - base structure +typedef struct InitialiseSecureChannelRequest_ASN1_st { + RemoteOpId* remoteOpId; + ASN1_OCTET_STRING* transactionId; + ControlRefTemplate* controlRefTemplate; + SmdpOtpk* smdpOtpk; + SmdpSign* smdpSign; +} InitialiseSecureChannelRequest_ASN1; + +DECLARE_ASN1_FUNCTIONS(InitialiseSecureChannelRequest_ASN1) + +ASN1_SEQUENCE(InitialiseSecureChannelRequest_ASN1) = { + ASN1_SIMPLE(InitialiseSecureChannelRequest_ASN1, remoteOpId, RemoteOpId), + ASN1_IMP(InitialiseSecureChannelRequest_ASN1, transactionId, ASN1_OCTET_STRING, 0), + ASN1_IMP(InitialiseSecureChannelRequest_ASN1, controlRefTemplate, ControlRefTemplate, 6), + ASN1_SIMPLE(InitialiseSecureChannelRequest_ASN1, smdpOtpk, SmdpOtpk), + ASN1_SIMPLE(InitialiseSecureChannelRequest_ASN1, smdpSign, SmdpSign) +} ASN1_SEQUENCE_END(InitialiseSecureChannelRequest_ASN1) + +IMPLEMENT_ASN1_FUNCTIONS(InitialiseSecureChannelRequest_ASN1) + +// InitialiseSecureChannelRequest with tag [35] +typedef InitialiseSecureChannelRequest_ASN1 InitialiseSecureChannelRequest_tagged; +DECLARE_ASN1_FUNCTIONS(InitialiseSecureChannelRequest_tagged) + +ASN1_ITEM_TEMPLATE(InitialiseSecureChannelRequest_tagged) = + ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_IMPTAG | ASN1_TFLG_CONTEXT, 35, InitialiseSecureChannelRequest_tagged, InitialiseSecureChannelRequest_ASN1) +ASN1_ITEM_TEMPLATE_END(InitialiseSecureChannelRequest_tagged) + +IMPLEMENT_ASN1_FUNCTIONS(InitialiseSecureChannelRequest_tagged) + +// Define the tagged OCTET STRING types +typedef ASN1_OCTET_STRING ASN1_OCTET_STRING_TAG7; +typedef ASN1_OCTET_STRING ASN1_OCTET_STRING_TAG8; +typedef ASN1_OCTET_STRING ASN1_OCTET_STRING_TAG6; + +DECLARE_ASN1_ITEM(ASN1_OCTET_STRING_TAG7) +DECLARE_ASN1_ITEM(ASN1_OCTET_STRING_TAG8) +DECLARE_ASN1_ITEM(ASN1_OCTET_STRING_TAG6) + +ASN1_ITEM_TEMPLATE(ASN1_OCTET_STRING_TAG7) = + ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_IMPTAG | ASN1_TFLG_CONTEXT, 7, ASN1_OCTET_STRING_TAG7, ASN1_OCTET_STRING) +ASN1_ITEM_TEMPLATE_END(ASN1_OCTET_STRING_TAG7) + +ASN1_ITEM_TEMPLATE(ASN1_OCTET_STRING_TAG8) = + ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_IMPTAG | ASN1_TFLG_CONTEXT, 8, ASN1_OCTET_STRING_TAG8, ASN1_OCTET_STRING) +ASN1_ITEM_TEMPLATE_END(ASN1_OCTET_STRING_TAG8) + +ASN1_ITEM_TEMPLATE(ASN1_OCTET_STRING_TAG6) = + ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_IMPTAG | ASN1_TFLG_CONTEXT, 6, ASN1_OCTET_STRING_TAG6, ASN1_OCTET_STRING) +ASN1_ITEM_TEMPLATE_END(ASN1_OCTET_STRING_TAG6) + +DECLARE_ASN1_FUNCTIONS(ASN1_OCTET_STRING_TAG7) +DECLARE_ASN1_FUNCTIONS(ASN1_OCTET_STRING_TAG8) +DECLARE_ASN1_FUNCTIONS(ASN1_OCTET_STRING_TAG6) + +IMPLEMENT_ASN1_FUNCTIONS(ASN1_OCTET_STRING_TAG7) +IMPLEMENT_ASN1_FUNCTIONS(ASN1_OCTET_STRING_TAG8) +IMPLEMENT_ASN1_FUNCTIONS(ASN1_OCTET_STRING_TAG6) + +// BoundProfilePackage +typedef struct BoundProfilePackage_st { + InitialiseSecureChannelRequest_tagged* initialiseSecureChannelRequest; + STACK_OF(ASN1_OCTET_STRING)* firstSequenceOf87; + STACK_OF(ASN1_OCTET_STRING)* sequenceOf88; + STACK_OF(ASN1_OCTET_STRING)* secondSequenceOf87; + STACK_OF(ASN1_OCTET_STRING)* sequenceOf86; +} BoundProfilePackage; + +DECLARE_ASN1_FUNCTIONS(BoundProfilePackage) + +// Define the base sequence without the tag +ASN1_SEQUENCE(BoundProfilePackage) = { + ASN1_SIMPLE(BoundProfilePackage, initialiseSecureChannelRequest, InitialiseSecureChannelRequest_tagged), + ASN1_IMP_SEQUENCE_OF(BoundProfilePackage, firstSequenceOf87, ASN1_OCTET_STRING_TAG7, 0), + ASN1_IMP_SEQUENCE_OF(BoundProfilePackage, sequenceOf88, ASN1_OCTET_STRING_TAG8, 1), + ASN1_IMP_SEQUENCE_OF_OPT(BoundProfilePackage, secondSequenceOf87, ASN1_OCTET_STRING_TAG7, 2), + ASN1_IMP_SEQUENCE_OF(BoundProfilePackage, sequenceOf86, ASN1_OCTET_STRING_TAG6, 3) +} ASN1_SEQUENCE_END(BoundProfilePackage) + +IMPLEMENT_ASN1_FUNCTIONS(BoundProfilePackage) + +// For BoundProfilePackage ::= [54] SEQUENCE +// We need a version that's implicitly tagged as [54], replacing the SEQUENCE tag +typedef BoundProfilePackage BoundProfilePackage_tagged; +DECLARE_ASN1_FUNCTIONS(BoundProfilePackage_tagged) + +// This creates a type where [54] replaces the SEQUENCE tag +ASN1_ITEM_TEMPLATE(BoundProfilePackage_tagged) = + ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_IMPTAG | ASN1_TFLG_CONTEXT,54, BoundProfilePackage_tagged, BoundProfilePackage) +ASN1_ITEM_TEMPLATE_END(BoundProfilePackage_tagged) + +IMPLEMENT_ASN1_FUNCTIONS(BoundProfilePackage_tagged) + +// reusing a dirty pointer fails +#define ossl_free_reset(add) { \ + OPENSSL_free(add); \ + add = 0; \ +} + +// Custom deleters wrapped in structs (since free functions might be macros) +struct BoundProfilePackage_tagged_Deleter { + void operator()(BoundProfilePackage_tagged *ss1) const + { + BoundProfilePackage_tagged_free(ss1); + } +}; + +struct ReplaceSessionKeysRequest_outer_Deleter { + void operator()(ReplaceSessionKeysRequest_outer *ss1) const + { + ReplaceSessionKeysRequest_outer_free(ss1); + } +}; + +struct EVP_CIPHER_CTX_Deleter { + void operator()(EVP_CIPHER_CTX *ctx) const + { + if (ctx) EVP_CIPHER_CTX_free(ctx); + } +}; + +struct EVP_MAC_CTX_Deleter { + void operator()(EVP_MAC_CTX *ctx) const + { + if (ctx) EVP_MAC_CTX_free(ctx); + } +}; + +struct EVP_MAC_Deleter { + void operator()(EVP_MAC *mac) const + { + if (mac) EVP_MAC_free(mac); + } +}; + +using BPP_ptr = std::unique_ptr; +using RPK_ptr = std::unique_ptr; +using EVP_CIPHER_CTX_unique_ptr = std::unique_ptr; +using EVP_MAC_CTX_unique_ptr = std::unique_ptr; +using EVP_MAC_unique_ptr = std::unique_ptr; + +class BspCrypto { +private: + static const size_t MAX_SEGMENT_SIZE = 1020; + static const size_t MAC_LENGTH = 8; +// static const size_t AES_BLOCK_SIZE = 16; + static const size_t AES_KEY_SIZE = 16; + + std::vector s_enc; + std::vector s_mac; + std::vector mac_chain; + uint32_t block_number; + + static std::vector compute_cmac(const std::vector& key, + const std::vector& data, + size_t output_size = AES_BLOCK_SIZE) { + EVP_MAC *mac = EVP_MAC_fetch(nullptr, "CMAC", nullptr); + if (!mac) throw std::runtime_error("Failed to fetch CMAC implementation"); + + EVP_MAC_CTX_unique_ptr mac_ctx(EVP_MAC_CTX_new(mac)); + EVP_MAC_free(mac); + if (!mac_ctx) throw std::runtime_error("Failed to create MAC context"); + + // Set up parameters for CMAC with AES-128-CBC + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_CIPHER, (char*)"AES-128-CBC", 0), + OSSL_PARAM_construct_end() + }; + + if (EVP_MAC_init(mac_ctx.get(), key.data(), key.size(), params) != 1) { + throw std::runtime_error("Failed to init MAC"); + } + + if (EVP_MAC_update(mac_ctx.get(), data.data(), data.size()) != 1) { + throw std::runtime_error("Failed to update MAC"); + } + + std::vector output(output_size); + size_t mac_len = output_size; + if (EVP_MAC_final(mac_ctx.get(), output.data(), &mac_len, output.size()) != 1) { + throw std::runtime_error("Failed to finalize MAC"); + } + + output.resize(mac_len); + return output; + } + + // Generic AES-128-CBC cipher operation helper + std::vector aes_cipher_operation(const std::vector& input, + const std::vector& key, + const std::vector& iv, + bool encrypt) { + if (key.size() != AES_KEY_SIZE) { + throw std::runtime_error("Invalid key size"); + } + if (iv.size() != AES_BLOCK_SIZE) { + throw std::runtime_error("Invalid IV size"); + } + + EVP_CIPHER_CTX_unique_ptr ctx(EVP_CIPHER_CTX_new()); + if (!ctx) throw std::runtime_error("Failed to create cipher context"); + + const EVP_CIPHER* cipher = EVP_aes_128_cbc(); + + int result; + if (encrypt) { + result = EVP_EncryptInit_ex(ctx.get(), cipher, nullptr, key.data(), iv.data()); + } else { + result = EVP_DecryptInit_ex(ctx.get(), cipher, nullptr, key.data(), iv.data()); + } + + if (result != 1) { + throw std::runtime_error(encrypt ? "Failed to init AES encryption" : "Failed to init AES decryption"); + } + + // Disable padding since we handle custom padding ourselves + if (EVP_CIPHER_CTX_set_padding(ctx.get(), 0) != 1) { + throw std::runtime_error("Failed to disable padding"); + } + + // Allocate output buffer (may need extra block for padding) + std::vector output(input.size() + AES_BLOCK_SIZE); + int len = 0; + + if (encrypt) { + result = EVP_EncryptUpdate(ctx.get(), output.data(), &len, input.data(), input.size()); + } else { + result = EVP_DecryptUpdate(ctx.get(), output.data(), &len, input.data(), input.size()); + } + + if (result != 1) { + throw std::runtime_error(encrypt ? "Failed to encrypt data" : "Failed to decrypt data"); + } + + // Verify output length matches input (since padding is disabled) + if (len != static_cast(input.size())) { + throw std::runtime_error("Unexpected " + std::string(encrypt ? "encryption" : "decryption") + + " output length: " + std::to_string(len) + + " expected: " + std::to_string(input.size())); + } + + int final_len = 0; + if (encrypt) { + result = EVP_EncryptFinal_ex(ctx.get(), output.data() + len, &final_len); + } else { + result = EVP_DecryptFinal_ex(ctx.get(), output.data() + len, &final_len); + } + + if (result != 1) { + throw std::runtime_error(encrypt ? "Failed to finalize encryption" : "Failed to finalize decryption"); + } + + // With disabled padding, final_len should be 0 + if (final_len != 0) { + throw std::runtime_error("Unexpected final " + std::string(encrypt ? "encryption" : "decryption") + + " length: " + std::to_string(final_len)); + } + + output.resize(len); + return output; + } + + // BERTLV length encoding + static std::vector encode_bertlv_length(size_t length) { + if (length < 0x80) { + return {static_cast(length)}; + } else if (length < 0x100) { + return {0x81, static_cast(length)}; + } else if (length < 0x10000) { + return {0x82, static_cast(length >> 8), static_cast(length & 0xFF)}; + } else { + throw std::runtime_error("Length too large for BERTLV encoding"); + } + } + + static void print_hex(const std::string& label, const std::vector& data) { + std::cout << label << ": "; + for (uint8_t b : data) { + std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast(b); + } + std::cout << std::dec << std::endl; + } + + static void print_hex(const char *label, const unsigned char *data, int len) { + const std::vector t = std::vector((uint8_t*)data, (uint8_t*)data+len); + const std::string l = std::string("").assign(label); + print_hex(l, t); + } + + static std::pair parse_bertlv_length(const std::vector& data, size_t offset) { + if (offset >= data.size()) throw std::runtime_error("Invalid length offset"); + + uint8_t first_byte = data[offset]; + if (first_byte < 0x80) { + return {first_byte, offset + 1}; + } else if (first_byte == 0x81) { + if (offset + 1 >= data.size()) throw std::runtime_error("Invalid length encoding"); + return {data[offset + 1], offset + 2}; + } else if (first_byte == 0x82) { + if (offset + 2 >= data.size()) throw std::runtime_error("Invalid length encoding"); + size_t length = (data[offset + 1] << 8) | data[offset + 2]; + return {length, offset + 3}; + } else { + throw std::runtime_error("Unsupported length encoding"); + } + } + +public: + BspCrypto(const std::vector& s_enc_key, + const std::vector& s_mac_key, + const std::vector& initial_mcv) + : s_enc(s_enc_key), s_mac(s_mac_key), mac_chain(initial_mcv), block_number(1) { + assert(s_enc.size() == AES_KEY_SIZE); + assert(s_mac.size() == AES_KEY_SIZE); + assert(mac_chain.size() == AES_BLOCK_SIZE); + } + + // X9.63 KDF + static std::vector x963_kdf_sha256(const std::vector& shared_secret, + const std::vector& shared_info, + size_t output_length) { + std::vector output; + output.reserve(output_length); + + const size_t hash_length = 32; // SHA256 + uint32_t counter = 1; + + while (output.size() < output_length) { + // input: shared_secret || counter || shared_info + std::vector hash_input; + hash_input.insert(hash_input.end(), shared_secret.begin(), shared_secret.end()); + + uint32_t counter_be = htonl(counter); + const uint8_t* counter_bytes = reinterpret_cast(&counter_be); + hash_input.insert(hash_input.end(), counter_bytes, counter_bytes + 4); + + hash_input.insert(hash_input.end(), shared_info.begin(), shared_info.end()); + + std::vector hash_output(hash_length); + if (SHA256(hash_input.data(), hash_input.size(), hash_output.data()) == nullptr) { + throw std::runtime_error("SHA256 computation failed"); + } + + size_t bytes_needed = std::min(hash_length, output_length - output.size()); + output.insert(output.end(), hash_output.begin(), hash_output.begin() + bytes_needed); + + counter++; + } + + // Truncate to desired length + output.resize(output_length); + return output; + } + + static BspCrypto from_kdf(const std::vector& shared_secret, + uint8_t key_type, uint8_t key_length, + const std::vector& host_id, + const std::vector& eid) { + + // shared_info: key_type || key_length || len(host_id) || host_id || len(eid) || eid + std::vector shared_info; + shared_info.push_back(key_type); + shared_info.push_back(key_length); + + if (host_id.size() > 255) throw std::runtime_error("Host ID too long"); + shared_info.push_back(static_cast(host_id.size())); + shared_info.insert(shared_info.end(), host_id.begin(), host_id.end()); + + if (eid.size() > 255) throw std::runtime_error("EID too long"); + shared_info.push_back(static_cast(eid.size())); + shared_info.insert(shared_info.end(), eid.begin(), eid.end()); + + // Derive 48 bytes: 16 for initial_mcv + 16 for s_enc + 16 for s_mac + auto kdf_output = x963_kdf_sha256(shared_secret, shared_info, 48); + + std::vector initial_mcv(kdf_output.begin(), kdf_output.begin() + 16); + std::vector s_enc(kdf_output.begin() + 16, kdf_output.begin() + 32); + std::vector s_mac(kdf_output.begin() + 32, kdf_output.begin() + 48); + + return BspCrypto(s_enc, s_mac, initial_mcv); + } + + std::vector generate_icv() { + // Use generate_icv_for_block and increment block_number + auto icv = generate_icv_for_block(block_number); + block_number++; + return icv; + } + + // Custom padding: 0x80 followed by 0x00s to 16-byte boundary + std::vector add_padding(const std::vector& data) { + std::vector padded = data; + padded.push_back(0x80); + + while (padded.size() % AES_BLOCK_SIZE != 0) { + padded.push_back(0x00); + } + + return padded; + } + + // Remove custom padding + std::vector remove_padding(const std::vector &data) { + if (data.empty()) + return data; + + // Remove trailing zeros first + int last_nonzero = data.size() - 1; + while (last_nonzero >= 0 && data[last_nonzero] == 0x00) { + last_nonzero--; + } + if (last_nonzero >= 0 && data[last_nonzero] == 0x80) { + return std::vector(data.begin(), data.begin() + last_nonzero); + } + return data; + + } + + // AES-CBC encryption + std::vector aes_encrypt(const std::vector& plaintext) { + auto padded = add_padding(plaintext); + auto icv = generate_icv(); + return aes_cipher_operation(padded, s_enc, icv, true); + } + + std::vector aes_decrypt_with_icv(const std::vector& ciphertext, const std::vector& icv) { + auto decrypted = aes_cipher_operation(ciphertext, s_enc, icv, false); + return remove_padding(decrypted); + } + + std::vector generate_icv_for_block(uint32_t block_num) { + std::vector block_data(AES_BLOCK_SIZE, 0); + + uint32_t block_num_be = htonl(block_num); + memcpy(&block_data[12], &block_num_be, 4); + + std::vector zero_iv(AES_BLOCK_SIZE, 0); + return aes_cipher_operation(block_data, s_enc, zero_iv, true); + } + + // Private helper for verify_and_decrypt operations + std::vector verify_and_decrypt_helper(const std::vector& segment, + const std::vector& mac_chain_to_use, + bool decrypt) { + if (segment.size() < 3) throw std::runtime_error("Segment too small"); + + uint8_t tag = segment[0]; + auto [length, length_end] = parse_bertlv_length(segment, 1); + + if (length_end + length > segment.size()) { + throw std::runtime_error("Invalid segment length"); + } + + if (length <= MAC_LENGTH) { + throw std::runtime_error("Invalid segment length: payload too small"); + } + + size_t payload_length = length - MAC_LENGTH; + std::vector payload(segment.begin() + length_end, segment.begin() + length_end + payload_length); + std::vector received_mac(segment.begin() + length_end + payload_length, segment.begin() + length_end + length); + + size_t lcc = payload.size() + MAC_LENGTH; + std::vector temp_data; + temp_data.insert(temp_data.end(), mac_chain_to_use.begin(), mac_chain_to_use.end()); + temp_data.push_back(tag); + + auto length_bytes = encode_bertlv_length(lcc); + temp_data.insert(temp_data.end(), length_bytes.begin(), length_bytes.end()); + temp_data.insert(temp_data.end(), payload.begin(), payload.end()); + + std::vector computed_full_mac = compute_cmac(s_mac, temp_data); + std::vector computed_mac(computed_full_mac.begin(), computed_full_mac.begin() + MAC_LENGTH); + + if (received_mac != computed_mac) { + throw std::runtime_error("MAC verification failed"); + } + + // Update instance state + mac_chain = computed_full_mac; + + if (decrypt) { + // generate_icv() increments block_number + auto icv = generate_icv(); + return aes_decrypt_with_icv(payload, icv); + } else { + // MAC-only: return payload as-is and increment block counter + block_number++; + return payload; + } + } + + std::vector compute_mac(uint8_t tag, const std::vector& data) { + size_t lcc = data.size() + MAC_LENGTH; + + // temp_data: mac_chain + tag + length + data + std::vector temp_data; + temp_data.insert(temp_data.end(), mac_chain.begin(), mac_chain.end()); + temp_data.push_back(tag); + + auto length_bytes = encode_bertlv_length(lcc); + temp_data.insert(temp_data.end(), length_bytes.begin(), length_bytes.end()); + temp_data.insert(temp_data.end(), data.begin(), data.end()); + + // Compute CMAC + std::vector full_mac = compute_cmac(s_mac, temp_data); + + mac_chain = full_mac; + + // truncated MAC (first 8 bytes) + return std::vector(full_mac.begin(), full_mac.begin() + MAC_LENGTH); + } + + std::vector encrypt_and_mac_one(uint8_t tag, const std::vector& plaintext) { + if (plaintext.size() > MAX_SEGMENT_SIZE - 10) { // Account for tag, length, MAC + throw std::runtime_error("Segment too large"); + } + + auto ciphertext = aes_encrypt(plaintext); + auto mac = compute_mac(tag, ciphertext); + + // final result: tag + length + ciphertext + mac + std::vector result; + result.push_back(tag); + + auto length_bytes = encode_bertlv_length(ciphertext.size() + MAC_LENGTH); + result.insert(result.end(), length_bytes.begin(), length_bytes.end()); + result.insert(result.end(), ciphertext.begin(), ciphertext.end()); + result.insert(result.end(), mac.begin(), mac.end()); + + return result; + } + + std::vector decrypt_and_verify(const std::vector& segments, bool decrypt = true) { + return verify_and_decrypt_helper(segments, mac_chain, decrypt); + } + + struct ReplaceSessionKeysRequest { + std::vector ppkEnc; // PPK-ENC (16 bytes) + std::vector ppkCmac; // PPK-MAC (16 bytes) + std::vector initialMacChainingValue; // Initial MCV for PPK (16 bytes) + }; + + static std::vector asn1_octet_string_to_vector(const ASN1_OCTET_STRING* asn1_str) { + if (!asn1_str || !asn1_str->data || asn1_str->length <= 0) { + throw std::runtime_error("Invalid ASN1_OCTET_STRING"); + } + + return std::vector(asn1_str->data, asn1_str->data + asn1_str->length); + } + + static ReplaceSessionKeysRequest parse_replace_session_keys(const std::vector& data) { + if (data.empty()) { + throw std::runtime_error("Empty ReplaceSessionKeysRequest data"); + } + + const unsigned char *p = data.data(); + + RPK_ptr rsk_asn1(d2i_ReplaceSessionKeysRequest_outer(nullptr, &p, static_cast(data.size()))); + + if (!rsk_asn1) { + unsigned long err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(err, err_buf, sizeof(err_buf)); + + std::string error_msg = "Failed to parse ReplaceSessionKeysRequest ASN.1: "; + error_msg += err_buf; + throw std::runtime_error(error_msg); + } + + try { + if (!rsk_asn1->initialMacChainingValue || !rsk_asn1->ppkEnc || !rsk_asn1->ppkCmac) { + throw std::runtime_error("Missing required fields in ReplaceSessionKeysRequest"); + } + + if (rsk_asn1->initialMacChainingValue->length != 16) { + throw std::runtime_error("Invalid initialMacChainingValue length: expected 16 bytes"); + } + if (rsk_asn1->ppkEnc->length != 16) { + throw std::runtime_error("Invalid ppkEnc length: expected 16 bytes"); + } + if (rsk_asn1->ppkCmac->length != 16) { + throw std::runtime_error("Invalid ppkCmac length: expected 16 bytes"); + } + + ReplaceSessionKeysRequest result; + result.initialMacChainingValue = asn1_octet_string_to_vector(rsk_asn1->initialMacChainingValue); + result.ppkEnc = asn1_octet_string_to_vector(rsk_asn1->ppkEnc); + result.ppkCmac = asn1_octet_string_to_vector(rsk_asn1->ppkCmac); + + return result; + + } catch (...) { + throw; + } + } + + static BspCrypto from_replace_session_keys(const ReplaceSessionKeysRequest& rsk) { + return BspCrypto(rsk.ppkEnc, rsk.ppkCmac, rsk.initialMacChainingValue); + } + struct BppProcessingResult { + std::vector configureIsdp; + std::vector storeMetadata; + ReplaceSessionKeysRequest replaceSessionKeys; + std::vector profileData; + bool hasReplaceSessionKeys; + }; + + BppProcessingResult process_bound_profile_package( + const std::vector& allofit + ) { + BppProcessingResult result; + auto p = allofit.data(); + BPP_ptr bpp(d2i_BoundProfilePackage_tagged(nullptr, &p, allofit.size())); + if (!bpp) { + std::cout << "Failed to decode BoundProfilePackage" << std::endl; + print_hex(" Data", allofit.data(), (allofit.size() > 32) ? 32 : allofit.size()); + return result; + } + + // all mandatory + if (bpp->firstSequenceOf87 == nullptr ||bpp->sequenceOf88 == nullptr || bpp->sequenceOf86 == nullptr ) { + std::cout << "Malformed BoundProfilePackage" << std::endl; + return result; + } + + result.hasReplaceSessionKeys = bpp->secondSequenceOf87 != nullptr; + + + unsigned char *encoded = nullptr; + ASN1_OCTET_STRING *elem = nullptr; + int len = 0; + + // Step 1: Decrypt ConfigureISDP with session keys + std::cout << "Step 1: Decrypting ConfigureISDP with session keys..." << std::endl; + elem = sk_ASN1_OCTET_STRING_value(bpp->firstSequenceOf87, 0); + len = i2d_ASN1_OCTET_STRING_TAG7(elem, &encoded); + result.configureIsdp = decrypt_and_verify({encoded, encoded + len}, true); + ossl_free_reset(encoded); + + // Step 2: Verify StoreMetadata with session keys (MAC-only) + std::cout << "Step 2: Verifying StoreMetadata with session keys (MAC-only)..." << std::endl; + elem = sk_ASN1_OCTET_STRING_value(bpp->sequenceOf88, 0); + len = i2d_ASN1_OCTET_STRING_TAG8(elem, &encoded); + result.storeMetadata = decrypt_and_verify({encoded, encoded + len}, false); + ossl_free_reset(encoded); + + // Step 3: If present, decrypt ReplaceSessionKeys with session keys + if (result.hasReplaceSessionKeys) { + std::cout << "Step 3: Decrypting ReplaceSessionKeys with session keys..." << std::endl; + + elem = sk_ASN1_OCTET_STRING_value(bpp->secondSequenceOf87, 0); + len = i2d_ASN1_OCTET_STRING_TAG7(elem, &encoded); + auto rsk_data = decrypt_and_verify({encoded, encoded + len}, true); + result.replaceSessionKeys = parse_replace_session_keys(rsk_data); + ossl_free_reset(encoded); + + // Step 4: Create NEW BSP instance with PPK and decrypt profile data + std::cout << "Step 4: Creating new BSP instance with PPK keys..." << std::endl; + auto ppk_bsp = from_replace_session_keys(result.replaceSessionKeys); + + print_hex("PPK-ENC", result.replaceSessionKeys.ppkEnc); + print_hex("PPK-MAC", result.replaceSessionKeys.ppkCmac); + print_hex("PPK Initial MCV", result.replaceSessionKeys.initialMacChainingValue); + + std::cout << "Step 5: Decrypting profile data with PPK keys..."<< std::endl; + int num = sk_ASN1_OCTET_STRING_num(bpp->sequenceOf86); + for (int i = 0; i < num; i++) { + elem = sk_ASN1_OCTET_STRING_value(bpp->sequenceOf86, i); + len = i2d_ASN1_OCTET_STRING_TAG6(elem, &encoded); + auto rv = ppk_bsp.decrypt_and_verify({encoded, encoded + len}, true); + result.profileData.insert(result.profileData.end(), rv.begin(), rv.end()); + ossl_free_reset(encoded); + } + std::cout << "Step 5: "<< num << " profile chunks verified and decrypted"<< std::endl; + + } else { + // No ReplaceSessionKeys - decrypt profile data with session keys + std::cout << "Step 3: Decrypting profile data with session keys (no PPK)..." << std::endl; + int num = sk_ASN1_OCTET_STRING_num(bpp->sequenceOf86); + for (int i = 0; i < num; i++) { + elem = sk_ASN1_OCTET_STRING_value(bpp->sequenceOf86, 0); + len = i2d_ASN1_OCTET_STRING_TAG6(elem, &encoded); + auto rv = decrypt_and_verify({encoded, encoded + len}, true); + result.profileData.insert(result.profileData.end(), rv.begin(), rv.end()); + ossl_free_reset(encoded); + } + std::cout << "Step 3: "<< num << " profile chunks verified and decrypted"<< std::endl; + } + + return result; + } + + BppProcessingResult process_bound_profile_package( + const std::vector& firstSequenceOf87, // ConfigureISDP + const std::vector& sequenceOf88, // StoreMetadata + const std::vector& secondSequenceOf87, // ReplaceSessionKeys (optional) + const std::vector& sequenceOf86 // Profile data + ) { + BppProcessingResult result; + result.hasReplaceSessionKeys = !secondSequenceOf87.empty(); + + // Step 1: Decrypt ConfigureISDP with session keys + std::cout << "Step 1: Decrypting ConfigureISDP with session keys..." << std::endl; + result.configureIsdp = decrypt_and_verify(firstSequenceOf87, true); + + // Step 2: Verify StoreMetadata with session keys (MAC-only) + std::cout << "Step 2: Verifying StoreMetadata with session keys (MAC-only)..." << std::endl; + result.storeMetadata = decrypt_and_verify(sequenceOf88, false); + + // Step 3: If present, decrypt ReplaceSessionKeys with session keys + if (result.hasReplaceSessionKeys) { + std::cout << "Step 3: Decrypting ReplaceSessionKeys with session keys..." << std::endl; + auto rsk_data = decrypt_and_verify(secondSequenceOf87, true); + result.replaceSessionKeys = parse_replace_session_keys(rsk_data); + + // Step 4: Create NEW BSP instance with PPK and decrypt profile data + std::cout << "Step 4: Creating new BSP instance with PPK keys..." << std::endl; + auto ppk_bsp = from_replace_session_keys(result.replaceSessionKeys); + + print_hex("PPK-ENC", result.replaceSessionKeys.ppkEnc); + print_hex("PPK-MAC", result.replaceSessionKeys.ppkCmac); + print_hex("PPK Initial MCV", result.replaceSessionKeys.initialMacChainingValue); + + std::cout << "Step 5: Decrypting profile data with PPK keys..." << std::endl; + result.profileData = ppk_bsp.decrypt_and_verify(sequenceOf86, true); + + } else { + // No ReplaceSessionKeys - decrypt profile data with session keys + std::cout << "Step 3: Decrypting profile data with session keys (no PPK)..." << std::endl; + result.profileData = decrypt_and_verify(sequenceOf86, true); + } + + return result; + } + + // Reset for fresh encryption/decryption + void reset(const std::vector& initial_mcv) { + mac_chain = initial_mcv; + block_number = 1; + } + + static std::vector generate_replace_session_keys_data( + const std::vector& ppk_enc, + const std::vector& ppk_mac, + const std::vector& initial_mcv + ) { + if (ppk_enc.size() != 16 || ppk_mac.size() != 16 || initial_mcv.size() != 16) { + throw std::runtime_error("All PPK components must be 16 bytes"); + } + + std::vector rsk_data; + rsk_data.insert(rsk_data.end(), ppk_enc.begin(), ppk_enc.end()); + rsk_data.insert(rsk_data.end(), ppk_mac.begin(), ppk_mac.end()); + rsk_data.insert(rsk_data.end(), initial_mcv.begin(), initial_mcv.end()); + + return rsk_data; + } + + std::vector mac_only_one(uint8_t tag, const std::vector& plaintext) { + if (plaintext.size() > MAX_SEGMENT_SIZE - 10) { // Account for tag, length, MAC + throw std::runtime_error("Segment too large"); + } + + auto mac = compute_mac(tag, plaintext); + + // final result: tag + length + plaintext + mac + std::vector result; + result.push_back(tag); + + auto length_bytes = encode_bertlv_length(plaintext.size() + MAC_LENGTH); + result.insert(result.end(), length_bytes.begin(), length_bytes.end()); + result.insert(result.end(), plaintext.begin(), plaintext.end()); + result.insert(result.end(), mac.begin(), mac.end()); + + block_number++; + + return result; + } + + std::vector> encrypt_and_mac_seg(uint8_t tag, const std::vector& data) { + std::vector> segments; + size_t max_payload = MAX_SEGMENT_SIZE - 10; // Account for overhead + + for (size_t offset = 0; offset < data.size(); offset += max_payload) { + size_t segment_size = std::min(max_payload, data.size() - offset); + std::vector segment(data.begin() + offset, data.begin() + offset + segment_size); + segments.push_back(encrypt_and_mac_one(tag, segment)); + } + + return segments; + } + + std::vector> mac_only_seg(uint8_t tag, const std::vector& data) { + std::vector> segments; + size_t max_payload = MAX_SEGMENT_SIZE - 10; // Account for overhead + + for (size_t offset = 0; offset < data.size(); offset += max_payload) { + size_t segment_size = std::min(max_payload, data.size() - offset); + std::vector segment(data.begin() + offset, data.begin() + offset + segment_size); + segments.push_back(mac_only_one(tag, segment)); + } + + return segments; + } + + +}; + +// Python binding wrapper class +class BspCryptoWrapper { +private: + BspCrypto bsp; + // Private for from_kdf + explicit BspCryptoWrapper(BspCrypto&& bsp_instance) : bsp(std::move(bsp_instance)) {} + +public: + BspCryptoWrapper(const std::vector& s_enc, + const std::vector& s_mac, + const std::vector& initial_mcv) + : bsp(s_enc, s_mac, initial_mcv) { + } + + static BspCryptoWrapper from_kdf(const std::vector& shared_secret, + uint8_t key_type, uint8_t key_length, + const std::vector& host_id, + const std::vector& eid) { + auto bsp_instance = BspCrypto::from_kdf(shared_secret, key_type, key_length, host_id, eid); + return BspCryptoWrapper(std::move(bsp_instance)); + } + + pybind11::dict process_bound_profile_package( + pybind11::bytes& firstSequenceOf87, + pybind11::bytes& sequenceOf88, + pybind11::bytes& secondSequenceOf87, + pybind11::bytes& sequenceOf86) { + + auto pbh = [](pybind11::bytes &bytes) { + pybind11::buffer_info info(pybind11::buffer(bytes).request()); + const uint8_t *data = reinterpret_cast(info.ptr); + size_t length = static_cast(info.size); + return std::vector(data, data + length); + }; + + auto result = bsp.process_bound_profile_package(pbh(firstSequenceOf87), pbh(sequenceOf88), pbh(secondSequenceOf87), pbh(sequenceOf86)); + + pybind11::dict py_result; + py_result["configureIsdp"] = result.configureIsdp; + py_result["storeMetadata"] = result.storeMetadata; + py_result["profileData"] = result.profileData; + py_result["hasReplaceSessionKeys"] = result.hasReplaceSessionKeys; + + if (result.hasReplaceSessionKeys) { + pybind11::dict rsk; + rsk["ppkEnc"] = result.replaceSessionKeys.ppkEnc; + rsk["ppkCmac"] = result.replaceSessionKeys.ppkCmac; + rsk["initialMacChainingValue"] = result.replaceSessionKeys.initialMacChainingValue; + py_result["replaceSessionKeys"] = rsk; + } + + return py_result; + } + + pybind11::dict process_bound_profile_package2(pybind11::bytes& allofit) { + + auto pbh = [](pybind11::bytes &bytes) { + pybind11::buffer_info info(pybind11::buffer(bytes).request()); + const uint8_t *data = reinterpret_cast(info.ptr); + size_t length = static_cast(info.size); + return std::vector(data, data + length); + }; + + auto result = bsp.process_bound_profile_package(pbh(allofit)); + + pybind11::dict py_result; + py_result["configureIsdp"] = result.configureIsdp; + py_result["storeMetadata"] = result.storeMetadata; + py_result["profileData"] = result.profileData; + py_result["hasReplaceSessionKeys"] = result.hasReplaceSessionKeys; + + if (result.hasReplaceSessionKeys) { + pybind11::dict rsk; + rsk["ppkEnc"] = result.replaceSessionKeys.ppkEnc; + rsk["ppkCmac"] = result.replaceSessionKeys.ppkCmac; + rsk["initialMacChainingValue"] = result.replaceSessionKeys.initialMacChainingValue; + py_result["replaceSessionKeys"] = rsk; + } + + return py_result; + } + + std::vector encrypt_and_mac_one(uint8_t tag, const std::vector& plaintext) { + return bsp.encrypt_and_mac_one(tag, plaintext); + } + + std::vector mac_only_one(uint8_t tag, const std::vector& plaintext) { + return bsp.mac_only_one(tag, plaintext); + } + + std::vector> encrypt_and_mac_seg(uint8_t tag, const std::vector& data) { + return bsp.encrypt_and_mac_seg(tag, data); + } + + std::vector> mac_only(uint8_t tag, const std::vector& data) { + return bsp.mac_only_seg(tag, data); + } + + std::vector decrypt_and_verify(const std::vector& segments, bool decrypt = true) { + return bsp.decrypt_and_verify(segments, decrypt); + } + + void reset(const std::vector& initial_mcv) { + bsp.reset(initial_mcv); + } + + static std::vector hex_to_bytes(const std::string& hex_str) { + std::vector result; + for (size_t i = 0; i < hex_str.length(); i += 2) { + std::string byte_str = hex_str.substr(i, 2); + uint8_t byte = static_cast(std::strtol(byte_str.c_str(), nullptr, 16)); + result.push_back(byte); + } + return result; + } + + static std::string bytes_to_hex(const std::vector& data) { + std::string result; + for (uint8_t b : data) { + char hex[3]; + sprintf(hex, "%02x", b); + result += hex; + } + return result; + } +}; + +PYBIND11_MODULE(bsp_crypto, m) { + m.doc() = "BSP/BPP GSMA RSP test mod"; + + pybind11::class_(m, "BspCrypto") + .def(pybind11::init&, const std::vector&, const std::vector&>()) + .def_static("from_kdf", &BspCryptoWrapper::from_kdf) + .def("process_bound_profile_package", &BspCryptoWrapper::process_bound_profile_package) + .def("process_bound_profile_package2", &BspCryptoWrapper::process_bound_profile_package2) + .def("encrypt_and_mac_one", &BspCryptoWrapper::encrypt_and_mac_one) + .def("mac_only_one", &BspCryptoWrapper::mac_only_one) + .def("encrypt_and_mac", &BspCryptoWrapper::encrypt_and_mac_seg) + .def("mac_only", &BspCryptoWrapper::mac_only) + .def("decrypt_and_verify", &BspCryptoWrapper::decrypt_and_verify) + .def("reset", &BspCryptoWrapper::reset) + .def_static("hex_to_bytes", &BspCryptoWrapper::hex_to_bytes) + .def_static("bytes_to_hex", &BspCryptoWrapper::bytes_to_hex); +} diff --git a/bsp_test_integration.py b/bsp_test_integration.py new file mode 100644 index 00000000..d3267b3b --- /dev/null +++ b/bsp_test_integration.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 + +# (C) 2025 by sysmocom s.f.m.c. GmbH +# All Rights Reserved +# +# Author: Eric Wild +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Integrates C++ BSP implementation for testing getBoundProfilePackage in osmo-smdpp.py +""" + +import os +import sys +from typing import Dict, List, Optional, Tuple +from osmocom.utils import h2b, b2h +from osmocom.tlv import bertlv_parse_one_rawtag, bertlv_return_one_rawtlv +import base64 + +try: + import bsp_crypto + CPP_BSP_AVAILABLE = True + print("C++ BSP module loaded successfully") +except ImportError as e: + CPP_BSP_AVAILABLE = False + print(f"C++ BSP module not available: {e} - Please compile the C++ extension with: python setup.py build_ext --inplace") + +class BspTestIntegration: + """Integration class for testing BSP functionality with C++ implementation""" + + def __init__(self): + self.cpp_available = CPP_BSP_AVAILABLE + + def parse_bound_profile_package(self, bpp_der: bytes) -> Dict: + def split_bertlv_sequence(sequence: bytes) -> List[bytes]: + """Split a SEQUENCE OF into individual TLV elements""" + remainder = sequence + ret = [] + while remainder: + _tag, _l, tlv, remainder = bertlv_return_one_rawtlv(remainder) + ret.append(tlv) + return ret + + # outer BoundProfilePackage structure + tag, _l, v, _remainder = bertlv_parse_one_rawtag(bpp_der) + if len(_remainder): + raise ValueError('Excess data at end of BPP TLV') + if tag != 0xbf36: + raise ValueError(f'Unexpected BPP outer tag: 0x{tag:x}') + + result = {} + + # InitialiseSecureChannelRequest + tag, _l, iscr_bin, remainder = bertlv_return_one_rawtlv(v) + if tag != 0xbf23: # Expected tag for InitialiseSecureChannelRequest + raise ValueError(f"Unexpected ISCR tag: 0x{tag:x}") + result['iscr'] = iscr_bin + + # firstSequenceOf87 (ConfigureISDP) + tag, _l, firstSeqOf87, remainder = bertlv_parse_one_rawtag(remainder) + if tag != 0xa0: + raise ValueError(f"Unexpected 'firstSequenceOf87' tag: 0x{tag:x}") + result['firstSequenceOf87'] = split_bertlv_sequence(firstSeqOf87) + + # sequenceOf88 (StoreMetadata) + tag, _l, seqOf88, remainder = bertlv_parse_one_rawtag(remainder) + if tag != 0xa1: + raise ValueError(f"Unexpected 'sequenceOf88' tag: 0x{tag:x}") + result['sequenceOf88'] = split_bertlv_sequence(seqOf88) + + # optional secondSequenceOf87 or sequenceOf86 + tag, _l, tlv, remainder = bertlv_parse_one_rawtag(remainder) + if tag == 0xa2: # secondSequenceOf87 (ReplaceSessionKeys) + result['secondSequenceOf87'] = split_bertlv_sequence(tlv) + # sequenceOf86 + tag2, _l, seqOf86, remainder = bertlv_parse_one_rawtag(remainder) + if tag2 != 0xa3: + raise ValueError(f"Unexpected 'sequenceOf86' tag: 0x{tag2:x}") + result['sequenceOf86'] = split_bertlv_sequence(seqOf86) + elif tag == 0xa3: # straight sequenceOf86 (no ReplaceSessionKeys) + result['secondSequenceOf87'] = [] + result['sequenceOf86'] = split_bertlv_sequence(tlv) + else: + raise ValueError(f"Unexpected tag after sequenceOf88: 0x{tag:x}") + + if remainder: + raise ValueError("Unexpected data after BPP structure") + + return result + + def verify_bound_profile_package(self, + shared_secret: bytes, + key_type: int, + key_length: int, + host_id: bytes, + eid: bytes, + bpp_der: bytes, + expected_configure_isdp: Optional[bytes] = None, + expected_store_metadata: Optional[bytes] = None, + expected_profile_data: Optional[bytes] = None) -> Dict: + if not self.cpp_available: + raise RuntimeError("C++ BSP module not available") + + parsed = self.parse_bound_profile_package(bpp_der) + + print(f"BPP_VERIFY: Parsed BPP with {len(parsed['firstSequenceOf87'])} ConfigureISDP segments") + print(f"BPP_VERIFY: {len(parsed['sequenceOf88'])} StoreMetadata segments") + print(f"BPP_VERIFY: {len(parsed['secondSequenceOf87'])} ReplaceSessionKeys segments") + print(f"BPP_VERIFY: {len(parsed['sequenceOf86'])} profile data segments") + + # Convert bytes to lists for C++ - just to be safe + shared_secret_list = list(shared_secret) + host_id_list = list(host_id) + eid_bytes_list = list(eid) + + bsp = bsp_crypto.BspCrypto.from_kdf(shared_secret_list, key_type, key_length, host_id_list, eid_bytes_list) + + try: + # result = bsp.process_bound_profile_package( + # parsed['firstSequenceOf87'][0], + # parsed['sequenceOf88'][0], + # parsed['secondSequenceOf87'][0], + # parsed['sequenceOf86'][0] + # ) + + result = bsp.process_bound_profile_package2(bpp_der) + + verification_result = { + 'success': True, + 'error': None, + 'configureIsdp': bytes(result['configureIsdp']), + 'storeMetadata': bytes(result['storeMetadata']), + 'profileData': bytes(result['profileData']), + 'hasReplaceSessionKeys': result['hasReplaceSessionKeys'] + } + + if result['hasReplaceSessionKeys']: + rsk = result['replaceSessionKeys'] + verification_result['replaceSessionKeys'] = { + 'ppkEnc': bytes(rsk['ppkEnc']), + 'ppkCmac': bytes(rsk['ppkCmac']), + 'initialMacChainingValue': bytes(rsk['initialMacChainingValue']) + } + + verification_result['verification'] = {} + if expected_configure_isdp is not None: + verification_result['verification']['configureIsdp'] = ( + verification_result['configureIsdp'] == expected_configure_isdp + ) + if expected_store_metadata is not None: + verification_result['verification']['storeMetadata'] = ( + verification_result['storeMetadata'] == expected_store_metadata + ) + if expected_profile_data is not None: + verification_result['verification']['profileData'] = ( + verification_result['profileData'] == expected_profile_data + ) + + print("BPP_VERIFY: Successfully processed BoundProfilePackage") + print(f"BPP_VERIFY: ConfigureISDP: {len(verification_result['configureIsdp'])} bytes") + print(f"BPP_VERIFY: StoreMetadata: {len(verification_result['storeMetadata'])} bytes") + print(f"BPP_VERIFY: ProfileData: {len(verification_result['profileData'])} bytes") + print(f"BPP_VERIFY: Has ReplaceSessionKeys: {verification_result['hasReplaceSessionKeys']}") + + return verification_result + + except Exception as e: + return { + 'success': False, + 'error': str(e), + 'configureIsdp': None, + 'storeMetadata': None, + 'profileData': None, + 'hasReplaceSessionKeys': False + } diff --git a/osmo-smdpp.py b/osmo-smdpp.py index 9328b99b..d4b5a21d 100755 --- a/osmo-smdpp.py +++ b/osmo-smdpp.py @@ -17,6 +17,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature +from cryptography import x509 +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, PrivateFormat, NoEncryption import json import sys import argparse @@ -42,27 +48,203 @@ from pySim.esim.x509_cert import CertAndPrivkey, CertificateSet, cert_get_subjec # HACK: make this configurable DATA_DIR = './smdpp-data' -HOSTNAME = 'testsmdpplus1.example.com' # must match certificates! +HOSTNAME = 'testsmdpplus1.example.com' # must match certificates! def b64encode2str(req: bytes) -> str: """Encode given input bytes as base64 and return result as string.""" return base64.b64encode(req).decode('ascii') + def set_headers(request: IRequest): """Set the request headers as mandatory by GSMA eSIM RSP.""" request.setHeader('Content-Type', 'application/json;charset=UTF-8') request.setHeader('X-Admin-Protocol', 'gsma/rsp/v2.1.0') + +def validate_request_headers(request: IRequest): + """Validate mandatory HTTP headers according to SGP.22.""" + content_type = request.getHeader('Content-Type') + if not content_type or not content_type.startswith('application/json'): + raise ApiError('1.2.1', '2.1', 'Invalid Content-Type header') + + admin_protocol = request.getHeader('X-Admin-Protocol') + if admin_protocol and not admin_protocol.startswith('gsma/rsp/v'): + raise ApiError('1.2.2', '2.1', 'Unsupported X-Admin-Protocol version') + +def get_eum_certificate_variant(eum_cert) -> str: + """Determine EUM certificate variant by checking Certificate Policies extension. + Returns 'O' for old variant, or 'NEW' for Ov3/A/B/C variants.""" + + try: + cert_policies_ext = eum_cert.extensions.get_extension_for_oid( + x509.oid.ExtensionOID.CERTIFICATE_POLICIES + ) + + for policy in cert_policies_ext.value: + policy_oid = policy.policy_identifier.dotted_string + print(f"Found certificate policy: {policy_oid}") + + if policy_oid == '2.23.146.1.2.1.2': + print("Detected EUM certificate variant: O (old)") + return 'O' + elif policy_oid == '2.23.146.1.2.1.0.0.0': + print("Detected EUM certificate variant: Ov3/A/B/C (new)") + return 'NEW' + except x509.ExtensionNotFound: + print("No Certificate Policies extension found") + except Exception as e: + print(f"Error checking certificate policies: {e}") + +def parse_permitted_eins_from_cert(eum_cert) -> List[str]: + """Extract permitted IINs from EUM certificate using the appropriate method + based on certificate variant (O vs Ov3/A/B/C). + Returns list of permitted IINs (basically prefixes that valid EIDs must start with).""" + + # Determine certificate variant first + cert_variant = get_eum_certificate_variant(eum_cert) + permitted_iins = [] + + if cert_variant == 'O': + # Old variant - use nameConstraints extension + print("Using nameConstraints parsing for variant O certificate") + permitted_iins.extend(_parse_name_constraints_eins(eum_cert)) + + else: + # New variants (Ov3, A, B, C) - use GSMA permittedEins extension + print("Using GSMA permittedEins parsing for newer certificate variant") + permitted_iins.extend(_parse_gsma_permitted_eins(eum_cert)) + + unique_iins = list(set(permitted_iins)) + + print(f"Total unique permitted IINs found: {len(unique_iins)}") + return unique_iins + +def _parse_gsma_permitted_eins(eum_cert) -> List[str]: + """Parse the GSMA permittedEins extension using correct ASN.1 structure. + PermittedEins ::= SEQUENCE OF PrintableString + Each string contains an IIN (Issuer Identification Number) - a prefix of valid EIDs.""" + permitted_iins = [] + + try: + permitted_eins_oid = x509.ObjectIdentifier('2.23.146.1.2.2.0') # sgp26: 2.23.146.1.2.2.0 = ASN1:SEQUENCE:permittedEins + + for ext in eum_cert.extensions: + if ext.oid == permitted_eins_oid: + print(f"Found GSMA permittedEins extension: {ext.oid}") + + # Get the DER-encoded extension value + ext_der = ext.value.value if hasattr(ext.value, 'value') else ext.value + + if isinstance(ext_der, bytes): + try: + import asn1tools + + permitted_eins_schema = """ + PermittedEins DEFINITIONS ::= BEGIN + PermittedEins ::= SEQUENCE OF PrintableString + END + """ + decoder = asn1tools.compile_string(permitted_eins_schema) + decoded_strings = decoder.decode('PermittedEins', ext_der) + + for iin_string in decoded_strings: + # Each string contains an IIN -> prefix of euicc EID + iin_clean = iin_string.strip().upper() + + # IINs is 8 chars per sgp22, var len according to sgp29, fortunately we don't care + if (len(iin_clean) == 8 and + all(c in '0123456789ABCDEF' for c in iin_clean) and + len(iin_clean) % 2 == 0): + permitted_iins.append(iin_clean) + print(f"Found permitted IIN (GSMA): {iin_clean}") + else: + print(f"Invalid IIN format: {iin_string} (cleaned: {iin_clean})") + except Exception as e: + print(f"Error parsing GSMA permittedEins extension: {e}") + + except Exception as e: + print(f"Error accessing GSMA certificate extensions: {e}") + + return permitted_iins + + +def _parse_name_constraints_eins(eum_cert) -> List[str]: + """Parse permitted IINs from nameConstraints extension (variant O).""" + permitted_iins = [] + + try: + # Look for nameConstraints extension + name_constraints_ext = eum_cert.extensions.get_extension_for_oid( + x509.oid.ExtensionOID.NAME_CONSTRAINTS + ) + + print("Found nameConstraints extension (variant O)") + name_constraints = name_constraints_ext.value + + # Check permittedSubtrees for IIN constraints + if name_constraints.permitted_subtrees: + for subtree in name_constraints.permitted_subtrees: + print(f"Processing permitted subtree: {subtree}") + + if isinstance(subtree, x509.DirectoryName): + for attribute in subtree.value: + # IINs for O in serialNumber + if attribute.oid == x509.oid.NameOID.SERIAL_NUMBER: + serial_value = attribute.value.upper() + # sgp22 8, sgp29 var len, fortunately we don't care + if (len(serial_value) == 8 and + all(c in '0123456789ABCDEF' for c in serial_value) and + len(serial_value) % 2 == 0): + permitted_iins.append(serial_value) + print(f"Found permitted IIN (nameConstraints/DN): {serial_value}") + + except x509.ExtensionNotFound: + print("No nameConstraints extension found") + except Exception as e: + print(f"Error parsing nameConstraints: {e}") + + return permitted_iins + + +def validate_eid_range(eid: str, eum_cert) -> bool: + """Validate that EID is within the permitted EINs of the EUM certificate.""" + if not eid or len(eid) != 32: + print(f"Invalid EID format: {eid}") + return False + + try: + permitted_eins = parse_permitted_eins_from_cert(eum_cert) + + if not permitted_eins: + print("Warning: No permitted EINs found in EUM certificate") + return False + + eid_normalized = eid.upper() + print(f"Validating EID {eid_normalized} against {len(permitted_eins)} permitted EINs") + + for permitted_ein in permitted_eins: + if eid_normalized.startswith(permitted_ein): + print(f"EID {eid_normalized} matches permitted EIN {permitted_ein}") + return True + + print(f"EID {eid_normalized} is not in any permitted EIN list") + return False + + except Exception as e: + print(f"Error validating EID: {e}") + return False + + def build_status_code(subject_code: str, reason_code: str, subject_id: Optional[str], message: Optional[str]) -> Dict: - r = {'subjectCode': subject_code, 'reasonCode': reason_code } + r = {'subjectCode': subject_code, 'reasonCode': reason_code} if subject_id: r['subjectIdentifier'] = subject_id if message: r['message'] = message return r -def build_resp_header(js: dict, status: str = 'Executed-Success', status_code_data = None) -> None: +def build_resp_header(js: dict, status: str = 'Executed-Success', status_code_data=None) -> None: # SGP.22 v3.0 6.5.1.4 js['header'] = { 'functionExecutionStatus': { @@ -72,12 +254,6 @@ def build_resp_header(js: dict, status: str = 'Executed-Success', status_code_da if status_code_data: js['header']['functionExecutionStatus']['statusCodeData'] = status_code_data -from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature -from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, PrivateFormat, NoEncryption -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives import hashes -from cryptography.exceptions import InvalidSignature -from cryptography import x509 def ecdsa_tr03111_to_dss(sig: bytes) -> bytes: """convert an ECDSA signature from BSI TR-03111 format to DER: first get long integers; then encode those.""" @@ -116,9 +292,9 @@ class SmDppHttpServer: with open(os.path.join(dirpath, filename), 'rb') as f: cert = x509.load_pem_x509_certificate(f.read()) if cert: - # verify it is a CI certificate (keyCertSign + i-rspRole-ci) - if not cert_policy_has_oid(cert, oid.id_rspRole_ci): - raise ValueError("alleged CI certificate %s doesn't have CI policy" % filename) + # # verify it is a CI certificate (keyCertSign + i-rspRole-ci) + # if not cert_policy_has_oid(cert, oid.id_rspRole_ci): + # raise ValueError("alleged CI certificate %s doesn't have CI policy" % filename) certs.append(cert) return certs @@ -134,6 +310,20 @@ class SmDppHttpServer: return cert return None + def validate_certificate_chain_for_verification(self, euicc_ci_pkid_list: List[bytes]) -> bool: + """Validate that SM-DP+ has valid certificate chains for the given CI PKIDs.""" + for ci_pkid in euicc_ci_pkid_list: + ci_cert = self.ci_get_cert_for_pkid(ci_pkid) + if ci_cert: + # Check if our DPauth certificate chains to this CI + try: + cs = CertificateSet(ci_cert) + cs.verify_cert_chain(self.dp_auth.cert) + return True + except VerifyError: + continue + return False + def __init__(self, server_hostname: str, ci_certs_path: str, use_brainpool: bool = False): self.server_hostname = server_hostname self.upp_dir = os.path.realpath(os.path.join(DATA_DIR, 'upp')) @@ -179,11 +369,10 @@ class SmDppHttpServer: functionality, such as JSON decoding/encoding and debug-printing.""" @functools.wraps(func) def _api_wrapper(self, request: IRequest): - # TODO: evaluate User-Agent + X-Admin-Protocol header - # TODO: reject any non-JSON Content-type + validate_request_headers(request) content = json.loads(request.content.read()) - print("Rx JSON: %s" % json.dumps(content)) + # print("Rx JSON: %s" % json.dumps(content)) set_headers(request) output = func(self, request, content) @@ -191,7 +380,7 @@ class SmDppHttpServer: return '' build_resp_header(output) - print("Tx JSON: %s" % json.dumps(output)) + # print("Tx JSON: %s" % json.dumps(output)) return json.dumps(output) return _api_wrapper @@ -202,7 +391,7 @@ class SmDppHttpServer: # Verify that the received address matches its own SM-DP+ address, where the comparison SHALL be # case-insensitive. Otherwise, the SM-DP+ SHALL return a status code "SM-DP+ Address - Refused". if content['smdpAddress'] != self.server_hostname: - raise ApiError('8.8.1', '3.8', 'Invalid SM-DP+ Address') + raise ApiError('8.8.1', '3.8', 'Invalid SM-DP+ Address') euiccChallenge = b64decode(content['euiccChallenge']) if len(euiccChallenge) != 16: @@ -211,13 +400,19 @@ class SmDppHttpServer: euiccInfo1_bin = b64decode(content['euiccInfo1']) euiccInfo1 = rsp.asn1.decode('EUICCInfo1', euiccInfo1_bin) print("Rx euiccInfo1: %s" % euiccInfo1) - #euiccInfo1['svn'] + # euiccInfo1['svn'] # TODO: If euiccCiPKIdListForSigningV3 is present ... pkid_list = euiccInfo1['euiccCiPKIdListForSigning'] if 'euiccCiPKIdListForSigningV3' in euiccInfo1: pkid_list = pkid_list + euiccInfo1['euiccCiPKIdListForSigningV3'] + + # Validate that SM-DP+ supports certificate chains for verification + # verification_pkid_list = euiccInfo1.get('euiccCiPKIdListForVerification', []) + # if verification_pkid_list and not self.validate_certificate_chain_for_verification(verification_pkid_list): + # raise ApiError('8.8.4', '3.7', 'The SM-DP+ has no CERT.DPauth.SIG which chains to one of the eSIM CA Root CA Certificate with a Public Key supported by the eUICC') + # verify it supports one of the keys indicated by euiccCiPKIdListForSigning ci_cert = None for x in pkid_list: @@ -230,14 +425,7 @@ class SmDppHttpServer: else: ci_cert = None if not ci_cert: - raise ApiError('8.8.2', '3.1', 'None of the proposed Public Key Identifiers is supported by the SM-DP+') - - # TODO: Determine the set of CERT.DPauth.SIG that satisfy the following criteria: - # * Part of a certificate chain ending at one of the eSIM CA RootCA Certificate, whose Public Keys is - # supported by the eUICC (indicated by euiccCiPKIdListForVerification). - # * Using a certificate chain that the eUICC and the LPA both support: - #euiccInfo1['euiccCiPKIdListForVerification'] - # raise ApiError('8.8.4', '3.7', 'The SM-DP+ has no CERT.DPauth.SIG which chains to one of the eSIM CA Root CA CErtificate with a Public Key supported by the eUICC') + raise ApiError('8.8.2', '3.1', 'None of the proposed Public Key Identifiers is supported by the SM-DP+') # Generate a TransactionID which is used to identify the ongoing RSP session. The TransactionID # SHALL be unique within the scope and lifetime of each SM-DP+. @@ -253,7 +441,7 @@ class SmDppHttpServer: 'euiccChallenge': euiccChallenge, 'serverAddress': self.server_hostname, 'serverChallenge': serverChallenge, - } + } print("Tx serverSigned1: %s" % serverSigned1) serverSigned1_bin = rsp.asn1.encode('ServerSigned1', serverSigned1) print("Tx serverSigned1: %s" % rsp.asn1.decode('ServerSigned1', serverSigned1_bin)) @@ -267,9 +455,9 @@ class SmDppHttpServer: output['transactionId'] = transactionId server_cert_aki = self.dp_auth.get_authority_key_identifier() output['euiccCiPKIdToBeUsed'] = b64encode2str(b'\x04\x14' + server_cert_aki.key_identifier) - output['serverCertificate'] = b64encode2str(self.dp_auth.get_cert_as_der()) # CERT.DPauth.SIG + output['serverCertificate'] = b64encode2str(self.dp_auth.get_cert_as_der()) # CERT.DPauth.SIG # FIXME: add those certificate - #output['otherCertsInChain'] = b64encode2str() + # output['otherCertsInChain'] = b64encode2str() # create SessionState and store it in rss self.rss[transactionId] = rsp.RspSessionState(transactionId, serverChallenge, @@ -288,8 +476,8 @@ class SmDppHttpServer: print("Rx %s: %s" % authenticateServerResp) if authenticateServerResp[0] == 'authenticateResponseError': r_err = authenticateServerResp[1] - #r_err['transactionId'] - #r_err['authenticateErrorCode'] + # r_err['transactionId'] + # r_err['authenticateErrorCode'] raise ValueError("authenticateResponseError %s" % r_err) r_ok = authenticateServerResp[1] @@ -313,7 +501,7 @@ class SmDppHttpServer: if ss is None: raise ApiError('8.10.1', '3.9', 'Unknown') ss.euicc_cert = euicc_cert - ss.eum_cert = eum_cert # TODO: do we need this in the state? + ss.eum_cert = eum_cert # TODO: do we need this in the state? # Verify that the Root Certificate of the eUICC certificate chain corresponds to the # euiccCiPKIdToBeUsed or TODO: euiccCiPKIdToBeUsedV3 @@ -330,17 +518,18 @@ class SmDppHttpServer: raise ApiError('8.1.3', '6.1', 'Verification failed (certificate chain)') # raise ApiError('8.1.3', '6.3', 'Expired') - # Verify euiccSignature1 over euiccSigned1 using pubkey from euiccCertificate. # Otherwise, the SM-DP+ SHALL return a status code "eUICC - Verification failed" if not self._ecdsa_verify(euicc_cert, euiccSignature1_bin, euiccSigned1_bin): raise ApiError('8.1', '6.1', 'Verification failed (euiccSignature1 over euiccSigned1)') - # TODO: verify EID of eUICC cert is within permitted range of EUM cert - ss.eid = ss.euicc_cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value print("EID (from eUICC cert): %s" % ss.eid) + # Verify EID is within permitted range of EUM certificate + if not validate_eid_range(ss.eid, eum_cert): + raise ApiError('8.1.4', '6.1', 'EID is not within the permitted range of the EUM certificate') + # Verify that the serverChallenge attached to the ongoing RSP session matches the # serverChallenge returned by the eUICC. Otherwise, the SM-DP+ SHALL return a status code "eUICC - # Verification failed". @@ -360,7 +549,7 @@ class SmDppHttpServer: # look up profile based on matchingID. We simply check if a given file exists for now.. path = os.path.join(self.upp_dir, matchingId) + '.der' # prevent directory traversal attack - if os.path.commonprefix((os.path.realpath(path),self.upp_dir)) != self.upp_dir: + if os.path.commonprefix((os.path.realpath(path), self.upp_dir)) != self.upp_dir: raise ApiError('8.2.6', '3.8', 'Refused') if not os.path.isfile(path) or not os.access(path, os.R_OK): raise ApiError('8.2.6', '3.8', 'Refused') @@ -385,8 +574,8 @@ class SmDppHttpServer: smdpSigned2 = { 'transactionId': h2b(ss.transactionId), 'ccRequiredFlag': False, # whether the Confirmation Code is required - #'bppEuiccOtpk': None, # whether otPK.EUICC.ECKA already used for binding the BPP, tag '5F49' - } + # 'bppEuiccOtpk': None, # whether otPK.EUICC.ECKA already used for binding the BPP, tag '5F49' + } smdpSigned2_bin = rsp.asn1.encode('SmdpSigned2', smdpSigned2) ss.smdpSignature2_do = b'\x5f\x37\x40' + self.dp_pb.ecdsa_sign(smdpSigned2_bin + b'\x5f\x37\x40' + euiccSignature1_bin) @@ -398,7 +587,7 @@ class SmDppHttpServer: 'profileMetadata': b64encode2str(profileMetadata_bin), 'smdpSigned2': b64encode2str(smdpSigned2_bin), 'smdpSignature2': b64encode2str(ss.smdpSignature2_do), - 'smdpCertificate': b64encode2str(self.dp_pb.get_cert_as_der()), # CERT.DPpb.SIG + 'smdpCertificate': b64encode2str(self.dp_pb.get_cert_as_der()), # CERT.DPpb.SIG } @app.route('/gsma/rsp2/es9plus/getBoundProfilePackage', methods=['POST']) @@ -418,8 +607,8 @@ class SmDppHttpServer: if prepDownloadResp[0] == 'downloadResponseError': r_err = prepDownloadResp[1] - #r_err['transactionId'] - #r_err['downloadErrorCode'] + # r_err['transactionId'] + # r_err['downloadErrorCode'] raise ValueError("downloadResponseError %s" % r_err) r_ok = prepDownloadResp[1] @@ -444,8 +633,8 @@ class SmDppHttpServer: ss.smdp_ot = ec.generate_private_key(self.dp_pb.get_curve()) # extract the public key in (hopefully) the right format for the ES8+ interface ss.smdp_otpk = ss.smdp_ot.public_key().public_bytes(Encoding.X962, PublicFormat.UncompressedPoint) - print("smdpOtpk: %s" % b2h(ss.smdp_otpk)) - print("smdpOtsk: %s" % b2h(ss.smdp_ot.private_bytes(Encoding.DER, PrivateFormat.PKCS8, NoEncryption()))) + # print("smdpOtpk: %s" % b2h(ss.smdp_otpk)) + # print("smdpOtsk: %s" % b2h(ss.smdp_ot.private_bytes(Encoding.DER, PrivateFormat.PKCS8, NoEncryption()))) ss.host_id = b'mahlzeit' @@ -461,7 +650,7 @@ class SmDppHttpServer: upp = UnprotectedProfilePackage.from_der(f.read(), metadata=ss.profileMetadata) # HACK: Use empty PPP as we're still debuggin the configureISDP step, and we want to avoid # cluttering the log with stuff happening after the failure - #upp = UnprotectedProfilePackage.from_der(b'', metadata=ss.profileMetadata) + # upp = UnprotectedProfilePackage.from_der(b'', metadata=ss.profileMetadata) if False: # Use random keys bpp = BoundProfilePackage.from_upp(upp) @@ -472,10 +661,24 @@ class SmDppHttpServer: # update non-volatile state with updated ss object self.rss[transactionId] = ss - return { + rv = { 'transactionId': transactionId, 'boundProfilePackage': b64encode2str(bpp.encode(ss, self.dp_pb)), } + import bsp_test_integration as integ + integration = integ.BspTestIntegration() + bpp_der = base64.b64decode(rv['boundProfilePackage']) #.decode('ascii') + verification = integration.verify_bound_profile_package( + shared_secret=ss.shared_secret, + key_type=0x88, + key_length=16, + host_id=ss.host_id, + eid=h2b(ss.eid), + bpp_der=bpp_der + ) + + assert verification['success'], f"BPP verification failed: {verification['error']}" + return rv @app.route('/gsma/rsp2/es9plus/handleNotification', methods=['POST']) @rsp_api_wrapper @@ -530,9 +733,9 @@ class SmDppHttpServer: else: raise ValueError(pendingNotification) - #@app.route('/gsma/rsp3/es9plus/handleDeviceChangeRequest, methods=['POST']') - #@rsp_api_wrapper - #"""See ES9+ ConfirmDeviceChange in SGP.22 Section 5.6.6""" + # @app.route('/gsma/rsp3/es9plus/handleDeviceChangeRequest, methods=['POST']') + # @rsp_api_wrapper + # """See ES9+ ConfirmDeviceChange in SGP.22 Section 5.6.6""" # TODO: implement this @app.route('/gsma/rsp2/es9plus/cancelSession', methods=['POST']) @@ -576,20 +779,67 @@ class SmDppHttpServer: # delete actual session data del self.rss[transactionId] - return { 'transactionId': transactionId } + return {'transactionId': transactionId} def main(argv): parser = argparse.ArgumentParser() parser.add_argument("-H", "--host", help="Host/IP to bind HTTP to", default="localhost") parser.add_argument("-p", "--port", help="TCP port to bind HTTP to", default=8000) - #parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0) + # parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0) args = parser.parse_args() hs = SmDppHttpServer(HOSTNAME, os.path.join(DATA_DIR, 'certs', 'CertificateIssuer'), use_brainpool=False) - #hs.app.run(endpoint_description="ssl:port=8000:dhParameters=dh_param_2048.pem") - hs.app.run(args.host, args.port) + # hs.app.run(HOSTNAME,endpoint_description="ssl:port=8000:dhParameters=dh_param_2048.pem") + + from cryptography.hazmat.primitives.asymmetric import dh + from cryptography.hazmat.primitives import serialization + from pathlib import Path + + cert_derpath = Path(DATA_DIR) / 'certs' / 'DPtls' / 'CERT_S_SM_DP_TLS_NIST.der' + cert_pempath = Path(DATA_DIR) / 'certs' / 'DPtls' / 'CERT_S_SM_DP_TLS_NIST.pem' + cert_skpath = Path(DATA_DIR) / 'certs' / 'DPtls' / 'SK_S_SM_DP_TLS_NIST.pem' + dhparam_path = Path("dhparam2048.pem") + if not dhparam_path.exists(): + print("Generating dh params, this takes a few seconds..") + # Generate DH parameters with 2048-bit key size and generator 2 + parameters = dh.generate_parameters(generator=2, key_size=2048) + pem_data = parameters.parameter_bytes(encoding=serialization.Encoding.PEM,format=serialization.ParameterFormat.PKCS3) + with open(dhparam_path, 'wb') as file: + file.write(pem_data) + print("DH params created successfully") + + if not cert_pempath.exists(): + print("Translating tls server cert from DER to PEM..") + with open(cert_derpath, 'rb') as der_file: + der_cert_data = der_file.read() + + cert = x509.load_der_x509_certificate(der_cert_data) + pem_cert = cert.public_bytes(serialization.Encoding.PEM) #.decode('utf-8') + + with open(cert_pempath, 'wb') as pem_file: + pem_file.write(pem_cert) + + SERVER_STRING = f'ssl:8000:privateKey={cert_skpath}:certKey={cert_pempath}:dhParameters={dhparam_path}' + print(SERVER_STRING) + + hs.app.run(HOSTNAME, endpoint_description=SERVER_STRING) + # hs.app.run(args.host, args.port) + if __name__ == "__main__": main(sys.argv) + + +# (.venv) âžœ ~/work/smdp/pysim git:(master) ✗ cp -a ../sgp26/SGP.26_v1.5_Certificates_18_07_2024/SGP.26_v1.5-2024_files/Valid\ Test\ Cases/SM-DP+/DPtls/CERT_S_SM_DP_TLS_NIST.der . +# (.venv) âžœ ~/work/smdp/pysim git:(master) ✗ cp -a ../sgp26/SGP.26_v1.5_Certificates_18_07_2024/SGP.26_v1.5-2024_files/Valid\ Test\ Cases/SM-DP+/DPtls/SK_S_SM_DP_TLS_NIST.pem . +# (.venv) âžœ ~/work/smdp/pysim git:(master) ✗ openssl x509 -inform der -in CERT_S_SM_DP_TLS_NIST.der -out CERT_S_SM_DP_TLS_NIST.pem + + +# cp -a Variants\ A_B_C/CI/CERT_CI_SIG_* ../pysim/smdpp-data/certs/CertificateIssuer +# cp -a Variants\ A_B_C/CI_subCA/CERT_*_SIG_* ../pysim/smdpp-data/certs/CertificateIssuer +# cp -a Variants\ A_B_C/Variant\ C/SM-DP+/SM_DPauth/CERT* ../pysim/smdpp-data/certs/DPauth +# cp -a Variants\ A_B_C/Variant\ C/SM-DP+/SM_DPpb/CERT* ../pysim/smdpp-data/certs/DPpb +# cp -a Variants\ A_B_C/Variant\ C/SM-DP+/SM_DPtls/CERT* ../pysim/smdpp-data/certs/DPtls +# cp -a Variants\ A_B_C/Variant\ C/EUM_SUB_CA/CERT_EUMSubCA_VARC_SIG_* ../pysim/smdpp-data/certs/intermediate \ No newline at end of file diff --git a/pySim/esim/bsp.py b/pySim/esim/bsp.py index fb4c0b37..fbc687ab 100644 --- a/pySim/esim/bsp.py +++ b/pySim/esim/bsp.py @@ -149,8 +149,18 @@ class BspAlgoMac(BspAlgo, abc.ABC): temp_data = self.mac_chain + tag_and_length + data old_mcv = self.mac_chain c_mac = self._auth(temp_data) + + # DEBUG: Show MAC computation details + print(f"MAC_DEBUG: tag=0x{tag:02x}, lcc={lcc}") + print(f"MAC_DEBUG: tag_and_length: {tag_and_length.hex()}") + print(f"MAC_DEBUG: mac_chain[:20]: {old_mcv[:20].hex()}") + print(f"MAC_DEBUG: temp_data[:20]: {temp_data[:20].hex()}") + print(f"MAC_DEBUG: c_mac: {c_mac.hex()}") + # The output data is computed by concatenating the following data: the tag, the final length, the result of step 2 and the C-MAC value. ret = tag_and_length + data + c_mac + print(f"MAC_DEBUG: final_output[:20]: {ret[:20].hex()}") + logger.debug("auth(tag=0x%x, mcv=%s, s_mac=%s, plaintext=%s, temp=%s) -> %s", tag, b2h(old_mcv), b2h(self.s_mac), b2h(data), b2h(temp_data), b2h(ret)) return ret @@ -203,6 +213,11 @@ def bsp_key_derivation(shared_secret: bytes, key_type: int, key_length: int, hos initial_mac_chaining_value = out[0:l] s_enc = out[l:2*l] s_mac = out[l*2:3*l] + + print(f"BSP_KDF_DEBUG: kdf_out = {b2h(out)}") + print(f"BSP_KDF_DEBUG: initial_mcv = {b2h(initial_mac_chaining_value)}") + print(f"BSP_KDF_DEBUG: s_enc = {b2h(s_enc)}") + print(f"BSP_KDF_DEBUG: s_mac = {b2h(s_mac)}") return s_enc, s_mac, initial_mac_chaining_value @@ -231,9 +246,21 @@ class BspInstance: """Encrypt + MAC a single plaintext TLV. Returns the protected ciphertext.""" assert tag <= 255 assert len(plaintext) <= self.max_payload_size + + # DEBUG: Show what we're processing + print(f"BSP_DEBUG: encrypt_and_mac_one(tag=0x{tag:02x}, plaintext_len={len(plaintext)})") + print(f"BSP_DEBUG: plaintext[:20]: {plaintext[:20].hex()}") + print(f"BSP_DEBUG: s_enc[:20]: {self.c_algo.s_enc[:20].hex()}") + print(f"BSP_DEBUG: s_mac[:20]: {self.m_algo.s_mac[:20].hex()}") + logger.debug("encrypt_and_mac_one(tag=0x%x, plaintext=%s)", tag, b2h(plaintext)) ciphered = self.c_algo.encrypt(plaintext) + print(f"BSP_DEBUG: ciphered[:20]: {ciphered[:20].hex()}") + maced = self.m_algo.auth(tag, ciphered) + print(f"BSP_DEBUG: final_result[:20]: {maced[:20].hex()}") + print(f"BSP_DEBUG: final_result_len: {len(maced)}") + return maced def encrypt_and_mac(self, tag: int, plaintext:bytes) -> List[bytes]: diff --git a/pySim/esim/es8p.py b/pySim/esim/es8p.py index 8bd7e14b..74b23c0c 100644 --- a/pySim/esim/es8p.py +++ b/pySim/esim/es8p.py @@ -196,8 +196,12 @@ class BoundProfilePackage(ProfilePackage): # 'initialiseSecureChannelRequest' bpp_seq = rsp.asn1.encode('InitialiseSecureChannelRequest', iscr) # firstSequenceOf87 + print(f"BPP_ENCODE_DEBUG: Encrypting ConfigureISDP with BSP keys") + print(f"BPP_ENCODE_DEBUG: BSP S-ENC: {bsp.c_algo.s_enc.hex()}") + print(f"BPP_ENCODE_DEBUG: BSP S-MAC: {bsp.m_algo.s_mac.hex()}") bpp_seq += encode_seq(0xa0, bsp.encrypt_and_mac(0x87, conf_idsp_bin)) # sequenceOF88 + print(f"BPP_ENCODE_DEBUG: MAC-only StoreMetadata with BSP keys") bpp_seq += encode_seq(0xa1, bsp.mac_only(0x88, smr_bin)) if self.ppp: # we have to use session keys diff --git a/pyproject.toml b/pyproject.toml index 9787c3bd..8902b081 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools", "wheel", "pybind11"] build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt index 9749d7fd..18c7684e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,8 @@ git+https://github.com/osmocom/asn1tools packaging git+https://github.com/hologram-io/smpp.pdu smpp.twisted3 @ git+https://github.com/jookies/smpp.twisted +pybind11 +klein +service-identity +pyopenssl +requests \ No newline at end of file diff --git a/setup.py b/setup.py index b33fb67d..397b41f9 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,15 @@ from setuptools import setup +from pybind11.setup_helpers import Pybind11Extension, build_ext + +ext_modules = [ + Pybind11Extension( + "bsp_crypto", + ["bsp_python_bindings.cpp"], + libraries=["ssl", "crypto"], + extra_compile_args=["-ggdb", "-O0"], + cxx_std=17, + ), +] setup( name='pySim', @@ -34,6 +45,11 @@ setup( "smpp.pdu @ git+https://github.com/hologram-io/smpp.pdu", "asn1tools", "smpp.twisted3 @ git+https://github.com/jookies/smpp.twisted", + "pybind11", + "klein", + "service-identity", + "pyopenssl", + "requests", ], scripts=[ 'pySim-prog.py', @@ -49,4 +65,8 @@ setup( 'asn1/saip/*.asn', ], }, + ext_modules=ext_modules, + cmdclass={"build_ext": build_ext}, + zip_safe=False, + python_requires=">=3.6", ) diff --git a/smdpp-data/certs/DPauth/data_sig.der b/smdpp-data/certs/DPauth/data_sig.der deleted file mode 100644 index 7f44ce27..00000000 --- a/smdpp-data/certs/DPauth/data_sig.der +++ /dev/null @@ -1 +0,0 @@ -0D AL¶þV¿eRÌÍìAˆHÊt£×ôͺ„nìEYGh_&5hc!RWNKhxXb9nMY&yv3qQGb&^pR0;+2iw~wRS6a zEI+O=iABNhC`;__HKw&sOWcnzDKh+Ua17;WnsT?J==|Tim(*sTDVhH0fauZ*Ec*o% Jr*~V}0RSrBC@25` delta 106 zcmZo?ZD*a3A!Te}W?*S(X<%w-7$weYWMW`$Xb9nMY&yv3qR8;8f71-p9dGaa3!Zmv zUN6gzko28L+$*h*aV*>B<<;(9%A`>KBs7IPr7CmM-bJNXtc&J;-K6XP{-xs&VfSx` JyU}$P+7$weYWNKhxXb9$RY&^v1qR6mm{_Q;WuknkrJ<@X+ zw|>{LQDS-GeEWS*bAb$tU2~TSlY(!%tiz<6FT;xFO#3U}$P&5+%-SWNKhxXb9nMY&yi~Y$cjo7{geX$B`g-(r^B+ z=eF9JVHp?t?@!ts^J+olpDRp?4E^i3mvk6Ze7$vc&PlDA-!mh%WW?Dyeco+&l9#mj H!S7!HMYJl^ delta 105 zcmZo?ZD*a3A!Te}W?*S(X<%w(870nZWMW`$Xb9nMY&yi~T(?zz0*7BgL%l}EjrZE? zYGi3_7A4MWWNKhxXb9nMY&^{9RQN2$XYVZTx$R%0wLLr! zrT(sXA-$*Q`Lq|Wm-4(1yr;*c;2JVRsN76{T~h6H^^ScR#}AA2u(4>~ydL-Fb!>>! G9&G@EX(`qK delta 104 zcmZo;ZDXB~E@f#V#;Zx~lfE3lE2u%wwJJ FEdZE|B|HEC diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_NIST.der b/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_NIST.der index 0cb9ba7500a802f3918d7c3f7162388ee7127750..b819e77ad8538c855b82e6d39f98c4590ca8492e 100644 GIT binary patch delta 105 zcmZo?ZD*a3E@fh1ZeVF>YGh$-93{?cWNKhxXb9nMY&^{9yf@4FyZRb6r?;NZb>@pz ztlyHE$|;&rTJX|Q*v7;xS2ZNLdq#3sz-8DbzO&TC{~U}9isU}0ir zXb~mOYiwX(Y-|AK8W_MM|*D#yzI52lGLuNQoO;_k9pnV+^wbZ-_CZsOW>SMFmMDV=1FZ!b3)I?FGD=Dc ztn~GhGxb1Wi5!Ib&Otu9j(W*OIYg>;0;@$zAI$Cy1};nr>b&b^#O9}56MZe{#o)-$ z+;WevHEef7cy)NAdFEMG7bXP@E*rVv>-o~WJ;lGbpG$9)O~0#Gb2$4rpPk_Q`eUaZ E0pO3cvH$=8 diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP4_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP4_TLS.der deleted file mode 100644 index 70820fa019a0db99eba434a017dcd372bf547a44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 647 zcmXqLVrn*MV$xi|%*4pV#KbwpfQyYotIgw_EekV~fs3Jl0UsN4C<`+Wdq`?=iGs7I zp`ZajNQ9e*Bg8d0#Mw0{1g40Khdn4b03>S2ZNLdq#3sz-8DbzO&TC{~U}9isU}0ir zXb~mOYiwX(Y-|AK8W<0J+cnwR)>Ery?)EDYV1BW`M4v;LX;tmA#Z8P-22G4& zV4utKGcx{XVPR%sZvZ<=Ru#k-GZ10p&}L&~Wo2h(gtM3oMA(=^WffR>4M6^9Pj3`r zWMX7CkOhhIv52vV2=m)kZ{Co))+VB&Y|rlOmcO1=s}1Bq(#k9n24W2&Umc6~{_L81 zEoR@2E{lZ+WgEC!4;yHL6bP~?87MU24v-G!4s$+(M37Q>mMDV=1FZ!b3)I?FGD=Dc ztn~GhGxb1Wi5!Ib&Otu9j(W*OIYg>;0;@$zAIu&M2ChsBg8i&4sXd3U>ua`nZ=0ME zyn4E@mC`rWo#J;stS2ZNLdq#3sz-8DbzO&TC{~U}9itXk=n$ zY#AlaYYgI=L%9aVhP(#cAmv;G=*i?vr;IP>|m z>@RGyG9xUtpITmEcon$Rqg$Z;+H3a%j4iox8Ffb{ypU>`=IO)}l)bo#QOcl+Q4H*J zS$;;w|12!bOzaI{N6D&!_+kbkY#iEbjI6Be%#3grlYs~ubEvEW3$Fpl|Lp0FLX1p| z%m%U`Q9c$i7LgrwT*aSUuFmw)NPPAx&!FI%iB_vD#m1r4=5fxJg_+5~#ZbV2kBvE$g_(yvB(=Ci!P(PL z(10H#!p*}G;u;*{>>3mTQ^du?9uyn^5;f#D-~=gR6K3)ZF_07IH8LDHZU+YHh^*sj174WxIxOfcvu{reO(Qe3=}|IW**6s)Z&uj+?0ZXoYG=Lz0``t z+=84`z2y8{gD3+LHg>Q>nHbqPwN;pznK&33*_oXfSbUVu+|cP|Qv7)%#JcDq`<|13 zCxl$fDLwGIWcl9b2G0}wzD28Z$2C2%s?6_w$GyJm_v^0Uq~I#1zp2OPIZ9@4UEIVd zWzfVZ2KKrvKO^IR78YhE_6D%4WK}_YF#{1c4sA9@R#tXqMmUSfK!lAsR91n7*8mg% z?CFg{j7*Ho2C^VgJ{B<+5nE%<=FjubL|=NcHe;FOa(9n)-E$4(LDI@B5(Z)oA_w*q z>@u4)*Lh3&|L|~DgQrzH{@gIo1St??Q8G|y!W|+V%pK-j28kf0@+?sX5e8ZdG#03} zsbrLt6jjYMdlt7r>84O&Q6oRjQNseSr^pJM> zdqT5z@BH(^n^<>eEM8~gZ5wt_KISTug8ZCKM#_tJc#8I|J^ttDzN0RH^G=Kufz diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP_TLS_NIST.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP_TLS_NIST.der deleted file mode 100644 index 38927b17046cc9fb3dc9a420bb9c0b00461f8bc6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 647 zcmXqLVrn*MV$xW^%*4pV#K>vD#m1r4=5fxJg_+5~#ZbV2kBvE$g_(yvB(=Ci!P(PL z(10H#!p*}G;u;*{>>3mTQ^du?9uyn^5;f#D-~=gR6K3)ZF_07IH8LQ>nHZriW@cn(c4A=BP%$Vy*Y{U`U3!M4L+z$KpVQ6-Q<7w6 zTP7NR+~mELamPQuO}=)#minsp`O;I83O1k8)HAuAU)8>O>9;jg;yD&KF-jRUF^YkG zF3Zoz_@9M^nTfpt>?m1P5MRtdgpEU+jggg=otY8NVloh6V-A&7VBs|Y`JX+#QHYU= zk=Z~NB+ADk#v-Er@8c&;iMr#>Z@)dzOj#1x8-8V_fjmfBnMJ}ttU=_fW6|E9T~n{c z?Ay_0vGAa516S)|15JK+T+cCx4v8TwVX}gD2qvvVUxWlo8TUst__RJr(an7KxzAIldRW8Cx2fxFkPLq H`y2-VF+aQ> diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP2_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP2_TLS.der deleted file mode 100644 index 32909ce2299daef2809ed5da1190b879c8d8fe89..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 647 zcmXqLVrn*MV$xi|%*4pV#KbVufQyYotIgw_EekV~fs3Jl0UsN4C<`+Wdq`?=iGs7I zp`ZajNQ9e*Bg8d0#Mw0{1g40Khdn4b03>S2ZNLdq#3sz-8DbzO&TC|7U}|V+U}0on zY7iyPYh+|#W@rrM8W_MM|*D#yzI52lGLuNQoO;_k9pnV+^wbZ-_CZsOW>SMFmMDV=1FZ!b3)I?FGD=Dc ztn~GhGxb1Wi5!Ib&Otu9j(W*OIYg>;0;@$zAIu&M2ChsBB70Rb*U7EaTOP$Jw{K79 zyGh4F4I)?X|MdQ!((bF07f&%MGF;ppk-7BDj7%*ZS2ZNLdq#3sz-8DbzO&TC|7U}|V+U}0ov zY!D^RYh+|#W@rrM8W<0J+cnwR)>Ery?)EDYV1BW`M4v;LX;tmA#Z8P-22G4& zV4utKGcx{XVPR%sZvZ<=Ru#k-GZ10p&}L&~Wo2h(gtM3oMA(=^WffR>4M6^9Pj3`r zWMX7CkOhhIv52vV2=m)kZ{Co))+VB&Y|rlOmcO1=s}1Bq(#k9n24W2&Umc6~{_L81 zEoR@2E{lZ+WgEC!4;yHL6bP~?87MU24v-G!4s$+(M37Q>mMDV=1FZ!b3)I?FGD=Dc ztn~GhGxb1Wi5!Ib&Otu9j(W*OIYg>;0;@$zAI$Cy1};nr@_H}LH`M)0b4gGZY;KfAR1iOThWiS|qi8NXf?+W6PktkQ{}zIDc%+^@XVuPyD{FCDXsPZ#1X Gp8)`DiMuHP diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP8_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP8_TLS.der deleted file mode 100644 index 93a0cc0e183cb5125b49f9b732a47f9b486484c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 646 zcmXqLVrnvIV$xi|%*4pV#KbwnfQyYotIgw_EekV~fs3Jl0UsN4C<`+Wdq`?=iGs7I zp`ZajNQ9e*Bg8d0#Mw0{1g40Khdn4b03>S2ZNLdq#3sz-8DbzO&TC|7U}|V+U}0or zXb>gNYh+|#W@rrM8W!-48IfRH-H@_s|wyP!3OdmX=N4(1F;5?uZ~4~e|Am1 z7PD_hm&L+^vJG6VhYd7A3Ith{3>2Dh2S^8Vhq;hJB1ow`OO!!`fz|?z1!`?786_nJ zR{HwMnR=kGL=HlI=O7IDivD#m1r4=5fxJg_+5~#ZbV2kBvE$g_(yvB(=Ci!P(PL z(10H#!p*}G;u;*{>>3mTQ^du?9uyn^5;f#D-~=gR6K3)ZF_07IH8M0XH8eD^Ftju= zjuPiJGBPkTG=_2wj174WxIxOfcvu{reO(Qe3=}|IW**6s)Z&uj+?0ZXoYG=Lz0``t z+=84`z2y8{gD3+LHg>Q>nHbqPwN;pznK&33*_oXfSbUVu+|cP|Qv7)%#JcDq`<|13 zCxl$fDLwGIWcl9b2G0}wzD28Z$2C2%s?6_w$GyJm_v^0Uq~I#1zp2OPIZ9@4UEIVd zWzfVZ2KKrvKO^IR78YhE_6D%4WK}_YF#{1c4sA9@R#tXqMmUSfK!lAsR91n7*8mg% z?CFg{j7*Ho2C^VgJ{B<+5nE%<=FjubL|=NcHe;FOa(9n)-E$4(LDI@B5(Z)oA_w*q z>@u4)*Lh3&|L|~DgQrzH{@gIo1St??Q8G|y!W|+V%pK-j28kf0@+?sX5e8ZdG#03} zsbrLt6jjYMdlt7r>84O&Q6vRU3*RR{Q(Bgeg zX;i9++pZ%!cU}Ctf0y>{m#?36F7Vc1QqVbNw?)>f|8>RAlu5~}8aXzu@vJ*(e|}-u MlsjcB_WxW80Cv;Ba{vGU diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP_TLS_NIST.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP_TLS_NIST.der deleted file mode 100644 index 179c37bbf09ef7942a4583a46b0abeffd843a06e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 645 zcmXqLVrn#KV$xW^%*4pV#K>vD#m1r4=5fxJg_+5~#ZbV2kBvE$g_(yvB(=Ci!P(PL z(10H#!p*}G;u;*{>>3mTQ^du?9uyn^5;f#D-~=gR6K3)ZF_07IH8M0XH8eD^Ftji< zixTHGGBPkTG=_2wj174WxIxOfcvu{reO(Qe3=}|IW**6s)Z&uj+?0ZXoYG=Lz0``t z+=84`z2y8{gGd8mHg>Q>nHZriW@cn(c4A=BP%$Vy*Y{U`U3!M4L+z$KpVQ6-Q<7w6 zTP7NR+~mELamPQuO}=)#minsp`O;I83O1k8)HAuAU)8>O>9;jg;yD&KF-jRUF^YkG zF3Zoz_@9M^nTfpt>?m1P5MRtdgpEU+jggg=otY8NVloh6V-A&7VBs|Y`JX+#QHYU= zk=Z~NB+ADk#v-Er@8c&;iMr#>Z@)dzOj#1x8-8V_fjmfBnMJ}ttU=_fW6|E9T~n{c z?Ay_0vGAa516S)|15J-rP^gz_>XqVNBXHPlQHYvVWs{eQx+Mbu Dc<{AG diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP2_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP2_TLS.der deleted file mode 100644 index da5516c89b9efcd504e75e4083cfce0175018260..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 646 zcmXqLVrnvIV$xi|%*4pV#KbVufQyYotIgw_EekV~fs3Jl0UsN4C<`+Wdq`?=iGs7I zp`ZajNQ9e*Bg8d0#Mw0{1g40Khdn4b03>S2ZNLdq#3sz-8DbzO&TC|3U}j)pXk=n) zY!)TXYh-L-ZfFSQ8W_MM|*D#yzI52lGLuNQoO;_k9pnV+^wbZ-_CZsOW>SMFmMDV=1FZ!b3)I?FGD=Dc ztn~GhGxb1Wi5!Ib&Otu9j(W*OIYg>;0;@$zAI$Cy1};nrQnx=W2xd8DrY#&^Cq3Ct zUFSB-qe)X43j6xQEXs=i&Sg@_yW+}kH01_&l~UUd$ub`s&%Omh(UY8$CSTdWRFx%m F4FEpuwMzg1 diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP4_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP4_TLS.der deleted file mode 100644 index b1c222c82f34943a6c07d08a9962af054f937c6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 647 zcmXqLVrn*MV$xi|%*4pV#KbwpfQyYotIgw_EekV~fs3Jl0UsN4C<`+Wdq`?=iGs7I zp`ZajNQ9e*Bg8d0#Mw0{1g40Khdn4b03>S2ZNLdq#3sz-8DbzO&TC|3U}j)pXk=n) zVjLyTYh-L-ZfFSQ8W<0J+cnwR)>Ery?)EDYV1BW`M4v;LX;tmA#Z8P-22G4& zV4utKGcx{XVPR%sZvZ<=Ru#k-GZ10p&}L&~Wo2h(gtM3oMA(=^WffR>4M6^9Pj3`r zWMX7CkOhhIv52vV2=m)kZ{Co))+VB&Y|rlOmcO1=s}1Bq(#k9n24W2&Umc6~{_L81 zEoR@2E{lZ+WgEC!4;yHL6bP~?87MU24v-G!4s$+(M37Q>mMDV=1FZ!b3)I?FGD=Dc ztn~GhGxb1Wi5!Ib&Otu9j(W*OIYg>;0;@$zAIu&M2ChsB!44lEOcmqsk#lHmFWxKq znW22ymF*(0Hv6xN&K0;L|AI-8VV%eti!wLSc8?q1o_+Bv(O0+odt$psoOPGQy!9y) HCtm>o&pf%B diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP8_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP8_TLS.der deleted file mode 100644 index 638e4a1fc2f8db7937baecafdc988068a2034e14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 648 zcmXqLVrnsHV$xi|%*4pV#KbwnfQyYotIgw_EekV~fs3Jl0UsN4C<`+Wdq`?=iGs7I zp`ZajNQ9e*Bg8d0#Mw0{1g40Khdn4b03>S2ZNLdq#3sz-8DbzO&TC|3U}j)pXk=n) zY7!;RYh-L-ZfFSQ8W!-48IfRH-H@_s|wyP!3OdmX=N4(1F;5?uZ~4~e|Am1 z7PD_hm&L+^vJG6VhYd7A3Ith{3>2Dh2S^8Vhq;hJB1ow`OO!!`fz|?z1!`?786_nJ zR{HwMnR=kGL=HlI=O7~q}q H;mdOX%pAF% diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_BRP.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_BRP.der deleted file mode 100644 index 6746cbb1b6c7fd4b6eebff1dcf9816804d0487a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 648 zcmXqLVrnsHV$xi|%*4pV#K>vD#m1r4=5fxJg_+5~#ZbV2kBvE$g_(yvB(=Ci!P(PL z(10H#!p*}G;u;*{>>3mTQ^du?9uyn^5;f#D-~=gR6K3)ZF_07IH8L_VGq5l;GBGtW zixTHGGBz+bG=y>uj174WxIxOfcvu{reO(Qe3=}|IW**6s)Z&uj+?0ZXoYG=Lz0``t z+=84`z2y8{gD3+LHg>Q>nHbqPwN;pznK&33*_oXfSbUVu+|cP|Qv7)%#JcDq`<|13 zCxl$fDLwGIWcl9b2G0}wzD28Z$2C2%s?6_w$GyJm_v^0Uq~I#1zp2OPIZ9@4UEIVd zWzfVZ2KKrvKO^IR78YhE_6D%4WK}_YF#{1c4sA9@R#tXqMmUSfK!lAsR91n7*8mg% z?CFg{j7*Ho2C^VgJ{B<+5nE%<=FjubL|=NcHe;FOa(9n)-E$4(LDI@B5(Z)oA_w*q z>@u4)*Lh3&|L|~DgQrzH{@gIo1St??Q8G|y!W|+V%pK-j28kf0@+?sX5e8ZdG#03} zsbrLt6jjYMdlt7q084TQ*6d795c%I+b%#|7M zlcKsy?HpTF|Ky9)r(HE)y`|y2?tkeLCPjvs{FC%|@7x$ULwJq#yW1=O@Uw57=xZD| OKkCx&<%hX83j+X6puE5U diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_NIST.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_NIST.der deleted file mode 100644 index 6977bd324cf3fd3846bfac54074d5c942329b835..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 646 zcmXqLVrnvIV$xW^%*4pV#K>vD#m1r4=5fxJg_+5~#ZbV2kBvE$g_(yvB(=Ci!P(PL z(10H#!p*}G;u;*{>>3mTQ^du?9uyn^5;f#D-~=gR6K3)ZF_07IH8L_VGq5l;GBGhV zj1uQHGBz+bG=y>uj174WxIxOfcvu{reO(Qe3=}|IW**6s)Z&uj+?0ZXoYG=Lz0``t z+=84`z2y8{gGd8mHg>Q>nHZriW@cn(c4A=BP%$Vy*Y{U`U3!M4L+z$KpVQ6-Q<7w6 zTP7NR+~mELamPQuO}=)#minsp`O;I83O1k8)HAuAU)8>O>9;jg;yD&KF-jRUF^YkG zF3Zoz_@9M^nTfpt>?m1P5MRtdgpEU+jggg=otY8NVloh6V-A&7VBs|Y`JX+#QHYU= zk=Z~NB+ADk#v-Er@8c&;iMr#>Z@)dzOj#1x8-8V_fjmfBnMJ}ttU=_fW6|E9T~n{c z?Ay_0vGAa516S)|15JfQ|Hhfd#Y~C}f8!f$tr?atyS=S4wEg56(cFjEip?JG$S7AkR`J2V G@;m_kWxm${