Hoernchen has uploaded this change for review. ( 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(-) git pull ssh://gerrit.osmocom.org:29418/osmo-ttcn3-hacks refs/changes/21/41121/1 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 bca2261..0d1eab9 100644 --- a/smdpp/smdpp_Tests.ttcn +++ b/smdpp/smdpp_Tests.ttcn @@ -233,6 +233,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"; @@ -336,6 +354,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, @@ -352,7 +375,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 { @@ -372,7 +396,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; } @@ -920,16 +945,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)); @@ -937,11 +1008,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) @@ -3055,6 +3244,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 & " ==="); @@ -5638,6 +5833,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: newchange Gerrit-Project: osmo-ttcn3-hacks Gerrit-Branch: master Gerrit-Change-Id: I1d824931bd6513d2320ba30df0f8193cd8352863 Gerrit-Change-Number: 41121 Gerrit-PatchSet: 1 Gerrit-Owner: Hoernchen <ew...@sysmocom.de>