diff --git a/utils/lpac/Config.in b/utils/lpac/Config.in index 965e94ab04..d8377907c9 100644 --- a/utils/lpac/Config.in +++ b/utils/lpac/Config.in @@ -13,4 +13,10 @@ config LPAC_WITH_AT help Compile LPAC with APDU AT Backend support. +config LPAC_WITH_UQMI + bool "Include APDU uqmi Backend support" + default y + help + Compile LPAC with APDU uqmi Backend support. + endmenu diff --git a/utils/lpac/Makefile b/utils/lpac/Makefile index bbc08a5cce..3c0512cd77 100644 --- a/utils/lpac/Makefile +++ b/utils/lpac/Makefile @@ -49,6 +49,7 @@ TARGET_CFLAGS += $(FPIC) CMAKE_OPTIONS += \ -DLPAC_WITH_APDU_PCSC=$(if $(CONFIG_LPAC_WITH_PCSC),ON,OFF) \ -DLPAC_WITH_APDU_AT=$(if $(CONFIG_LPAC_WITH_AT),ON,OFF) \ + -DLPAC_WITH_APDU_UQMI=$(if $(CONFIG_LPAC_WITH_UQMI),ON,OFF) \ -DLPAC_WITH_APDU_QMI_QRTR=OFF define Package/lpac/install diff --git a/utils/lpac/files/lpac.sh b/utils/lpac/files/lpac.sh index 1eda622244..f69dda8ff1 100644 --- a/utils/lpac/files/lpac.sh +++ b/utils/lpac/files/lpac.sh @@ -2,15 +2,12 @@ . /lib/config/uci.sh -APDU_BACKEND="$(uci_get lpac global apdu_backend at)" +APDU_BACKEND="$(uci_get lpac global apdu_backend uqmi)" APDU_DEBUG="$(uci_get lpac global apdu_debug 0)" HTTP_BACKEND="$(uci_get lpac global http_backend curl)" HTTP_DEBUG="$(uci_get lpac global http_debug 0)" -AT_DEVICE="$(uci_get lpac at device /dev/ttyUSB2)" -AT_DEBUG="$(uci_get lpac at debug 0)" - export LPAC_HTTP="$HTTP_BACKEND" if [ "$HTTP_DEBUG" -eq 1 ]; then export LIBEUICC_DEBUG_HTTP="1" @@ -21,7 +18,16 @@ if [ "$APDU_DEBUG" -eq 1 ]; then export LIBEUICC_DEBUG_APDU="1" fi -export AT_DEVICE="$AT_DEVICE" -export AT_DEBUG="$AT_DEBUG" +if [ "$APDU_BACKEND" = "at" ]; then + AT_DEVICE="$(uci_get lpac at device /dev/ttyUSB2)" + AT_DEBUG="$(uci_get lpac at debug 0)" + export AT_DEVICE="$AT_DEVICE" + export AT_DEBUG="$AT_DEBUG" +elif [ "$APDU_BACKEND" = "uqmi" ]; then + UQMI_DEV="$(uci_get lpac uqmi device /dev/cdc-wdm0)" + UQMI_DEBUG="$(uci_get lpac uqmi debug 0)" + export LPAC_QMI_DEV="$UQMI_DEV" + export LPAC_QMI_DEBUG="$UQMI_DEBUG" +fi /usr/lib/lpac "$@" diff --git a/utils/lpac/files/lpac.uci b/utils/lpac/files/lpac.uci index 9eeacd1d3c..79cae9e9c2 100644 --- a/utils/lpac/files/lpac.uci +++ b/utils/lpac/files/lpac.uci @@ -1,5 +1,5 @@ config global global - option apdu_backend 'at' + option apdu_backend 'uqmi' option http_backend 'curl' option apdu_debug '0' option http_debug '0' @@ -7,3 +7,7 @@ config global global config at at option device '/dev/ttyUSB2' option debug '0' + +config uqmi uqmi + option device '/dev/cdc-wdm0' + option debug '0' diff --git a/utils/lpac/patches/0001-driver-add-uqmi-backend.patch b/utils/lpac/patches/0001-driver-add-uqmi-backend.patch new file mode 100644 index 0000000000..656398b272 --- /dev/null +++ b/utils/lpac/patches/0001-driver-add-uqmi-backend.patch @@ -0,0 +1,347 @@ +From 96c73de212c84caa1cc2796980e762321e0acdc3 Mon Sep 17 00:00:00 2001 +From: David Bauer +Date: Wed, 27 Mar 2024 22:20:16 +0100 +Subject: [PATCH] driver: add uqmi backend + +This commit adds a backend to interface with an eUICC connected to a +compatible modem using the uqmi application. + +This allows OpenWrt to manage an eUICC including the download and +installation of SIM profiles. + +This was previously not possible with most modems, as the APDU +operations were aborted due to exceeding the timeout imposed +on the AT interface by the modem firmware. + +Tested-on: Quectel EC25 / Quectel EP06 / Quectel RG520N + +Signed-off-by: David Bauer +--- + driver/CMakeLists.txt | 3 + + driver/apdu/uqmi.c | 273 ++++++++++++++++++++++++++++++++++++++++++ + driver/apdu/uqmi.h | 7 ++ + driver/driver.c | 6 + + 4 files changed, 289 insertions(+) + create mode 100644 driver/apdu/uqmi.c + create mode 100644 driver/apdu/uqmi.h + +--- a/driver/CMakeLists.txt ++++ b/driver/CMakeLists.txt +@@ -47,6 +47,9 @@ if(LPAC_WITH_APDU_AT) + target_sources(euicc-drivers PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/apdu/at.c) + endif() + ++set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DLPAC_WITH_APDU_UQMI") ++target_sources(euicc-drivers PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/apdu/uqmi.c) ++ + if(LPAC_WITH_APDU_GBINDER) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DLPAC_WITH_APDU_GBINDER") + target_sources(euicc-drivers PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/apdu/gbinder_hidl.c) +--- /dev/null ++++ b/driver/apdu/uqmi.c +@@ -0,0 +1,273 @@ ++// SPDX-License-Identifier: MIT ++/* Copyright (c) 2024 David Bauer */ ++ ++#include "at.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++ ++static FILE *fuart; ++static int client_id = 0; ++static int logic_channel = 0; ++static char *devpath = NULL; ++ ++ ++static int uqmi_execute_command(const char *command, char *buf, size_t bufsize) ++{ ++ char *debug_str = getenv("LPAC_QMI_DEBUG"); ++ char final_command[2048] = {}; ++ FILE *fp; ++ char *remaining; ++ int debug = debug_str ? atoi(debug_str) : 0; ++ ++ if (debug) ++ printf("UQMI_DEBUG_TX: %s\n", command); ++ ++ if (snprintf(final_command, sizeof(final_command), "uqmi -s -d %s %s", devpath, command) >= sizeof(final_command)) ++ { ++ fprintf(stderr, "Command too long\n"); ++ return -1; ++ } ++ ++ fp = popen(final_command, "r"); ++ if (fp == NULL) ++ { ++ fprintf(stderr, "Failed to execute command: %s\n", command); ++ return -1; ++ } ++ ++ remaining = buf; ++ if (buf) { ++ while (fgets(remaining, bufsize - (remaining - buf), fp) != NULL) ++ { ++ /* Read to buffer */ ++ } ++ if (debug) ++ printf("UQMI_DEBUG_RX: %s", buf); ++ } ++ ++ pclose(fp); ++ return 0; ++} ++ ++static int uqmi_open_client() ++{ ++ char buffer[2048] = {}; ++ int ret; ++ ++ ret = uqmi_execute_command("uqmi -s -d /dev/cdc-wdm0 --get-client-id uim", buffer, sizeof(buffer)); ++ if (ret) ++ { ++ return -1; ++ } ++ ++ client_id = atoi(buffer); ++ if (client_id == 0) ++ { ++ return -1; ++ } ++ ++ return 0; ++} ++ ++static int apdu_interface_connect(struct euicc_ctx *ctx) ++{ ++ const char *device; ++ ++ client_id = 0; ++ logic_channel = 0; ++ devpath = getenv("LPAC_QMI_DEV"); ++ ++ return uqmi_open_client(); ++} ++ ++static void apdu_interface_disconnect(struct euicc_ctx *ctx) ++{ ++ char command[64] = {}; ++ ++ snprintf(command, sizeof(command), "--set-client-id uim,%d --release-client-id uim", client_id); ++ uqmi_execute_command(command, NULL, 0); ++ ++ client_id = 0; ++ logic_channel = 0; ++} ++ ++static int apdu_interface_transmit(struct euicc_ctx *ctx, uint8_t **rx, uint32_t *rx_len, const uint8_t *tx, uint32_t tx_len) ++{ ++ cJSON *root = NULL, *jtmp = NULL; ++ char cmd[2048] = {}; ++ char buf[2048] = {}; ++ char *json; ++ ++ *rx = NULL; ++ *rx_len = 0; ++ ++ if (!client_id) ++ { ++ return -1; ++ } ++ ++ if (!logic_channel) ++ { ++ return -1; ++ } ++ ++ for (int i = 0; i < tx_len; i++) ++ { ++ snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "%02X", (uint8_t)(tx[i] & 0xFF)); ++ } ++ ++ snprintf(cmd, sizeof(cmd), "--set-client-id uim,%d --keep-client-id uim --uim-slot 1 --uim-channel-id %d --uim-apdu-send \"%s\"", ++ client_id, logic_channel, buf); ++ uqmi_execute_command(cmd, buf, sizeof(buf)); ++ ++ json = strstr(buf, "{"); ++ if (!json) ++ { ++ printf("No JSON found\n"); ++ goto fail; ++ } ++ ++ root = cJSON_Parse(json); ++ if (!root) ++ { ++ printf("Failed to parse JSON\n"); ++ goto fail; ++ } ++ ++ jtmp = cJSON_GetObjectItem(root, "response"); ++ if (!jtmp || !cJSON_IsString(jtmp)) ++ { ++ printf("Failed to get response\n"); ++ goto fail; ++ } ++ ++ *rx_len = strlen(jtmp->valuestring) / 2; ++ *rx = malloc(*rx_len); ++ if (euicc_hexutil_hex2bin_r(*rx, *rx_len, jtmp->valuestring, strlen(jtmp->valuestring)) < 0) ++ { ++ printf("Failed to convert hex '%s' to binary\n", jtmp->valuestring); ++ goto fail; ++ } ++ ++ cJSON_Delete(root); ++ return 0; ++fail: ++ if (root) ++ { ++ cJSON_Delete(root); ++ } ++ ++ if (*rx) ++ { ++ free(*rx); ++ } ++ *rx_len = 0; ++ return -1; ++} ++ ++static int apdu_interface_logic_channel_open(struct euicc_ctx *ctx, const uint8_t *aid, uint8_t aid_len) ++{ ++ char *response, *json; ++ char buf[2048] = {}; ++ char cmd[2048] = {}; ++ cJSON *root, *jtmp; ++ ++ if (!client_id) ++ { ++ return -1; ++ } ++ ++ if (logic_channel) ++ { ++ return logic_channel; ++ } ++ ++ snprintf(cmd, sizeof(cmd), "--set-client-id uim,%d --keep-client-id uim --uim-slot 1 --uim-channel-open ", client_id); ++ for (int i = 0; i < aid_len; i++) ++ { ++ snprintf(cmd + strlen(cmd), sizeof(cmd) - strlen(cmd), "%02X", (uint8_t)(aid[i] & 0xFF)); ++ } ++ ++ uqmi_execute_command(cmd, buf, sizeof(buf)); ++ ++ json = strstr(buf, "{"); ++ if (!json) ++ { ++ printf("No JSON found\n"); ++ return -1; ++ } ++ ++ root = cJSON_Parse(json); ++ if (!root) ++ { ++ printf("Failed to parse JSON\n"); ++ return -1; ++ } ++ ++ jtmp = cJSON_GetObjectItem(root, "channel_id"); ++ if (!jtmp || !cJSON_IsNumber(jtmp)) ++ { ++ printf("Failed to get channel_id\n"); ++ cJSON_Delete(root); ++ return -1; ++ } ++ ++ logic_channel = jtmp->valueint; ++ ++ cJSON_Delete(root); ++ return logic_channel; ++} ++ ++static void apdu_interface_logic_channel_close(struct euicc_ctx *ctx, uint8_t channel) ++{ ++ char cmd[2048] = {}; ++ ++ if (!logic_channel) ++ { ++ return; ++ } ++ ++ snprintf(cmd, sizeof(cmd), "--set-client-id uim,%d --keep-client-id uim --uim-slot 1 --uim-channel-id %d --uim-channel-close", client_id, logic_channel); ++ uqmi_execute_command(cmd, NULL, 0); ++ logic_channel = 0; ++ return; ++} ++ ++static int libapduinterface_init(struct euicc_apdu_interface *ifstruct) ++{ ++ memset(ifstruct, 0, sizeof(struct euicc_apdu_interface)); ++ ++ ifstruct->connect = apdu_interface_connect; ++ ifstruct->disconnect = apdu_interface_disconnect; ++ ifstruct->logic_channel_open = apdu_interface_logic_channel_open; ++ ifstruct->logic_channel_close = apdu_interface_logic_channel_close; ++ ifstruct->transmit = apdu_interface_transmit; ++ ++ return 0; ++} ++ ++static int libapduinterface_main(int argc, char **argv) ++{ ++ return 0; ++} ++ ++static void libapduinterface_fini(void) ++{ ++} ++ ++const struct euicc_driver driver_apdu_uqmi = { ++ .type = DRIVER_APDU, ++ .name = "uqmi", ++ .init = (int (*)(void *))libapduinterface_init, ++ .main = libapduinterface_main, ++ .fini = libapduinterface_fini, ++}; +--- /dev/null ++++ b/driver/apdu/uqmi.h +@@ -0,0 +1,7 @@ ++// SPDX-License-Identifier: MIT ++/* Copyright (c) 2024 David Bauer */ ++ ++#pragma once ++#include ++ ++extern const struct euicc_driver driver_apdu_uqmi; +--- a/driver/driver.c ++++ b/driver/driver.c +@@ -22,6 +22,9 @@ + #ifdef LPAC_WITH_APDU_AT + #include "driver/apdu/at.h" + #endif ++#ifdef LPAC_WITH_APDU_UQMI ++#include "driver/apdu/uqmi.h" ++#endif + #ifdef LPAC_WITH_HTTP_CURL + #include "driver/http/curl.h" + #endif +@@ -44,6 +47,9 @@ static const struct euicc_driver *driver + #ifdef LPAC_WITH_APDU_AT + &driver_apdu_at, + #endif ++#ifdef LPAC_WITH_APDU_UQMI ++ &driver_apdu_uqmi, ++#endif + #ifdef LPAC_WITH_HTTP_CURL + &driver_http_curl, + #endif