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 <ewild(a)sysmocom.de>