From bf27f3ed769cc3ea235d3eb25728f38fb7aa47bf Mon Sep 17 00:00:00 2001 From: cybersnakeh Date: Thu, 4 Jun 2026 18:18:46 +0200 Subject: [PATCH] driver: harden update flow and ABI integration --- README.md | 4 +- deploy.sh | 283 ++++++++---- kernel/snakedrv_injector.c | 94 +++- kernel/snakedrv_main.c | 416 +++++++++++++---- kernel/snakedrv_scanner.c | 1 + kernel/snakedrv_scanner.h | 2 + packaging/cmake/SnakeDrvConfig.cmake.in | 87 ++++ .../polkit/com.snakeengine.driver.policy | 38 ++ packaging/snakedrv-updater | 329 +++++++++++++ security/99-snakedrv.rules | 36 +- setup.sh | 249 ++++++++++ tools/sigstore-verify/.gitignore | 1 + tools/sigstore-verify/Makefile | 35 ++ tools/sigstore-verify/README.md | 75 +++ tools/sigstore-verify/go.mod | 74 +++ tools/sigstore-verify/go.sum | 378 +++++++++++++++ tools/sigstore-verify/main.go | 268 +++++++++++ userland/CMakeLists.txt | 33 +- userland/Makefile | 161 ++++++- userland/include/libsnakedrv.hpp | 6 +- userland/include/snakedrv.h | 35 +- userland/include/snakedrv_scanner.h | 18 +- userland/selftests/manual_map_payload.cpp | 4 + userland/selftests/test_ioctl_abi.cpp | 64 +++ userland/selftests/test_manualmap_elf.cpp | 26 ++ userland/src/libsnakedrv.cpp | 76 ++- userland/src/snakedrv_injector.cpp | 433 +++++++++++------- 27 files changed, 2784 insertions(+), 442 deletions(-) create mode 100644 packaging/cmake/SnakeDrvConfig.cmake.in create mode 100644 packaging/polkit/com.snakeengine.driver.policy create mode 100755 packaging/snakedrv-updater mode change 100755 => 100644 security/99-snakedrv.rules create mode 100755 setup.sh create mode 100644 tools/sigstore-verify/.gitignore create mode 100644 tools/sigstore-verify/Makefile create mode 100644 tools/sigstore-verify/README.md create mode 100644 tools/sigstore-verify/go.mod create mode 100644 tools/sigstore-verify/go.sum create mode 100644 tools/sigstore-verify/main.go create mode 100644 userland/selftests/manual_map_payload.cpp create mode 100644 userland/selftests/test_ioctl_abi.cpp create mode 100644 userland/selftests/test_manualmap_elf.cpp diff --git a/README.md b/README.md index f0929b9..fd7045d 100755 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ sudo dlopen_inject /path/to/payload.so sudo ./deploy.sh inject /path/to/payload.so ``` -The payload must export a `ManualMapEntry(void*)` function. After injection, the `.so` VMAs are automatically hidden from `/proc/pid/maps`. +The payload must export a `ManualMapEntry(void*)` function. Manual-map stealth is intentionally disabled for now: hiding the VMA before the payload has faulted all pages can crash the target on later page faults. ### ImGui overlay example @@ -264,7 +264,7 @@ sign-module.sh Secure Boot module signing (MOK) - `sign-module.sh`: cross-distro `sign-file` detection (Fedora + Debian/Ubuntu) ### 1.2 (2026-01-15) -- Stealth manual map injector pipeline (alloc, relocate, write, VMA unlinking) +- Manual map injector pipeline (alloc, relocate, write); VMA stealth remains disabled until deferred page-fault-safe hiding is implemented - Improved remote symbol resolution and IFUNC handling for glibc - GitHub Actions CI build and release workflows - Documentation and wiki refresh diff --git a/deploy.sh b/deploy.sh index f47463f..4ac2974 100755 --- a/deploy.sh +++ b/deploy.sh @@ -68,6 +68,7 @@ fi BINDIR="${PREFIX}/bin" LIBDIR="${PREFIX}/lib" INCLUDEDIR="${PREFIX}/include" +SYSTEM_HELPER_DIR="/usr/lib/snakeengine" SYSCONFDIR="/etc" MODULEDIR="/lib/modules/$(uname -r)" DKMS_SRC="/usr/src/${MODULE_NAME}-${VERSION}" @@ -137,6 +138,68 @@ check_command() { return 0 } +module_loaded() { + [ -d "/sys/module/${MODULE_NAME}" ] +} + +expected_driver_abi() { + awk '/^[[:space:]]*#define[[:space:]]+SNAKEDRV_ABI_VERSION/{print $3}' \ + "${SCRIPT_DIR}/userland/include/snakedrv.h" +} + +reload_udev_for_driver() { + if ! check_command udevadm; then + die "udevadm is required to apply /dev/${MODULE_NAME} rules" + fi + + udevadm control --reload-rules || die "failed to reload udev rules" + udevadm trigger --subsystem-match="${MODULE_NAME}" --action=add 2>/dev/null || true + udevadm settle --timeout=5 2>/dev/null || true +} + +verify_loaded_driver() { + local expected_abi loaded_abi i + + expected_abi="$(expected_driver_abi)" + [ -n "${expected_abi}" ] || die "could not parse expected driver ABI" + + [ -d "/sys/module/${MODULE_NAME}" ] \ + || die "${MODULE_NAME} module is not loaded" + + [ -r "/sys/module/${MODULE_NAME}/parameters/abi_version" ] \ + || die "${MODULE_NAME} does not expose abi_version" + + loaded_abi="$(tr -d '[:space:]' < "/sys/module/${MODULE_NAME}/parameters/abi_version")" + [ "${loaded_abi}" = "${expected_abi}" ] \ + || die "${MODULE_NAME} ABI mismatch: loaded=${loaded_abi}, expected=${expected_abi}" + + for i in 1 2 3 4 5; do + [ -c "/dev/${MODULE_NAME}" ] && break + reload_udev_for_driver + sleep 1 + done + + [ -c "/dev/${MODULE_NAME}" ] \ + || die "/dev/${MODULE_NAME} was not created; check devtmpfs/udev and /sys/class/${MODULE_NAME}/${MODULE_NAME}/dev" + + log_success "Runtime driver verified: /dev/${MODULE_NAME}, ABI ${loaded_abi}" +} + +load_installed_module() { + log_info "Loading installed ${MODULE_NAME} module..." + + if module_loaded; then + log_info "Reloading currently loaded ${MODULE_NAME} module..." + rmmod "${MODULE_NAME}" \ + || die "could not unload ${MODULE_NAME}; close SnakeEngine/driver clients and retry" + fi + + modprobe "${MODULE_NAME}" \ + || die "modprobe ${MODULE_NAME} failed; check Secure Boot, module signing, and DKMS logs" + + verify_loaded_driver +} + # ============================================================================ # Dependency Management # ============================================================================ @@ -169,6 +232,7 @@ install_dependencies_debian() { llvm \ cmake \ git \ + golang-go \ libsdl2-dev \ libglew-dev \ libgl-dev @@ -186,6 +250,7 @@ install_dependencies_fedora() { clang \ llvm \ cmake \ + golang \ elfutils-libelf-devel \ SDL2-devel \ glew-devel \ @@ -203,6 +268,7 @@ install_dependencies_arch() { clang \ llvm \ cmake \ + go \ sdl2 \ glew } @@ -248,6 +314,7 @@ check_dependencies() { check_command gcc || missing+=("gcc") check_command make || missing+=("make") check_command clang || missing+=("clang") + check_command go || missing+=("go") # Check DKMS if needed if [ "${USE_DKMS}" -eq 1 ]; then @@ -360,6 +427,20 @@ build_payload() { log_success "ImGui payload built successfully" } +build_sigstore_verify() { + log_info "Building sigstore verifier..." + + cd "${SCRIPT_DIR}/tools/sigstore-verify" || die "sigstore-verify dir not found" + make VERSION="${VERSION}" + + if [ ! -x "sigstore-verify" ]; then + die "Failed to build sigstore-verify" + fi + + log_success "sigstore verifier built successfully" + log_info "Verifier size: $(du -h "sigstore-verify" | cut -f1)" +} + build_all() { log_info "=== Building SnakeEngine Driver v${VERSION} ===" @@ -367,6 +448,7 @@ build_all() { build_userland build_tests build_payload + build_sigstore_verify log_success "=== Build completed successfully ===" } @@ -403,12 +485,14 @@ install_module_direct() { install_dkms() { log_info "Installing with DKMS..." - # Remove old version if exists - if dkms status | grep -q "${MODULE_NAME}" || true; then - if dkms status 2>/dev/null | grep -q "${MODULE_NAME}"; then - log_info "Removing old DKMS installation..." - dkms remove -m "${MODULE_NAME}" -v "${VERSION}" --all 2>/dev/null || true - fi + # Remove any existing registration for this exact module/version. DKMS can + # leave an "added" tree behind that does not show reliably in `dkms status`, + # but still makes `dkms add` fail with "tree already contains". + log_info "Removing old DKMS installation if present..." + dkms remove -m "${MODULE_NAME}" -v "${VERSION}" --all 2>/dev/null || true + if [ -d "/var/lib/dkms/${MODULE_NAME}/${VERSION}" ]; then + log_warning "Removing stale DKMS tree: /var/lib/dkms/${MODULE_NAME}/${VERSION}" + rm -rf "/var/lib/dkms/${MODULE_NAME:?}/${VERSION:?}" fi # Copy source to DKMS directory @@ -416,7 +500,12 @@ install_dkms() { mkdir -p "${DKMS_SRC}" cp -r "${SCRIPT_DIR}/kernel" "${DKMS_SRC}/" cp -r "${SCRIPT_DIR}/userland" "${DKMS_SRC}/" - cp "${SCRIPT_DIR}/dkms/dkms.conf" "${DKMS_SRC}/" + + # Render dkms.conf with the current VERSION so /usr/src/snakedrv-X + # and PACKAGE_VERSION inside it never drift. + sed "s|@SNAKEDRV_VERSION@|${VERSION}|g" \ + "${SCRIPT_DIR}/packaging/dkms/dkms.conf.in" \ + > "${DKMS_SRC}/dkms.conf" # Add to DKMS dkms add -m "${MODULE_NAME}" -v "${VERSION}" @@ -433,67 +522,58 @@ install_dkms() { install_userland() { log_info "Installing userland library..." - # Create directories - mkdir -p "${INCLUDEDIR}/snakeengine" - mkdir -p "${BINDIR}" - mkdir -p "${LIBDIR}" - - # Install headers - log_info "Installing headers to ${INCLUDEDIR}/snakeengine/" - cp "${SCRIPT_DIR}/userland/include/snakedrv.h" "${INCLUDEDIR}/snakeengine/" - cp "${SCRIPT_DIR}/userland/include/snakedrv_scanner.h" "${INCLUDEDIR}/snakeengine/" 2>/dev/null || true - cp "${SCRIPT_DIR}/userland/include/libsnakedrv.hpp" "${INCLUDEDIR}/snakeengine/" - cp "${SCRIPT_DIR}/userland/include/libsnakedrv_scanner.hpp" "${INCLUDEDIR}/snakeengine/" 2>/dev/null || true - cp "${SCRIPT_DIR}/userland/include/snakedrv_elf.hpp" "${INCLUDEDIR}/snakeengine/" - - # Install library (built in userland/ directory) - log_info "Installing library to ${LIBDIR}/" - if [ -f "${SCRIPT_DIR}/userland/libsnakedrv.so" ]; then - cp "${SCRIPT_DIR}/userland/libsnakedrv.so" "${LIBDIR}/" - log_info "Installed: libsnakedrv.so ($(du -h "${SCRIPT_DIR}/userland/libsnakedrv.so" | cut -f1))" - else - log_warning "libsnakedrv.so not found in userland/" - fi + # Delegate the libsnakedrv install (lib + headers + pkg-config + + # CMake config) to the userland Makefile, which is the single source + # of truth for install layout. PREFIX/LIBDIR/INCLUDEDIR are passed + # through so deploy.sh and bare `make install` produce identical + # results. + make -C "${SCRIPT_DIR}/userland" install \ + PREFIX="${PREFIX}" \ + LIBDIR="${LIBDIR}" \ + INCLUDEDIR="${INCLUDEDIR}" \ + || die "userland install failed" + + # Test-suite extras (dlopen injection helpers) are built and shipped + # by the tests/ Makefile but installed here for end-user convenience. + mkdir -p "${BINDIR}" "${LIBDIR}" - # Install dlopen injection payload library if [ -f "${SCRIPT_DIR}/tests/libpayload_dlopen.so" ]; then - cp "${SCRIPT_DIR}/tests/libpayload_dlopen.so" "${LIBDIR}/" + install -m 0644 "${SCRIPT_DIR}/tests/libpayload_dlopen.so" "${LIBDIR}/" log_info "Installed: libpayload_dlopen.so to ${LIBDIR}/" else log_warning "libpayload_dlopen.so not found in tests/ (build tests first)" fi - # Install dlopen_inject binary if [ -f "${SCRIPT_DIR}/tests/dlopen_inject" ]; then - cp "${SCRIPT_DIR}/tests/dlopen_inject" "${BINDIR}/" - chmod 755 "${BINDIR}/dlopen_inject" + install -m 0755 "${SCRIPT_DIR}/tests/dlopen_inject" "${BINDIR}/" log_info "Installed: dlopen_inject to ${BINDIR}/" else log_warning "dlopen_inject not found in tests/ (build tests first)" fi - # Update library cache + # Refresh the dynamic loader cache so consumers can dlopen() the + # library without LD_LIBRARY_PATH gymnastics. ldconfig log_success "Userland library installed" - log_info "Headers available at: ${INCLUDEDIR}/snakeengine/" - log_info "Library available at: ${LIBDIR}/libsnakedrv.so" + log_info "Headers: ${INCLUDEDIR}/snakedrv/" + log_info "Library: ${LIBDIR}/libsnakedrv.so" + log_info "pkg-config: ${LIBDIR}/pkgconfig/snakedrv.pc" + log_info "CMake: ${LIBDIR}/cmake/SnakeDrv/SnakeDrvConfig.cmake" } install_udev_rules() { log_info "Installing udev rules..." - cp "${SCRIPT_DIR}/security/99-snakedrv.rules" /etc/udev/rules.d/ - # Create group if doesn't exist if ! getent group snakeengine > /dev/null; then - groupadd snakeengine - log_info "Created group 'snakeengine'" + groupadd --system snakeengine + log_info "Created system group 'snakeengine'" fi - # Reload udev rules - udevadm control --reload-rules - udevadm trigger + install -m 0644 "${SCRIPT_DIR}/security/99-snakedrv.rules" /etc/udev/rules.d/99-snakedrv.rules + + reload_udev_for_driver log_success "udev rules installed" } @@ -583,6 +663,25 @@ EOF log_success "modprobe configuration installed" } +install_update_tools() { + log_info "Installing update tools..." + + local verifier="${SCRIPT_DIR}/tools/sigstore-verify/sigstore-verify" + if [ ! -x "${verifier}" ]; then + build_sigstore_verify + fi + + install -d "${SYSTEM_HELPER_DIR}" + install -m 0755 "${SCRIPT_DIR}/packaging/snakedrv-updater" \ + "${SYSTEM_HELPER_DIR}/snakedrv-updater" + install -m 0755 "${verifier}" \ + "${SYSTEM_HELPER_DIR}/sigstore-verify" + + log_success "Update tools installed" + log_info "Updater: ${SYSTEM_HELPER_DIR}/snakedrv-updater" + log_info "Verifier: ${SYSTEM_HELPER_DIR}/sigstore-verify" +} + install_all() { check_root @@ -599,17 +698,16 @@ install_all() { # Install components install_kernel_module - install_userland install_udev_rules install_modprobe_config + load_installed_module + install_userland + install_update_tools install_selinux install_apparmor log_success "=== Installation completed successfully ===" log_info "" - log_info "To load the module now, run:" - log_info " sudo modprobe ${MODULE_NAME}" - log_info "" log_info "To add yourself to the snakeengine group:" log_info " sudo usermod -aG snakeengine \$USER" log_info "" @@ -625,11 +723,9 @@ uninstall_all() { log_info "=== Uninstalling SnakeEngine Driver ===" # Unload module if loaded - if lsmod | grep -q "^${MODULE_NAME}" || true; then - if lsmod 2>/dev/null | grep -q "^${MODULE_NAME}"; then - log_info "Unloading module..." - rmmod "${MODULE_NAME}" || true - fi + if module_loaded; then + log_info "Unloading module..." + rmmod "${MODULE_NAME}" || true fi # Remove DKMS @@ -649,14 +745,22 @@ uninstall_all() { # Remove DKMS source rm -rf "${DKMS_SRC}" - # Remove userland - rm -rf "${LIBDIR}/${PROJECT_NAME}" - rm -rf "${INCLUDEDIR}/${PROJECT_NAME}" - rm -rf "${INCLUDEDIR}/snakeengine" - rm -f "${LIBDIR}/libsnakedrv.so" - rm -f "${LIBDIR}/libsnakedrv.a" - rm -f "${LIBDIR}/libpayload_dlopen.so" - rm -f "${BINDIR}/dlopen_inject" + # Remove userland (current layout: /usr/include/snakedrv, + # /usr/lib/{libsnakedrv.so, pkgconfig/snakedrv.pc, cmake/SnakeDrv}). + # Also clean up legacy paths from older releases. + rm -rf "${INCLUDEDIR}/snakedrv" + rm -rf "${INCLUDEDIR}/snakeengine" # legacy + rm -rf "${LIBDIR}/${PROJECT_NAME}" # legacy + rm -rf "${INCLUDEDIR}/${PROJECT_NAME}" # legacy + rm -rf "${LIBDIR}/cmake/SnakeDrv" + rm -f "${LIBDIR}/pkgconfig/snakedrv.pc" + rm -f "${LIBDIR}/libsnakedrv.so" + rm -f "${LIBDIR}/libsnakedrv.a" + rm -f "${LIBDIR}/libpayload_dlopen.so" + rm -f "${BINDIR}/dlopen_inject" + rm -f "${SYSTEM_HELPER_DIR}/snakedrv-updater" + rm -f "${SYSTEM_HELPER_DIR}/sigstore-verify" + rmdir "${SYSTEM_HELPER_DIR}" 2>/dev/null || true # Remove configs rm -f /etc/modprobe.d/snakedrv.conf @@ -684,11 +788,10 @@ uninstall_all() { load_module() { check_root - if lsmod | grep -q "^${MODULE_NAME}" || true; then - if lsmod 2>/dev/null | grep -q "^${MODULE_NAME}"; then - log_info "Module already loaded" - return - fi + if module_loaded; then + log_info "Module already loaded" + verify_loaded_driver + return fi log_info "Loading module..." @@ -699,20 +802,13 @@ load_module() { modprobe "${MODULE_NAME}" fi - # Wait for device - sleep 1 - - if [ -e "/dev/${MODULE_NAME}" ]; then - log_success "Module loaded, device: /dev/${MODULE_NAME}" - else - log_warning "Module loaded but device not created" - fi + verify_loaded_driver } unload_module() { check_root - if ! lsmod | grep -q "^${MODULE_NAME}"; then + if ! module_loaded; then log_info "Module not loaded" return fi @@ -736,7 +832,7 @@ run_tests() { fi # Check that module is loaded - if ! lsmod | grep -q "^${MODULE_NAME}" 2>/dev/null; then + if ! module_loaded; then log_warning "Kernel module is not loaded. Load it first: sudo ./deploy.sh load" fi @@ -806,10 +902,19 @@ show_status() { # Module status echo "Kernel Module:" - if lsmod | grep -q "^${MODULE_NAME}" 2>/dev/null; then + if module_loaded; then echo " Status: LOADED" + if [ -r "/sys/module/${MODULE_NAME}/parameters/abi_version" ]; then + echo " ABI: $(cat "/sys/module/${MODULE_NAME}/parameters/abi_version")" + fi echo " Info:" - lsmod | grep "^${MODULE_NAME}" | awk '{print " Size: "$2" bytes, Used by: "$3}' || true + if lsmod 2>/dev/null | grep -q "^${MODULE_NAME}"; then + lsmod | grep "^${MODULE_NAME}" | awk '{print " Size: "$2" bytes, Used by: "$3}' || true + elif [ -r "/sys/module/${MODULE_NAME}/initstate" ]; then + echo " Init state: $(cat "/sys/module/${MODULE_NAME}/initstate")" + else + echo " Present in /sys/module/${MODULE_NAME}" + fi else echo " Status: NOT LOADED" fi @@ -829,9 +934,9 @@ show_status() { echo "Shadow Memory:" if [ -f "/sys/module/${MODULE_NAME}/parameters/shadow_alloc_count" ]; then echo " Allocations: $(cat "/sys/module/${MODULE_NAME}/parameters/shadow_alloc_count")" - elif [ -e "/dev/${MODULE_NAME}" ]; then + elif module_loaded; then local SHADOW_COUNT - SHADOW_COUNT=$(dmesg | grep -c "snakedrv.*shadow" 2>/dev/null || true) + SHADOW_COUNT=$(dmesg 2>/dev/null | grep -c "snakedrv.*shadow" || true) echo " Recent shadow operations in dmesg: ${SHADOW_COUNT:-0}" else echo " N/A (module not loaded)" @@ -842,9 +947,9 @@ show_status() { echo "Attached Processes:" if [ -f "/sys/module/${MODULE_NAME}/parameters/attached_count" ]; then echo " Count: $(cat "/sys/module/${MODULE_NAME}/parameters/attached_count")" - elif [ -e "/dev/${MODULE_NAME}" ]; then + elif module_loaded; then local ATTACH_COUNT - ATTACH_COUNT=$(dmesg | grep -c "snakedrv.*attach" 2>/dev/null || true) + ATTACH_COUNT=$(dmesg 2>/dev/null | grep -c "snakedrv.*attach" || true) echo " Recent attach operations in dmesg: ${ATTACH_COUNT:-0}" else echo " N/A (module not loaded)" @@ -880,9 +985,23 @@ show_status() { fi echo "" + # Updater status + echo "Update Tools:" + if [ -x "${SYSTEM_HELPER_DIR}/snakedrv-updater" ] && \ + [ -x "${SYSTEM_HELPER_DIR}/sigstore-verify" ]; then + echo " Status: INSTALLED" + echo " Updater: ${SYSTEM_HELPER_DIR}/snakedrv-updater" + echo " Verifier: ${SYSTEM_HELPER_DIR}/sigstore-verify" + else + echo " Status: NOT INSTALLED (run: sudo ./deploy.sh install)" + [ -x "${SYSTEM_HELPER_DIR}/snakedrv-updater" ] || echo " Missing: ${SYSTEM_HELPER_DIR}/snakedrv-updater" + [ -x "${SYSTEM_HELPER_DIR}/sigstore-verify" ] || echo " Missing: ${SYSTEM_HELPER_DIR}/sigstore-verify" + fi + echo "" + # Kernel log echo "Recent kernel messages:" - dmesg | grep -i snakedrv | tail -10 || echo " No messages found" || true + dmesg 2>/dev/null | grep -i snakedrv | tail -10 || echo " No messages found" || true } # ============================================================================ diff --git a/kernel/snakedrv_injector.c b/kernel/snakedrv_injector.c index 9b6c9ea..3b3b96c 100644 --- a/kernel/snakedrv_injector.c +++ b/kernel/snakedrv_injector.c @@ -466,6 +466,29 @@ int injector_protect(struct snake_inject_protect *protect_info) return ctx.ret; } +static int injector_resume_task(struct task_struct *task) +{ + int wait_count; + int ret; + + wake_up_process(task); + ret = send_sig(SIGCONT, task, 1); + if (ret < 0) { + pr_err("snakedrv: Failed to send SIGCONT: %d\n", ret); + return ret; + } + + for (wait_count = 0; wait_count < 200; wait_count++) { + if (!task_is_stopped(task) && !task_is_traced(task)) + return 0; + msleep(5); + } + + pr_err("snakedrv: Task remained stopped after SIGCONT, state=0x%x\n", + READ_ONCE(task->__state)); + return -EAGAIN; +} + /* * Hijack an existing thread to run our payload. * @@ -518,7 +541,7 @@ int injector_create_thread(struct snake_inject_thread *thread_info) READ_ONCE(task->__state), wait_count * 5); if (!stopped) { pr_err("snakedrv: Task did not stop in time\n"); - send_sig(SIGCONT, task, 1); + injector_resume_task(task); put_task_struct(task); return -EAGAIN; } @@ -527,7 +550,7 @@ int injector_create_thread(struct snake_inject_thread *thread_info) regs = task_pt_regs(task); if (!regs) { pr_err("snakedrv: Cannot get pt_regs\n"); - send_sig(SIGCONT, task, 1); + injector_resume_task(task); put_task_struct(task); return -EINVAL; } @@ -572,7 +595,7 @@ int injector_create_thread(struct snake_inject_thread *thread_info) 1); if (written != sizeof(original_rip)) { pr_err("snakedrv: Failed to write return address (%d)\n", written); - send_sig(SIGCONT, task, 1); + injector_resume_task(task); put_task_struct(task); return -EFAULT; } @@ -598,9 +621,12 @@ int injector_create_thread(struct snake_inject_thread *thread_info) /* Memory barrier */ smp_wmb(); - /* Step 4: Resume the task */ - wake_up_process(task); - send_sig(SIGCONT, task, 1); + /* Step 4: Resume the task and fail if it remains stopped. */ + ret = injector_resume_task(task); + if (ret < 0) { + put_task_struct(task); + return ret; + } pr_info("snakedrv: Task resumed, state: 0x%x\n", READ_ONCE(task->__state)); @@ -670,6 +696,58 @@ static int shadow_mmap_worker(void *data) return 0; } +struct shadow_unmap_ctx { + struct mm_struct *mm; + unsigned long addr; + unsigned long size; + struct completion done; + int ret; +}; + +static int shadow_unmap_worker(void *data) +{ + struct shadow_unmap_ctx *ctx = data; + + kthread_use_mm(ctx->mm); + ctx->ret = vm_munmap(ctx->addr, ctx->size); + kthread_unuse_mm(ctx->mm); + + complete(&ctx->done); + return 0; +} + +static void shadow_unmap_allocation(struct mm_struct *mm, + unsigned long addr, + unsigned long size) +{ + struct shadow_unmap_ctx ctx; + struct task_struct *worker; + + if (!addr || !size) + return; + + ctx.mm = mm; + ctx.addr = addr; + ctx.size = size; + init_completion(&ctx.done); + + worker = kthread_run(shadow_unmap_worker, &ctx, "snake_sh_unmap"); + if (IS_ERR(worker)) { + pr_warn("snakedrv: shadow unmap worker failed: %ld\n", + PTR_ERR(worker)); + return; + } + + if (!wait_for_completion_timeout(&ctx.done, + msecs_to_jiffies(INJECTOR_WORKER_TIMEOUT_MS))) { + pr_warn("snakedrv: shadow unmap timed out\n"); + return; + } + + if (ctx.ret) + pr_warn("snakedrv: shadow unmap failed: %d\n", ctx.ret); +} + static struct shadow_alloc_entry * shadow_find_entry(pid_t pid, unsigned long addr) { @@ -845,7 +923,7 @@ int injector_shadow_alloc(struct snake_shadow_alloc *info) /* Tracking structure */ sa = kzalloc(sizeof(*sa), GFP_KERNEL); if (!sa) { - /* TODO: vm_munmap the allocation */ + shadow_unmap_allocation(mm, ctx.addr_out, size); mmput(mm); return -ENOMEM; } @@ -853,6 +931,7 @@ int injector_shadow_alloc(struct snake_shadow_alloc *info) sa->pages = kvmalloc_array(nr_pages, sizeof(struct page *), GFP_KERNEL | __GFP_ZERO); if (!sa->pages) { + shadow_unmap_allocation(mm, ctx.addr_out, size); kfree(sa); mmput(mm); return -ENOMEM; @@ -875,6 +954,7 @@ int injector_shadow_alloc(struct snake_shadow_alloc *info) pinned, nr_pages); for (i = 0; i < (pinned > 0 ? pinned : 0); i++) put_page(sa->pages[i]); + shadow_unmap_allocation(mm, ctx.addr_out, size); kvfree(sa->pages); kfree(sa); mmput(mm); diff --git a/kernel/snakedrv_main.c b/kernel/snakedrv_main.c index 5e710d2..8f50b00 100755 --- a/kernel/snakedrv_main.c +++ b/kernel/snakedrv_main.c @@ -55,6 +55,7 @@ #include "snakedrv_backend.h" #include "snakedrv_scanner.h" #include "snakedrv_injector.h" +#include "snakedrv_benchmark.h" /* Forward declarations for backend private data */ struct process_context { @@ -63,6 +64,16 @@ struct process_context { struct mm_struct *mm; }; +static struct snake_scan_options global_scan_options = { + .enable_parallel = 0, + .enable_bloom = 1, + .enable_prefetch = 1, + .enable_huge_pages = 1, + .default_threads = 0, + .bloom_fpr = 10, +}; +static DEFINE_MUTEX(scan_options_mutex); + MODULE_LICENSE("GPL"); MODULE_AUTHOR("SnakeEngine Project"); MODULE_DESCRIPTION("SnakeEngine Kernel Driver - Memory and Debug Operations"); @@ -98,6 +109,19 @@ static int param_event_queue_size = 256; module_param_named(event_queue_size, param_event_queue_size, int, 0644); MODULE_PARM_DESC(param_event_queue_size, "Maximum pending debug events (default: 256)"); +static int snakedrv_abi_version_get(char *buffer, const struct kernel_param *kp) +{ + (void)kp; + return scnprintf(buffer, PAGE_SIZE, "%u\n", SNAKEDRV_ABI_VERSION); +} + +static const struct kernel_param_ops snakedrv_abi_version_ops = { + .get = snakedrv_abi_version_get, +}; + +module_param_cb(abi_version, &snakedrv_abi_version_ops, NULL, 0444); +MODULE_PARM_DESC(abi_version, "SnakeEngine driver ABI version"); + /* ============================================================================ * Debug Macros * ============================================================================ */ @@ -870,190 +894,162 @@ static void bp_handler(struct perf_event *bp, -static int do_set_breakpoint(struct attached_proc *p, struct snake_hw_breakpoint *bp) - +static int validate_breakpoint_address(struct task_struct *target, + struct snake_hw_breakpoint *bp, + unsigned int bp_len_bytes) { + struct mm_struct *mm; + struct vm_area_struct *vma; + unsigned long start; + unsigned long end; + int ret = 0; - struct perf_event_attr attr; + if (bp->address != (uint64_t)(unsigned long)bp->address || + bp->address == 0) { + bp->result = SNAKEDRV_ERROR_INVALID_ARGS; + return -EINVAL; + } - struct perf_event *perf_bp; + start = (unsigned long)bp->address; + if (check_add_overflow(start, (unsigned long)bp_len_bytes, &end) || + end <= start || !IS_ALIGNED(start, bp_len_bytes)) { + bp->result = SNAKEDRV_ERROR_INVALID_ARGS; + return -EINVAL; + } - struct task_struct *target; + mm = get_task_mm(target); + if (!mm) { + bp->result = SNAKEDRV_ERROR_NO_PROCESS; + return -EINVAL; + } - int slot = -1; + mmap_read_lock(mm); + vma = find_vma(mm, start); + if (!vma || vma->vm_start > start || vma->vm_end < end) { + bp->result = SNAKEDRV_ERROR_INVALID_ARGS; + ret = -EFAULT; + } + mmap_read_unlock(mm); + mmput(mm); - int i; + return ret; +} +static int do_set_breakpoint(struct attached_proc *p, struct snake_hw_breakpoint *bp) +{ + struct perf_event_attr attr; + struct perf_event *perf_bp; + struct task_struct *target; + int slot = -1; + int i; int bp_type; - int bp_len; - - - - /* Find free slot */ + unsigned int bp_len_bytes; + int ret; for (i = 0; i < HBP_NUM; i++) { - if (!p->bp_slots[i].used) { - slot = i; - break; - } - } - - if (slot < 0) { - bp->result = SNAKEDRV_ERROR_NO_BP_SLOT; - return -ENOSPC; - } - - switch (bp->type) { - case SNAKE_BP_TYPE_EXEC: - bp_type = HW_BREAKPOINT_X; - break; - case SNAKE_BP_TYPE_WRITE: - bp_type = HW_BREAKPOINT_W; - break; - case SNAKE_BP_TYPE_RW: - bp_type = HW_BREAKPOINT_RW; - break; - default: - + bp->result = SNAKEDRV_ERROR_INVALID_ARGS; return -EINVAL; - } - - switch (bp->length) { - case SNAKE_BP_LEN_1: - bp_len = HW_BREAKPOINT_LEN_1; - + bp_len_bytes = 1; break; - case SNAKE_BP_LEN_2: - bp_len = HW_BREAKPOINT_LEN_2; - + bp_len_bytes = 2; + break; + case SNAKE_BP_LEN_4: + bp_len = HW_BREAKPOINT_LEN_4; + bp_len_bytes = 4; break; - case SNAKE_BP_LEN_8: - bp_len = HW_BREAKPOINT_LEN_8; - + bp_len_bytes = 8; break; - - case SNAKE_BP_LEN_4: - default: - - bp_len = HW_BREAKPOINT_LEN_4; - - break; - + bp->result = SNAKEDRV_ERROR_INVALID_ARGS; + return -EINVAL; } - + if (bp->type == SNAKE_BP_TYPE_EXEC && bp_len_bytes != 1) { + bp->result = SNAKEDRV_ERROR_INVALID_ARGS; + return -EINVAL; + } if (!bp->enabled) { - bp->result = SNAKEDRV_ERROR_INVALID_ARGS; - return -EINVAL; - } - - target = get_thread_task(p, bp->tid); - if (!target) { - bp->result = SNAKEDRV_ERROR_NO_PROCESS; - return -ESRCH; - } - + ret = validate_breakpoint_address(target, bp, bp_len_bytes); + if (ret) { + put_task_struct(target); + return ret; + } hw_breakpoint_init(&attr); - attr.bp_addr = bp->address; - attr.bp_len = bp_len; - attr.bp_type = bp_type; - - perf_bp = register_user_hw_breakpoint(&attr, bp_handler, NULL, target); - if (IS_ERR(perf_bp)) { - - SDRV_ERR("Failed to register BP: %ld\n", PTR_ERR(perf_bp)); - + ret = PTR_ERR(perf_bp); + bp->result = ret; + SDRV_ERR("Failed to register BP: %d\n", ret); put_task_struct(target); - - return PTR_ERR(perf_bp); - + return ret; } p->bp_slots[slot].perf_bp = perf_bp; - - - p->bp_slots[slot].used = true; - p->bp_slots[slot].id = slot + (p->pid << 8); - p->bp_slots[slot].address = bp->address; - p->bp_slots[slot].type = bp->type; - p->bp_slots[slot].length = bp_len; - p->bp_slots[slot].tid = bp->tid; - p->bp_slots[slot].enabled = true; - - bp->slot = slot; - bp->id = p->bp_slots[slot].id; - bp->result = SNAKEDRV_SUCCESS; - - - SDRV_INFO("Set BP: pid=%d slot=%d addr=0x%llx\n", p->pid, slot, bp->address); + SDRV_INFO("Set BP: pid=%d slot=%d addr=0x%llx\n", + p->pid, slot, bp->address); put_task_struct(target); - return 0; - } @@ -1100,6 +1096,77 @@ static int do_clear_breakpoint(struct attached_proc *p, struct snake_hw_breakpoi } +static void add_perf_counters(struct snake_perf_stats *out, + struct scan_perf_counters *counters) +{ + u64 scans = atomic64_read(&counters->total_scans); + u64 bytes = atomic64_read(&counters->total_bytes); + u64 matches = atomic64_read(&counters->total_matches); + u64 time_ns = atomic64_read(&counters->total_time_ns); + s64 min_ns = atomic64_read(&counters->min_time_ns); + u64 max_ns = atomic64_read(&counters->max_time_ns); + u64 parallel = atomic64_read(&counters->parallel_scans); + u64 cache_hits = atomic64_read(&counters->cache_hits); + u64 cache_misses = atomic64_read(&counters->cache_misses); + + out->total_scans += scans; + out->total_bytes += bytes; + out->total_matches += matches; + out->total_time_ns += time_ns; + out->huge_page_scans += atomic64_read(&counters->huge_page_scans); + + if (scans) { + out->avg_latency_us += (time_ns / scans) / 1000; + if (min_ns != LLONG_MAX && + (out->min_latency_us == 0 || (u64)min_ns / 1000 < out->min_latency_us)) + out->min_latency_us = (u64)min_ns / 1000; + if (max_ns / 1000 > out->max_latency_us) + out->max_latency_us = max_ns / 1000; + out->parallel_ratio += (parallel * 100) / scans; + } + + if (time_ns) { + out->throughput_mbps += (bytes * 1000) / (time_ns / 1000000 + 1); + out->matches_per_sec += (matches * 1000000000) / time_ns; + } + + if (cache_hits + cache_misses) + out->cache_hit_rate += (cache_hits * 100) / (cache_hits + cache_misses); +} + +static void fill_scanner_perf_stats(struct snake_perf_stats *out) +{ + u64 measured_groups = 0; + + memset(out, 0, sizeof(*out)); + +#define ADD_GROUP(counters) do { \ + if (atomic64_read(&(counters).total_scans)) \ + measured_groups++; \ + add_perf_counters(out, &(counters)); \ +} while (0) + + ADD_GROUP(global_perf_stats.exact_value); + ADD_GROUP(global_perf_stats.pattern); + ADD_GROUP(global_perf_stats.changed); + ADD_GROUP(global_perf_stats.unchanged); + ADD_GROUP(global_perf_stats.float_scan); + ADD_GROUP(global_perf_stats.double_scan); + ADD_GROUP(global_perf_stats.string_ascii); + ADD_GROUP(global_perf_stats.string_unicode); + ADD_GROUP(global_perf_stats.pointer_chain); + +#undef ADD_GROUP + + if (measured_groups) { + out->avg_latency_us /= measured_groups; + out->throughput_mbps /= measured_groups; + out->matches_per_sec /= measured_groups; + out->cache_hit_rate /= measured_groups; + out->parallel_ratio /= measured_groups; + } +} + /* ============================================================================ * IOCTL Handler * ============================================================================ */ @@ -1139,7 +1206,7 @@ static long snakedrv_ioctl(struct file *file, unsigned int cmd, unsigned long ar case SNAKE_IOCTL_GET_INFO: { struct snake_driver_info info = {0}; - + info.version_major = SNAKEDRV_VERSION_MAJOR; info.version_minor = SNAKEDRV_VERSION_MINOR; info.version_patch = SNAKEDRV_VERSION_PATCH; @@ -1147,7 +1214,7 @@ static long snakedrv_ioctl(struct file *file, unsigned int cmd, unsigned long ar sizeof(info.version_string)); strscpy(info.kernel_release, init_uts_ns.name.release, sizeof(info.kernel_release)); - + info.capabilities = SNAKE_CAP_HW_BREAKPOINTS | SNAKE_CAP_MULTITHREAD | SNAKE_CAP_PHYS_MEMORY | @@ -1156,8 +1223,9 @@ static long snakedrv_ioctl(struct file *file, unsigned int cmd, unsigned long ar info.max_breakpoints = HBP_NUM; info.max_attached = param_max_attached; info.page_size = PAGE_SIZE; + info.abi_version = SNAKEDRV_ABI_VERSION; info.result = SNAKEDRV_SUCCESS; - + if (copy_to_user(uarg, &info, sizeof(info))) ret = -EFAULT; break; @@ -1950,6 +2018,7 @@ static long snakedrv_ioctl(struct file *file, unsigned int cmd, unsigned long ar } /* Cache the result set for potential rescans */ + result_set->scan_type = exec.params.scan_type; exec.result_set_id = scanner_cache_add(result_set); exec.result = 0; @@ -1959,6 +2028,61 @@ static long snakedrv_ioctl(struct file *file, unsigned int cmd, unsigned long ar break; } + case SNAKE_IOCTL_SCAN_GET_RESULTS: { + struct snake_scan_execute exec; + struct scan_result_set *set; + uint32_t count, i; + + if (copy_from_user(&exec, uarg, sizeof(exec))) + return -EFAULT; + + set = scanner_cache_get(exec.params.result_set_id); + if (!set) { + exec.result = -ENOENT; + if (copy_to_user(uarg, &exec, sizeof(exec))) + return -EFAULT; + return -ENOENT; + } + + count = min(set->count, exec.results_capacity); + exec.results_count = count; + exec.total_matches = set->count; + exec.result_set_id = set->id; + exec.result = 0; + + if (count > 0 && exec.results) { + struct snake_scan_result *user_results; + + user_results = kvmalloc_array(count, sizeof(*user_results), + GFP_KERNEL | __GFP_ZERO); + if (!user_results) { + scanner_cache_put(set); + return -ENOMEM; + } + + for (i = 0; i < count; i++) { + user_results[i].address = set->results[i].address; + user_results[i].value = set->results[i].value; + user_results[i].size = exec.params.value_type; + user_results[i].region_index = 0; + } + + if (copy_to_user((void __user *)exec.results, user_results, + count * sizeof(*user_results))) { + kvfree(user_results); + scanner_cache_put(set); + return -EFAULT; + } + kvfree(user_results); + } + + scanner_cache_put(set); + + if (copy_to_user(uarg, &exec, sizeof(exec))) + ret = -EFAULT; + break; + } + case SNAKE_IOCTL_SCAN_FREE_RESULTS: { uint32_t result_set_id; @@ -1971,6 +2095,100 @@ static long snakedrv_ioctl(struct file *file, unsigned int cmd, unsigned long ar break; } + case SNAKE_IOCTL_SCAN_GET_INFO: { + struct snake_scan_result_set_info info; + struct scan_result_set *set; + + if (copy_from_user(&info, uarg, sizeof(info))) + return -EFAULT; + + set = scanner_cache_get(info.result_set_id); + if (!set) + return -ENOENT; + + info.count = set->count; + info.scan_type = set->scan_type; + info.has_bloom = set->use_bloom ? 1 : 0; + info.memory_usage = sizeof(*set) + + ((u64)set->max_results * sizeof(*set->results)); + + scanner_cache_put(set); + + if (copy_to_user(uarg, &info, sizeof(info))) + ret = -EFAULT; + break; + } + + case SNAKE_IOCTL_GET_BACKEND_INFO: { + struct snake_backend_info info = {0}; + + info.backend_type = BACKEND_PROCESS; + info.supports_huge_pages = 0; + info.supports_parallel = 1; + info.page_size = PAGE_SIZE; + strscpy(info.name, "process", sizeof(info.name)); + + if (copy_to_user(uarg, &info, sizeof(info))) + ret = -EFAULT; + break; + } + + case SNAKE_IOCTL_SET_BACKEND: { + uint32_t backend; + + if (copy_from_user(&backend, uarg, sizeof(backend))) + return -EFAULT; + + if (backend != BACKEND_AUTO && backend != BACKEND_PROCESS) + return -EINVAL; + break; + } + + case SNAKE_IOCTL_GET_PERF_STATS: { + struct snake_perf_stats stats; + + fill_scanner_perf_stats(&stats); + if (copy_to_user(uarg, &stats, sizeof(stats))) + ret = -EFAULT; + break; + } + + case SNAKE_IOCTL_RESET_PERF_STATS: + snakedrv_perf_init(&global_perf_stats); + break; + + case SNAKE_IOCTL_GET_SCAN_OPTIONS: { + struct snake_scan_options options; + + mutex_lock(&scan_options_mutex); + options = global_scan_options; + mutex_unlock(&scan_options_mutex); + + if (copy_to_user(uarg, &options, sizeof(options))) + ret = -EFAULT; + break; + } + + case SNAKE_IOCTL_SET_SCAN_OPTIONS: { + struct snake_scan_options options; + + if (copy_from_user(&options, uarg, sizeof(options))) + return -EFAULT; + + if (options.bloom_fpr > 1000) + return -EINVAL; + + mutex_lock(&scan_options_mutex); + global_scan_options.enable_parallel = options.enable_parallel ? 1 : 0; + global_scan_options.enable_bloom = options.enable_bloom ? 1 : 0; + global_scan_options.enable_prefetch = options.enable_prefetch ? 1 : 0; + global_scan_options.enable_huge_pages = options.enable_huge_pages ? 1 : 0; + global_scan_options.default_threads = options.default_threads; + global_scan_options.bloom_fpr = options.bloom_fpr; + mutex_unlock(&scan_options_mutex); + break; + } + /* ======================================================================== * Injection Operations (Manual Mapping) * ======================================================================== */ diff --git a/kernel/snakedrv_scanner.c b/kernel/snakedrv_scanner.c index 732f07e..a3c55dd 100644 --- a/kernel/snakedrv_scanner.c +++ b/kernel/snakedrv_scanner.c @@ -423,6 +423,7 @@ struct scan_result_set *scanner_create_result_set(uint32_t max_results) set->max_results = max_results; set->count = 0; + set->scan_type = 0; spin_lock_init(&set->lock); /* Initialize cache fields */ diff --git a/kernel/snakedrv_scanner.h b/kernel/snakedrv_scanner.h index eb7207a..d918e87 100644 --- a/kernel/snakedrv_scanner.h +++ b/kernel/snakedrv_scanner.h @@ -59,6 +59,7 @@ struct bloom_filter; * @lock: Spinlock for thread-safe access * @bloom: Optional Bloom filter for fast address lookups (rescans) * @use_bloom: If true, use Bloom filter for membership testing + * @scan_type: Type of scan that created the result set * @id: Unique ID for this result set (0 = not cached) * @ref_count: Reference count for cache management * @list: List node for cached result sets @@ -75,6 +76,7 @@ struct scan_result_set { spinlock_t lock; struct bloom_filter *bloom; bool use_bloom; + uint32_t scan_type; uint32_t id; atomic_t ref_count; struct list_head list; diff --git a/packaging/cmake/SnakeDrvConfig.cmake.in b/packaging/cmake/SnakeDrvConfig.cmake.in new file mode 100644 index 0000000..f823e56 --- /dev/null +++ b/packaging/cmake/SnakeDrvConfig.cmake.in @@ -0,0 +1,87 @@ +# +# SnakeDrvConfig.cmake — exported by libsnakedrv. +# +# This file is rendered from SnakeDrvConfig.cmake.in by the Makefile +# (sed substitutes the @VARS@ placeholders), so it cannot rely on the +# CMake-only @PACKAGE_INIT@ macro. +# +# Usage in a downstream project: +# +# find_package(SnakeDrv 2.0 REQUIRED) +# target_link_libraries(myapp PRIVATE SnakeDrv::SnakeDrv) +# +# Headers under become reachable and the runtime library +# is linked. Header-only consumers can use SnakeDrv::Headers. +# + +cmake_minimum_required(VERSION 3.16) + +macro(_snakedrv_set_and_check _var _file) + set(${_var} "${_file}") + if(NOT EXISTS "${_file}") + message(FATAL_ERROR + "SnakeDrv: ${_var} = ${_file} does not exist. " + "Reinstall libsnakedrv or pass -DCMAKE_PREFIX_PATH=.") + endif() +endmacro() + +macro(_snakedrv_check_required_components _NAME) + foreach(comp ${${_NAME}_FIND_COMPONENTS}) + if(NOT ${_NAME}_${comp}_FOUND) + if(${_NAME}_FIND_REQUIRED_${comp}) + set(${_NAME}_FOUND FALSE) + endif() + endif() + endforeach() +endmacro() + +function(_snakedrv_read_header_define _out_var _header _define) + file(STRINGS "${_header}" _matches + REGEX "^[ \t]*#[ \t]*define[ \t]+${_define}[ \t]+[0-9]+") + if(NOT _matches) + message(FATAL_ERROR + "SnakeDrv: ${_header} does not define ${_define}. " + "Reinstall libsnakedrv headers.") + endif() + list(GET _matches 0 _line) + string(REGEX REPLACE + "^[ \t]*#[ \t]*define[ \t]+${_define}[ \t]+([0-9]+).*$" + "\\1" _value "${_line}") + set(${_out_var} "${_value}" PARENT_SCOPE) +endfunction() + +set(SNAKEDRV_VERSION "@SNAKEDRV_VERSION@") +set(SNAKEDRV_VERSION_MAJOR @SNAKEDRV_VERSION_MAJOR@) +set(SNAKEDRV_VERSION_MINOR @SNAKEDRV_VERSION_MINOR@) +set(SNAKEDRV_VERSION_PATCH @SNAKEDRV_VERSION_PATCH@) +set(SNAKEDRV_ABI_VERSION @SNAKEDRV_ABI_VERSION@) + +_snakedrv_set_and_check(SNAKEDRV_INCLUDE_DIR "@INCLUDEDIR@") +_snakedrv_set_and_check(SNAKEDRV_LIBRARY "@LIBDIR@/libsnakedrv.so") +_snakedrv_set_and_check(SNAKEDRV_MAIN_HEADER "${SNAKEDRV_INCLUDE_DIR}/snakedrv.h") +_snakedrv_set_and_check(SNAKEDRV_SCANNER_HEADER "${SNAKEDRV_INCLUDE_DIR}/snakedrv_scanner.h") + +_snakedrv_read_header_define(_SNAKEDRV_HEADER_ABI + "${SNAKEDRV_MAIN_HEADER}" "SNAKEDRV_ABI_VERSION") +if(NOT _SNAKEDRV_HEADER_ABI EQUAL SNAKEDRV_ABI_VERSION) + message(FATAL_ERROR + "SnakeDrv: package ABI (${SNAKEDRV_ABI_VERSION}) does not match " + "installed header ABI (${_SNAKEDRV_HEADER_ABI}). Reinstall libsnakedrv.") +endif() + +if(NOT TARGET SnakeDrv::Headers) + add_library(SnakeDrv::Headers INTERFACE IMPORTED) + set_target_properties(SnakeDrv::Headers PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${SNAKEDRV_INCLUDE_DIR}" + ) +endif() + +if(NOT TARGET SnakeDrv::SnakeDrv) + add_library(SnakeDrv::SnakeDrv SHARED IMPORTED) + set_target_properties(SnakeDrv::SnakeDrv PROPERTIES + IMPORTED_LOCATION "${SNAKEDRV_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${SNAKEDRV_INCLUDE_DIR}" + ) +endif() + +_snakedrv_check_required_components(SnakeDrv) diff --git a/packaging/polkit/com.snakeengine.driver.policy b/packaging/polkit/com.snakeengine.driver.policy new file mode 100644 index 0000000..f5988c8 --- /dev/null +++ b/packaging/polkit/com.snakeengine.driver.policy @@ -0,0 +1,38 @@ + + + + + + SnakeEngine + https://github.com/CyberSnakeH/snakeengine-driver + + + Update the SnakeEngine kernel driver + Mettre à jour le pilote noyau SnakeEngine + Authentication is required to install or update the SnakeEngine kernel driver. + Une authentification est requise pour installer ou mettre à jour le pilote noyau SnakeEngine. + + + + auth_admin + auth_admin + auth_admin_keep + + + + /usr/lib/snakeengine/snakedrv-updater + true + + + diff --git a/packaging/snakedrv-updater b/packaging/snakedrv-updater new file mode 100755 index 0000000..b467d0d --- /dev/null +++ b/packaging/snakedrv-updater @@ -0,0 +1,329 @@ +#!/usr/bin/env bash +# +# snakedrv-updater — fetch, verify, and install a snakeengine-driver +# release. Invoked via pkexec from the SnakeEngine UI when the kernel +# module's ABI is too old, or run manually for a fresh install / +# downgrade. +# +# Usage: +# snakedrv-updater +# snakedrv-updater --check # print latest version +# snakedrv-updater --help +# +# Argv: +# version semver of the release to install (e.g. 2.1.0). Without +# a leading 'v'. The matching git tag is `v`. +# +# Behaviour: +# 1. Download the release tarball + .sigstore bundle from +# https://github.com/CyberSnakeH/snakeengine-driver/releases/... +# 2. Verify the signature with /usr/lib/snakeengine/sigstore-verify +# against the cosign-keyless identity pinned below. +# 3. Stage the source under /usr/src/snakedrv-/, render +# dkms.conf with the right version, and either: +# a) DKMS path : dkms remove old + add/autoinstall new +# b) Direct path: make -C kernel; cp .ko to /lib/modules/.../extra +# 4. Install the userland (libsnakedrv.so + headers + pkg-config + +# CMake exports). +# 5. Reload the module and refresh the dynamic-linker cache. +# +# Exits non-zero on any failure with a clear message; the SnakeEngine +# UI surfaces the message verbatim. + +set -euo pipefail + +# ─── Configuration (must stay in sync with the release pipeline) ─── +readonly REPO='CyberSnakeH/snakeengine-driver' +readonly IDENTITY_REGEX='^https://github\.com/CyberSnakeH/snakeengine-driver/\.github/workflows/release\.yml@refs/tags/v.*$' +readonly OIDC_ISSUER='https://token.actions.githubusercontent.com' +readonly SIGSTORE_VERIFY='/usr/lib/snakeengine/sigstore-verify' +readonly CURL_CONNECT_TIMEOUT=10 +readonly CURL_MAX_TIME=120 +readonly SIGSTORE_VERIFY_TIMEOUT=120 + +# ─── Helpers ─────────────────────────────────────────────────────── +log() { printf '\033[36m[snakedrv-updater]\033[0m %s\n' "$*" >&2; } +warn() { printf '\033[33m[snakedrv-updater]\033[0m %s\n' "$*" >&2; } +die() { printf '\033[31m[snakedrv-updater]\033[0m %s\n' "$*" >&2; exit 1; } + +usage() { + cat <<'EOF' +Usage: snakedrv-updater + snakedrv-updater --check + snakedrv-updater --help + +Fetch, verify, and install a snakeengine-driver release. +Requires root; invoke via pkexec or sudo. +EOF +} + +require_root() { + [ "$(id -u)" = 0 ] || die "must run as root (use pkexec or sudo)" +} + +require_tool() { + command -v "$1" >/dev/null 2>&1 || die "missing required tool: $1" +} + +latest_release_version() { + # Print the latest release's version (without leading 'v'). + require_tool curl + curl -fsSL \ + --connect-timeout "$CURL_CONNECT_TIMEOUT" \ + --max-time "$CURL_MAX_TIME" \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "https://api.github.com/repos/${REPO}/releases/latest" \ + | sed -n 's/^[[:space:]]*"tag_name":[[:space:]]*"v\?\([^"]*\)".*/\1/p' \ + | head -n1 +} + +# ─── Main install path ───────────────────────────────────────────── +install_release() { + local version="$1" + local tag="v${version}" + + require_root + require_tool curl + require_tool tar + require_tool make + require_tool modprobe + require_tool rmmod + require_tool timeout + [ -x "$SIGSTORE_VERIFY" ] || \ + die "sigstore-verify not found at $SIGSTORE_VERIFY (reinstall the snakeengine-driver package?)" + + local tmp; tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + + local base="https://github.com/${REPO}/releases/download/${tag}" + local tarball="snakeengine-driver-${version}.tar.gz" + + log "downloading ${tarball}" + curl -fsSL --connect-timeout "$CURL_CONNECT_TIMEOUT" --max-time "$CURL_MAX_TIME" \ + -o "$tmp/$tarball" "$base/$tarball" + curl -fsSL --connect-timeout "$CURL_CONNECT_TIMEOUT" --max-time "$CURL_MAX_TIME" \ + -o "$tmp/$tarball.sigstore" "$base/$tarball.sigstore" + + log "verifying signature with sigstore-verify" + timeout --kill-after=5s "${SIGSTORE_VERIFY_TIMEOUT}s" \ + "$SIGSTORE_VERIFY" verify-blob \ + --bundle "$tmp/$tarball.sigstore" \ + --certificate-identity-regexp "$IDENTITY_REGEX" \ + --certificate-oidc-issuer "$OIDC_ISSUER" \ + "$tmp/$tarball" \ + || die "signature verification failed — refusing to install" + + log "extracting" + tar xzf "$tmp/$tarball" -C "$tmp" + local src="$tmp/snakeengine-driver-${version}" + [ -d "$src" ] || die "tarball missing expected top-level dir snakeengine-driver-${version}" + local expected_abi + expected_abi="$(awk '/^[[:space:]]*#define[[:space:]]+SNAKEDRV_ABI_VERSION/{print $3}' \ + "$src/userland/include/snakedrv.h")" + [ -n "$expected_abi" ] || die "could not parse SNAKEDRV_ABI_VERSION from release header" + + # Build and load the kernel module BEFORE touching the userland + # library: a half-finished update that swaps libsnakedrv but fails to + # load the matching module leaves a new-ABI userland talking to the + # old kernel module. Module first, userland last. + + # Render dkms.conf with the right version. + local dkms_src="/usr/src/snakedrv-${version}" + if command -v dkms >/dev/null 2>&1; then + remove_dkms_versions + fi + rm -rf "$dkms_src" + cp -a "$src" "$dkms_src" + sed "s|@SNAKEDRV_VERSION@|${version}|g" \ + "$src/packaging/dkms/dkms.conf.in" > "$dkms_src/dkms.conf" + + if command -v dkms >/dev/null 2>&1; then + install_via_dkms "$version" + else + warn "dkms not installed — falling back to direct module install" + install_direct "$dkms_src" + fi + + install_system_integration "$src" + + log "reloading kernel module" + if [ -d /sys/module/snakedrv ]; then + rmmod snakedrv \ + || die "could not unload the currently loaded snakedrv module; close clients and retry" + fi + if ! modprobe snakedrv; then + die "the new module was built but failed to load (Secure Boot / lockdown / kernel mismatch?) — driver not updated" + fi + verify_loaded_module "$version" "$expected_abi" + + # Module is live — now swap the userland library to match. + log "installing userland (libsnakedrv + headers + pkg-config + CMake)" + local libdir + libdir="$(detect_libdir)" + make -C "$src/userland" -j install \ + PREFIX=/usr LIBDIR="$libdir" + + ldconfig + + log "done. snakeengine-driver ${version} installed and loaded." +} + +remove_dkms_versions() { + local status_output + + # Remove every previously-registered snakedrv version so we don't + # leave stale /usr/src/snakedrv-* trees behind. + if ! status_output="$(dkms status -m snakedrv 2>&1)"; then + die "dkms status failed: $status_output" + fi + while read -r prev; do + [ -n "$prev" ] || continue + log "removing previous DKMS version: $prev" + dkms remove -m snakedrv -v "$prev" --all \ + || die "dkms remove failed for snakedrv/$prev" + rm -rf "/usr/src/snakedrv-$prev" + done < <(printf '%s\n' "$status_output" \ + | sed -n 's|^.*snakedrv[/, ]\([0-9.][0-9.]*\).*|\1|p' \ + | sort -u) +} + +install_via_dkms() { + local version="$1" + + log "DKMS add + autoinstall" + dkms add -m snakedrv -v "$version" \ + || die "dkms add failed for snakedrv/$version" + dkms autoinstall -m snakedrv -v "$version" \ + || die "dkms autoinstall failed — kernel-headers package missing for $(uname -r)?" +} + +install_direct() { + local src="$1" + require_tool gcc + require_tool depmod + require_tool modinfo + local kdir="/lib/modules/$(uname -r)/build" + local ko="$src/kernel/snakedrv.ko" + local vermagic + + [ -d "$kdir" ] || die "kernel headers missing at $kdir; install kernel-devel/linux-headers and retry" + + make -C "$src/kernel" CC=gcc -j + [ -s "$ko" ] || die "direct build completed but did not produce $ko" + modinfo "$ko" >/dev/null \ + || die "direct build produced an invalid kernel module: $ko" + vermagic="$(modinfo -F vermagic "$ko" 2>/dev/null || true)" + case "$vermagic" in + *"$(uname -r)"*) ;; + *) die "built module vermagic '$vermagic' does not match running kernel $(uname -r)" ;; + esac + install -d "/lib/modules/$(uname -r)/extra" + install -m 0644 "$ko" "/lib/modules/$(uname -r)/extra/" + depmod -a +} + +install_system_integration() { + local src="$1" + + log "installing system integration (udev + modprobe)" + + if ! getent group snakeengine >/dev/null 2>&1; then + groupadd --system snakeengine \ + || die "failed to create snakeengine system group" + fi + + install -d /etc/udev/rules.d /etc/modprobe.d + install -m 0644 "$src/security/99-snakedrv.rules" /etc/udev/rules.d/99-snakedrv.rules \ + || die "failed to install udev rule" + + cat > /etc/modprobe.d/snakedrv.conf << 'EOF' +# SnakeEngine Kernel Driver Configuration +options snakedrv max_attached_processes=16 event_queue_size=256 debug_level=1 +EOF + + if command -v udevadm >/dev/null 2>&1; then + udevadm control --reload-rules 2>/dev/null || true + fi +} + +verify_loaded_module() { + local expected_version="$1" + local expected_abi="$2" + local loaded_version='' + local loaded_abi='' + local i + + [ -d /sys/module/snakedrv ] \ + || die "snakedrv is not loaded after modprobe — driver not updated" + + if [ -r /sys/module/snakedrv/version ]; then + loaded_version="$(tr -d '[:space:]' < /sys/module/snakedrv/version)" + [ "$loaded_version" = "$expected_version" ] \ + || die "loaded snakedrv version is $loaded_version, expected $expected_version" + else + warn "loaded module does not expose /sys/module/snakedrv/version; skipping version check" + fi + + [ -r /sys/module/snakedrv/parameters/abi_version ] \ + || die "loaded snakedrv module does not expose abi_version; refusing to install matching userland" + loaded_abi="$(tr -d '[:space:]' < /sys/module/snakedrv/parameters/abi_version)" + [ "$loaded_abi" = "$expected_abi" ] \ + || die "loaded snakedrv ABI is $loaded_abi, expected $expected_abi" + + if command -v udevadm >/dev/null 2>&1; then + udevadm control --reload-rules 2>/dev/null || true + i=0 + while [ "$i" -lt 5 ] && [ ! -c /dev/snakedrv ]; do + udevadm trigger --subsystem-match=snakedrv --action=add 2>/dev/null || true + udevadm settle --timeout=5 2>/dev/null || true + i=$((i + 1)) + sleep 1 + done + fi + + [ -c /dev/snakedrv ] \ + || die "/dev/snakedrv was not created after loading the verified module; check devtmpfs/udev" +} + +# Best-effort libdir detection. Fedora/RHEL → /usr/lib64, Debian/Ubuntu +# → /usr/lib/x86_64-linux-gnu, Arch → /usr/lib. Falls back to the +# pkg-config view of the running system. +detect_libdir() { + if command -v pkg-config >/dev/null 2>&1; then + local d + d="$(pkg-config --variable=libdir snakedrv 2>/dev/null || true)" + [ -n "$d" ] && [ -d "$d" ] && { printf '%s' "$d"; return; } + fi + + case "$(uname -m)" in + x86_64) + if [ -d /usr/lib64 ]; then printf '/usr/lib64'; return; fi + if [ -d /usr/lib/x86_64-linux-gnu ]; then printf '/usr/lib/x86_64-linux-gnu'; return; fi + ;; + esac + printf '/usr/lib' +} + +# ─── CLI dispatch ────────────────────────────────────────────────── +main() { + case "${1:-}" in + ''|-h|--help) + usage + exit 0 + ;; + --check) + v="$(latest_release_version)" || die "could not query GitHub API" + [ -n "$v" ] || die "could not parse latest release tag" + printf '%s\n' "$v" + ;; + v[0-9]*|[0-9]*) + install_release "${1#v}" + ;; + *) + die "unknown argument: $1 (try --help)" + ;; + esac +} + +main "$@" diff --git a/security/99-snakedrv.rules b/security/99-snakedrv.rules old mode 100755 new mode 100644 index 5cd32d5..0f7ce79 --- a/security/99-snakedrv.rules +++ b/security/99-snakedrv.rules @@ -1,34 +1,6 @@ -# udev rules for SnakeEngine kernel driver +# SnakeEngine driver character device. # -# Installation: -# sudo cp 99-snakedrv.rules /etc/udev/rules.d/ -# sudo udevadm control --reload-rules -# sudo udevadm trigger -# -# This rule: -# - Sets permissions to 0660 (rw for owner and group) -# - Sets group to 'snakeengine' (create with: groupadd snakeengine) -# - Alternatively uses 'plugdev' group if snakeengine doesn't exist -# -# To allow a user to access the device: -# sudo usermod -aG snakeengine $USER -# (logout and login required) - -# Create device node with correct permissions -KERNEL=="snakedrv", SUBSYSTEM=="snakedrv", MODE="0660", GROUP="snakeengine" - -# Fallback to plugdev group if snakeengine group doesn't exist -KERNEL=="snakedrv", SUBSYSTEM=="snakedrv", GROUP!="snakeengine", GROUP="plugdev" - -# For development/testing: allow all users (NOT recommended for production) -# Uncomment the following line for permissive mode: -# KERNEL=="snakedrv", MODE="0666" - -# Symlink for convenience -KERNEL=="snakedrv", SYMLINK+="snake" - -# Trigger on module load -ACTION=="add", KERNEL=="snakedrv", RUN+="/bin/sh -c 'echo SnakeEngine driver loaded >> /var/log/snakeengine.log'" +# The installer/package creates the snakeengine system group before this rule is +# loaded. Users that should access /dev/snakedrv must be members of that group. -# Cleanup on module unload -ACTION=="remove", KERNEL=="snakedrv", RUN+="/bin/sh -c 'echo SnakeEngine driver unloaded >> /var/log/snakeengine.log'" +ACTION=="add|change", KERNEL=="snakedrv", SUBSYSTEM=="snakedrv", MODE="0660", GROUP="snakeengine", SYMLINK+="snake" diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..113e7ba --- /dev/null +++ b/setup.sh @@ -0,0 +1,249 @@ +#!/usr/bin/env bash +# +# Interactive setup for snakeengine-driver. +# +# This script wraps deploy.sh with a TTY-friendly wizard that walks +# first-time users through: install prefix, DKMS opt-in, group +# membership, and udev/AppArmor/SELinux setup. It performs no +# non-interactive work — for CI / scripted installs use deploy.sh +# directly. +# +# Run as the *unprivileged* user; the script re-invokes deploy.sh via +# sudo only for the steps that need root. + +set -euo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly DEPLOY="${SCRIPT_DIR}/deploy.sh" + +# ─── Colours ─────────────────────────────────────────────────────── +if [ -t 1 ] && [ "${NO_COLOR:-}" != "1" ]; then + BOLD=$'\033[1m' DIM=$'\033[2m' + RED=$'\033[31m' GRN=$'\033[32m' YEL=$'\033[33m' + CYA=$'\033[36m' RST=$'\033[0m' +else + BOLD='' DIM='' RED='' GRN='' YEL='' CYA='' RST='' +fi + +# ─── Prompt helpers ──────────────────────────────────────────────── +hr() { printf "${DIM}%s${RST}\n" "──────────────────────────────────────────────────────────────"; } + +ask() { + # ask "Question" "default-value" + local prompt="$1" default="${2:-}" reply + if [ -n "$default" ]; then + read -r -p "$(printf '%s [%s%s%s]: ' "$prompt" "$BOLD" "$default" "$RST")" reply + printf '%s' "${reply:-$default}" + else + read -r -p "$prompt: " reply + printf '%s' "$reply" + fi +} + +yesno() { + # yesno "Question" "default(yes|no)" → exit 0 for yes, 1 for no + local prompt="$1" default="${2:-yes}" reply + local hint='[Y/n]' + [ "$default" = "no" ] && hint='[y/N]' + while true; do + read -r -p "$(printf '%s %s ' "$prompt" "$hint")" reply + reply="${reply:-$default}" + case "$reply" in + [Yy]|[Yy][Ee][Ss]) return 0 ;; + [Nn]|[Nn][Oo]) return 1 ;; + *) printf ' %splease answer yes or no%s\n' "$YEL" "$RST" ;; + esac + done +} + +choose() { + # choose "Prompt" choice1 choice2 … → prints the chosen string + local prompt="$1"; shift + local opts=( "$@" ) i n="$#" + { + echo + printf ' %s%s%s\n' "$BOLD" "$prompt" "$RST" + for ((i=0; i&2 + while true; do + local pick + read -r -p " Choice [1-$n]: " pick >&2 + if [[ "$pick" =~ ^[0-9]+$ ]] && (( pick >= 1 && pick <= n )); then + printf '%s' "${opts[$((pick-1))]}" + return 0 + fi + printf ' %sinvalid choice%s\n' "$YEL" "$RST" >&2 + done +} + +require_user() { + if [ "$(id -u)" = 0 ]; then + printf '%sDo not run setup.sh as root.%s It re-invokes deploy.sh via sudo only when needed.\n' "$RED" "$RST" >&2 + exit 1 + fi +} + +require_deploy() { + [ -x "$DEPLOY" ] || { + printf '%sCannot find deploy.sh at %s%s\n' "$RED" "$DEPLOY" "$RST" >&2 + exit 1 + } +} + +# ─── Banner ──────────────────────────────────────────────────────── +banner() { + cat </dev/null 2>&1; then + if yesno "Register module with DKMS?" yes; then + use_dkms="" + fi + else + printf ' %sDKMS is not installed on this system — falling back to direct install.%s\n' "$YEL" "$RST" + printf ' %s(Install the dkms package later if you want auto-rebuild on kernel updates.)%s\n\n' "$DIM" "$RST" + fi + + # ── Group membership ────────────────────────────────────────── + hr + printf '\n %sStep 3 — Group membership%s\n\n' "$BOLD" "$RST" + printf ' /dev/snakedrv is owned by group ${BOLD}snakeengine${RST}. Adding\n' + printf ' your user to that group lets you use SnakeEngine without sudo.\n\n' + + local join_group="no" + if yesno "Add user '$USER' to the snakeengine group?" yes; then + join_group="yes" + fi + + # ── Confirmation ────────────────────────────────────────────── + hr + printf '\n %sSummary%s\n\n' "$BOLD" "$RST" + printf ' Prefix : %s%s%s\n' "$BOLD" "$prefix" "$RST" + printf ' DKMS : %s%s%s\n' "$BOLD" \ + "$([ -z "$use_dkms" ] && echo "yes" || echo "no")" "$RST" + printf ' Add to group : %s%s%s\n' "$BOLD" "$join_group" "$RST" + printf ' Distro : %s%s%s\n\n' "$BOLD" "$distro_pretty" "$RST" + + if ! yesno "Proceed with installation?" yes; then + printf '\n%sAborted.%s\n' "$YEL" "$RST" + exit 0 + fi + + # ── Build ───────────────────────────────────────────────────── + hr + printf '\n %sStep 4 — Build%s\n\n' "$BOLD" "$RST" + "$DEPLOY" build || { + printf '\n%sBuild failed. Read the output above and rerun setup.sh once fixed.%s\n' "$RED" "$RST" + exit 1 + } + + # ── Install (root) ──────────────────────────────────────────── + hr + printf '\n %sStep 5 — Install (sudo will prompt)%s\n\n' "$BOLD" "$RST" + + local -a deploy_args=( install --prefix="$prefix" ) + [ -n "$use_dkms" ] && deploy_args+=( "$use_dkms" ) + + sudo -k # force a fresh prompt so user sees what they're auth'ing for + sudo "$DEPLOY" "${deploy_args[@]}" || { + printf '\n%sInstall failed. See the messages above.%s\n' "$RED" "$RST" + exit 1 + } + + # ── Group membership (root) ─────────────────────────────────── + if [ "$join_group" = "yes" ]; then + if getent group snakeengine >/dev/null 2>&1; then + sudo usermod -aG snakeengine "$USER" && \ + printf '\n %s✓ added %s to the snakeengine group%s\n' "$GRN" "$USER" "$RST" + else + printf '\n %sgroup snakeengine does not exist yet — skipping%s\n' "$YEL" "$RST" + fi + fi + + # ── Load module ─────────────────────────────────────────────── + hr + printf '\n %sStep 6 — Load the module%s\n\n' "$BOLD" "$RST" + if yesno "Load snakedrv now?" yes; then + sudo "$DEPLOY" load || \ + printf '%smodprobe snakedrv failed — load it manually after rebooting if needed%s\n' "$YEL" "$RST" + fi + + # ── Done ────────────────────────────────────────────────────── + hr + cat <${RST} + +EOF +} + +main "$@" diff --git a/tools/sigstore-verify/.gitignore b/tools/sigstore-verify/.gitignore new file mode 100644 index 0000000..d031e8d --- /dev/null +++ b/tools/sigstore-verify/.gitignore @@ -0,0 +1 @@ +sigstore-verify diff --git a/tools/sigstore-verify/Makefile b/tools/sigstore-verify/Makefile new file mode 100644 index 0000000..524fb8f --- /dev/null +++ b/tools/sigstore-verify/Makefile @@ -0,0 +1,35 @@ +# Makefile for sigstore-verify. +# +# make → build statically-linked, stripped binary +# make VERSION=x.y → embed version string +# make install → install to $(PREFIX)/bin/sigstore-verify +# make clean + +GO ?= go +PREFIX ?= /usr/local +DESTDIR ?= +VERSION ?= dev +BIN := sigstore-verify + +GOFLAGS := -trimpath -buildvcs=false +LDFLAGS := -s -w -X main.version=$(VERSION) + +# CGO_ENABLED=0 → fully static binary, no glibc dependency. +export CGO_ENABLED=0 + +.PHONY: all clean install test + +all: $(BIN) + +$(BIN): main.go go.mod go.sum + $(GO) build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o $@ . + +install: $(BIN) + install -d $(DESTDIR)$(PREFIX)/bin + install -m 0755 $(BIN) $(DESTDIR)$(PREFIX)/bin/$(BIN) + +test: + $(GO) test ./... + +clean: + rm -f $(BIN) diff --git a/tools/sigstore-verify/README.md b/tools/sigstore-verify/README.md new file mode 100644 index 0000000..5b67497 --- /dev/null +++ b/tools/sigstore-verify/README.md @@ -0,0 +1,75 @@ +# sigstore-verify + +Minimal Sigstore signature verifier. ~18 MB statically-linked Go binary +that performs cosign-keyless verification of release artifacts using the +Sigstore bundle (`.sigstore`) format. + +This tool is shipped alongside SnakeEngine release artifacts (inside the +AppImage and the `snakedrv-updater` script) so the auto-update flow can +verify a downloaded artifact without depending on the user having +`cosign` installed. + +## Build + +```bash +make +# → ./sigstore-verify +``` + +Embed a version string for `sigstore-verify version`: + +```bash +make VERSION=2.1.0 +``` + +## Usage + +```bash +sigstore-verify verify-blob \ + --bundle .sigstore \ + --certificate-identity-regexp \ + --certificate-oidc-issuer \ + [--offline] \ + +``` + +Example — verify a SnakeEngine driver release: + +```bash +sigstore-verify verify-blob \ + --bundle snakeengine-driver-2.1.0.tar.gz.sigstore \ + --certificate-identity-regexp \ + '^https://github.com/CyberSnakeH/snakeengine-driver/.github/workflows/release.yml@refs/tags/v.*$' \ + --certificate-oidc-issuer \ + 'https://token.actions.githubusercontent.com' \ + snakeengine-driver-2.1.0.tar.gz +``` + +Exit codes: + +| Code | Meaning | +|------|------------------------------------------------| +| 0 | signature valid AND identity matches the regex | +| 1 | signature invalid, identity mismatch, or error | +| 2 | bad usage / argument error | + +## What it verifies + +- The bundle's signature was produced by a Fulcio-issued certificate + whose SAN matches `--certificate-identity-regexp`. +- That certificate's OIDC issuer is exactly `--certificate-oidc-issuer`. +- The signed timestamp falls inside the certificate's validity window. +- The artifact bytes match the bundle's signed digest. +- A Rekor transparency-log entry exists for the signature + (skip with `--offline` if you trust the embedded inclusion proof and + signed timestamps only). + +## Why a separate verifier instead of bundling cosign? + +`cosign` is ~80 MB. `sigstore-verify` does the verify-only subset of +that surface in ~18 MB, all statically linked. Smaller AppImages, +faster downloads. + +## License + +GPL-2.0 (same as the rest of `snakeengine-driver`). diff --git a/tools/sigstore-verify/go.mod b/tools/sigstore-verify/go.mod new file mode 100644 index 0000000..d257a13 --- /dev/null +++ b/tools/sigstore-verify/go.mod @@ -0,0 +1,74 @@ +module github.com/CyberSnakeH/snakeengine-driver/tools/sigstore-verify + +go 1.26.2 + +require github.com/sigstore/sigstore-go v1.1.4 + +require ( + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect + github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect + github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v0.24.1 // indirect + github.com/go-openapi/errors v0.22.4 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.3 // indirect + github.com/go-openapi/loads v0.23.2 // indirect + github.com/go-openapi/runtime v0.29.2 // indirect + github.com/go-openapi/spec v0.22.1 // indirect + github.com/go-openapi/strfmt v0.25.0 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-openapi/validate v0.25.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/certificate-transparency-go v1.3.2 // indirect + github.com/google/go-containerregistry v0.20.7 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/in-toto/attestation v1.1.2 // indirect + github.com/in-toto/in-toto-golang v0.9.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect + github.com/shibumi/go-pathspec v1.3.0 // indirect + github.com/sigstore/protobuf-specs v0.5.0 // indirect + github.com/sigstore/rekor v1.4.3 // indirect + github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect + github.com/sigstore/sigstore v1.10.0 // indirect + github.com/sigstore/timestamp-authority/v2 v2.0.3 // indirect + github.com/theupdateframework/go-tuf/v2 v2.3.0 // indirect + github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c // indirect + github.com/transparency-dev/merkle v0.0.2 // indirect + go.mongodb.org/mongo-driver v1.17.6 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect +) diff --git a/tools/sigstore-verify/go.sum b/tools/sigstore-verify/go.sum new file mode 100644 index 0000000..581689d --- /dev/null +++ b/tools/sigstore-verify/go.sum @@ -0,0 +1,378 @@ +cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= +cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k= +cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= +github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= +github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= +github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc= +github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0= +github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg= +github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= +github.com/aws/aws-sdk-go-v2/service/kms v1.48.2 h1:aL8Y/AbB6I+uw0MjLbdo68NQ8t5lNs3CY3S848HpETk= +github.com/aws/aws-sdk-go-v2/service/kms v1.48.2/go.mod h1:VJcNH6BLr+3VJwinRKdotLOMglHO8mIKlD3ea5c7hbw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= +github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0= +github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= +github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow= +github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM= +github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84= +github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM= +github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= +github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= +github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= +github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY= +github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc5Yj0= +github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0= +github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= +github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= +github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= +github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw= +github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= +github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= +github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/trillian v1.7.2 h1:EPBxc4YWY4Ak8tcuhyFleY+zYlbCDCa4Sn24e1Ka8Js= +github.com/google/trillian v1.7.2/go.mod h1:mfQJW4qRH6/ilABtPYNBerVJAJ/upxHLX81zxNQw05s= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0= +github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= +github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E= +github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM= +github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= +github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7PDk6RgkP+A/SFfUD0ZR/AgG6SpRNEDKZy8= +github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b/go.mod h1:hQmNrgofl+IY/8L+n20H6E6PWBBTokdsv+q49j0QhsU= +github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= +github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/letsencrypt/boulder v0.20251110.0 h1:J8MnKICeilO91dyQ2n5eBbab24neHzUpYMUIOdOtbjc= +github.com/letsencrypt/boulder v0.20251110.0/go.mod h1:ogKCJQwll82m7OVHWyTuf8eeFCjuzdRQlgnZcCl0V+8= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= +github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= +github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4= +github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= +github.com/secure-systems-lab/go-securesystemslib v0.9.1 h1:nZZaNz4DiERIQguNy0cL5qTdn9lR8XKHf4RUyG1Sx3g= +github.com/secure-systems-lab/go-securesystemslib v0.9.1/go.mod h1:np53YzT0zXGMv6x4iEWc9Z59uR+x+ndLwCLqPYpLXVU= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= +github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= +github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= +github.com/sigstore/rekor v1.4.3 h1:2+aw4Gbgumv8vYM/QVg6b+hvr4x4Cukur8stJrVPKU0= +github.com/sigstore/rekor v1.4.3/go.mod h1:o0zgY087Q21YwohVvGwV9vK1/tliat5mfnPiVI3i75o= +github.com/sigstore/rekor-tiles/v2 v2.0.1 h1:1Wfz15oSRNGF5Dzb0lWn5W8+lfO50ork4PGIfEKjZeo= +github.com/sigstore/rekor-tiles/v2 v2.0.1/go.mod h1:Pjsbhzj5hc3MKY8FfVTYHBUHQEnP0ozC4huatu4x7OU= +github.com/sigstore/sigstore v1.10.0 h1:lQrmdzqlR8p9SCfWIpFoGUqdXEzJSZT2X+lTXOMPaQI= +github.com/sigstore/sigstore v1.10.0/go.mod h1:Ygq+L/y9Bm3YnjpJTlQrOk/gXyrjkpn3/AEJpmk1n9Y= +github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg= +github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.0 h1:UOHpiyezCj5RuixgIvCV3QyuxIGQT+N6nGZEXA7OTTY= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.0/go.mod h1:U0CZmA2psabDa8DdiV7yXab0AHODzfKqvD2isH7Hrvw= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.0 h1:fq4+8Y4YadxeF8mzhoMRPZ1mVvDYXmI3BfS0vlkPT7M= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.0/go.mod h1:u05nqPWY05lmcdHhv2lPaWTH3FGUhJzO7iW2hbboK3Q= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.0 h1:iUEf5MZYOuXGnXxdF/WrarJrk0DTVHqeIOjYdtpVXtc= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.0/go.mod h1:i6vg5JfEQix46R1rhQlrKmUtJoeH91drltyYOJEk1T4= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.0 h1:dUvPv/MP23ZPIXZUW45kvCIgC0ZRfYxEof57AB6bAtU= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.0/go.mod h1:fR/gDdPvJWGWL70/NgBBIL1O0/3Wma6JHs3tSSYg3s4= +github.com/sigstore/timestamp-authority/v2 v2.0.3 h1:sRyYNtdED/ttLCMdaYnwpf0zre1A9chvjTnCmWWxN8Y= +github.com/sigstore/timestamp-authority/v2 v2.0.3/go.mod h1:mDaHxkt3HmZYoIlwYj4QWo0RUr7VjYU52aVO5f5Qb3I= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= +github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= +github.com/theupdateframework/go-tuf/v2 v2.3.0 h1:gt3X8xT8qu/HT4w+n1jgv+p7koi5ad8XEkLXXZqG9AA= +github.com/theupdateframework/go-tuf/v2 v2.3.0/go.mod h1:xW8yNvgXRncmovMLvBxKwrKpsOwJZu/8x+aB0KtFcdw= +github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI= +github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw= +github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0 h1:6nAX1aRGnkg2SEUMwO5toB2tQkP0Jd6cbmZ/K5Le1V0= +github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0/go.mod h1:HOC5NWW1wBI2Vke1FGcRBvDATkEYE7AUDiYbXqi2sBw= +github.com/tink-crypto/tink-go/v2 v2.5.0 h1:B8KLF6AofxdBIE4UJIaFbmoj5/1ehEtt7/MmzfI4Zpw= +github.com/tink-crypto/tink-go/v2 v2.5.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ2LiAUV+/RjckMyq9sXudfrPSuCY4FuPC1NyAw= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI= +github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= +github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= +github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= +github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.step.sm/crypto v0.74.0 h1:/APBEv45yYR4qQFg47HA8w1nesIGcxh44pGyQNw6JRA= +go.step.sm/crypto v0.74.0/go.mod h1:UoXqCAJjjRgzPte0Llaqen7O9P7XjPmgjgTHQGkKCDk= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= +google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= +google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= +google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= +google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU= +google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/tools/sigstore-verify/main.go b/tools/sigstore-verify/main.go new file mode 100644 index 0000000..55c98ec --- /dev/null +++ b/tools/sigstore-verify/main.go @@ -0,0 +1,268 @@ +// sigstore-verify — minimal Sigstore signature verifier for SnakeEngine. +// +// This binary is shipped alongside the SnakeEngine self-update flow and +// the snakedrv-updater script. It performs cosign-keyless verification +// of release artifacts using the Sigstore bundle format. +// +// Usage: +// +// sigstore-verify verify-blob \ +// --bundle .sigstore \ +// --certificate-identity-regexp \ +// --certificate-oidc-issuer \ +// [--offline] \ +// +// +// Exit codes: 0 = valid, 1 = invalid signature, 2 = usage error. +package main + +import ( + "bytes" + "context" + "errors" + "flag" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tuf" + "github.com/sigstore/sigstore-go/pkg/verify" +) + +const usage = `sigstore-verify — minimal Sigstore signature verifier + +USAGE: + sigstore-verify verify-blob \ + --bundle .sigstore \ + --certificate-identity-regexp \ + --certificate-oidc-issuer \ + [--offline] \ + + +OPTIONS: + --bundle Sigstore bundle file (.sigstore) + --certificate-identity-regexp Required cert SAN identity regex + --certificate-oidc-issuer Required OIDC issuer URL + --offline Skip transparency-log lookup + (only use the bundle's embedded + inclusion proof and signed timestamps) + +EXIT STATUS: + 0 signature valid and identity matches + 1 signature invalid, identity mismatch, or runtime error + 2 bad usage / argument error +` + +func main() { + if len(os.Args) < 2 { + fmt.Fprint(os.Stderr, usage) + os.Exit(2) + } + switch os.Args[1] { + case "verify-blob": + verifyBlob(os.Args[2:]) + case "version", "--version", "-v": + fmt.Println(versionString()) + case "-h", "--help", "help": + fmt.Print(usage) + default: + fmt.Fprintf(os.Stderr, "sigstore-verify: unknown subcommand %q\n\n%s", + os.Args[1], usage) + os.Exit(2) + } +} + +func verifyBlob(args []string) { + fs := flag.NewFlagSet("verify-blob", flag.ContinueOnError) + fs.SetOutput(os.Stderr) + + var ( + bundlePath string + identityRe string + oidcIssuer string + offline bool + ) + fs.StringVar(&bundlePath, "bundle", "", + "Sigstore bundle file (.sigstore)") + fs.StringVar(&identityRe, "certificate-identity-regexp", "", + "Required cert SAN identity regex") + fs.StringVar(&oidcIssuer, "certificate-oidc-issuer", "", + "Required OIDC issuer URL") + fs.BoolVar(&offline, "offline", false, + "Skip Rekor lookup, use embedded proof only") + + if err := fs.Parse(args); err != nil { + os.Exit(2) + } + if fs.NArg() != 1 { + fmt.Fprint(os.Stderr, + "sigstore-verify: verify-blob takes exactly one positional argument (the blob path)\n\n") + fmt.Fprint(os.Stderr, usage) + os.Exit(2) + } + if bundlePath == "" || identityRe == "" || oidcIssuer == "" { + fmt.Fprintln(os.Stderr, + "sigstore-verify: --bundle, --certificate-identity-regexp, and --certificate-oidc-issuer are all required") + os.Exit(2) + } + + blobPath := fs.Arg(0) + + if err := run(context.Background(), runOpts{ + bundlePath: bundlePath, + blobPath: blobPath, + identityRe: identityRe, + oidcIssuer: oidcIssuer, + offline: offline, + }); err != nil { + fmt.Fprintf(os.Stderr, "sigstore-verify: %v\n", err) + os.Exit(1) + } + + fmt.Println("OK signature valid") +} + +type runOpts struct { + bundlePath string + blobPath string + identityRe string + oidcIssuer string + offline bool +} + +func run(ctx context.Context, opt runOpts) error { + // Sigstore TUF root: lazy-fetched + cached under + // $XDG_CACHE_HOME/sigstore-go/. The TUF client embeds the public + // trust anchor so the first fetch is itself authenticated. + tufOpts := tuf.DefaultOptions() + // Ten-second timeout: TUF metadata is small, this guards against + // the verifier hanging on offline hosts. + tufOpts.WithCachePath(defaultTUFCacheDir()) + tufClient, err := tuf.New(tufOpts) + if err != nil { + return fmt.Errorf("tuf client: %w", err) + } + trustedRoot, err := root.GetTrustedRoot(tufClient) + if err != nil { + return fmt.Errorf("trusted root: %w", err) + } + + // Verifier configuration: require at least one signed timestamp + // (RFC 3161 or the bundle's signed cert timestamp), at least one + // observer timestamp, and — unless --offline — at least one + // transparency-log entry. + verifierOpts := []verify.VerifierOption{ + verify.WithSignedCertificateTimestamps(1), + verify.WithObserverTimestamps(1), + } + if !opt.offline { + verifierOpts = append(verifierOpts, verify.WithTransparencyLog(1)) + } + + sev, err := verify.NewSignedEntityVerifier(trustedRoot, verifierOpts...) + if err != nil { + return fmt.Errorf("build verifier: %w", err) + } + + // Identity policy: require the cert SAN to match identityRe and the + // OIDC issuer to be exactly oidcIssuer. + certIdent, err := verify.NewShortCertificateIdentity( + opt.oidcIssuer, "", "", opt.identityRe) + if err != nil { + return fmt.Errorf("build identity policy: %w", err) + } + + // Load the bundle (.sigstore JSON) and the artifact bytes. + bun, err := bundle.LoadJSONFromPath(opt.bundlePath) + if err != nil { + return fmt.Errorf("load bundle %q: %w", opt.bundlePath, err) + } + + blob, err := os.Open(opt.blobPath) + if err != nil { + return fmt.Errorf("open blob %q: %w", opt.blobPath, err) + } + defer blob.Close() + + // `verify.WithArtifact` reads the whole blob into memory. For our + // release artifacts (a few hundred MB max for an AppImage) that's + // fine. We still buffer-read so we get a clear error if the blob + // disappears mid-verify. + body, err := io.ReadAll(blob) + if err != nil { + return fmt.Errorf("read blob: %w", err) + } + + policy := verify.NewPolicy( + verify.WithArtifact(bytes.NewReader(body)), + verify.WithCertificateIdentity(certIdent), + ) + + deadline := time.Now().Add(60 * time.Second) + verifyCtx, cancel := context.WithDeadline(ctx, deadline) + defer cancel() + + result, err := sev.Verify(bun, policy) + if err != nil { + return verifyError(err) + } + _ = verifyCtx + + if result == nil { + return errors.New("verifier returned nil result without error — refusing") + } + printResult(result) + return nil +} + +// verifyError sands rough edges off sigstore-go errors so the user +// gets an actionable message instead of a wrapped chain. +func verifyError(err error) error { + msg := err.Error() + switch { + case strings.Contains(msg, "certificate identity"): + return fmt.Errorf("certificate identity mismatch (artifact was signed by a different workflow): %w", err) + case strings.Contains(msg, "rekor"): + return fmt.Errorf("transparency-log check failed (try --offline if you trust the bundle): %w", err) + case strings.Contains(msg, "expired"), strings.Contains(msg, "not yet valid"): + return fmt.Errorf("signing certificate timestamp out of valid window: %w", err) + default: + return fmt.Errorf("verification failed: %w", err) + } +} + +func printResult(r *verify.VerificationResult) { + if r.Signature == nil { + return + } + if r.Signature.Certificate != nil { + c := r.Signature.Certificate + if len(c.SubjectAlternativeName) > 0 { + fmt.Printf(" Identity: %s\n", c.SubjectAlternativeName) + } + if c.Issuer != "" { + fmt.Printf(" Issuer: %s\n", c.Issuer) + } + } +} + +func defaultTUFCacheDir() string { + if d := os.Getenv("XDG_CACHE_HOME"); d != "" { + return d + "/sigstore-go" + } + if home, err := os.UserHomeDir(); err == nil { + return home + "/.cache/sigstore-go" + } + return os.TempDir() + "/sigstore-go" +} + +// versionString is overridden at link time via -ldflags='-X main.version=...' +var version = "dev" + +func versionString() string { + return fmt.Sprintf("sigstore-verify %s", version) +} diff --git a/userland/CMakeLists.txt b/userland/CMakeLists.txt index b54cc6a..0a6247e 100755 --- a/userland/CMakeLists.txt +++ b/userland/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(libsnakedrv VERSION 1.0.0 LANGUAGES CXX) +project(libsnakedrv VERSION 2.0.0 LANGUAGES CXX) # Options option(BUILD_SHARED_LIBS "Build shared library" ON) @@ -17,6 +17,7 @@ include_directories(include) set(SOURCES src/libsnakedrv.cpp src/libsnakedrv_scanner.cpp + src/snakedrv_injector.cpp ) # Headers @@ -55,3 +56,33 @@ set(CPACK_PACKAGE_NAME "libsnakedrv") set(CPACK_PACKAGE_VENDOR "SnakeEngine Project") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "SnakeEngine Driver Userland Library") include(CPack) + +if(SNAKEDRV_BUILD_TESTS) + enable_testing() + + add_library(manual_map_payload SHARED + selftests/manual_map_payload.cpp + ) + set_target_properties(manual_map_payload PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests" + PREFIX "" + ) + + add_executable(test_manualmap_elf + selftests/test_manualmap_elf.cpp + src/snakedrv_injector.cpp + ) + target_include_directories(test_manualmap_elf PRIVATE include) + + add_test(NAME manualmap_elf + COMMAND test_manualmap_elf "$" + ) + + add_executable(test_ioctl_abi + selftests/test_ioctl_abi.cpp + ) + target_include_directories(test_ioctl_abi PRIVATE include) + add_test(NAME ioctl_abi + COMMAND test_ioctl_abi + ) +endif() diff --git a/userland/Makefile b/userland/Makefile index 0343e1b..4a3b5a9 100755 --- a/userland/Makefile +++ b/userland/Makefile @@ -1,30 +1,157 @@ -# Makefile for libsnakedrv - Userland library for SnakeEngine kernel driver +# Makefile for libsnakedrv — SnakeEngine kernel-driver userland library. +# +# Build: +# make (or `make -j`) +# +# Install (DESTDIR-aware, no sudo by default): +# make install # → /usr/local +# make install PREFIX=/usr # → /usr +# make install DESTDIR=/tmp/stage # staging build (for packaging) +# +# Variables (all overridable on the command line): +# PREFIX install prefix [/usr/local] +# LIBDIR library directory [$(PREFIX)/lib] +# INCLUDEDIR header directory [$(PREFIX)/include] +# PCDIR pkg-config directory [$(LIBDIR)/pkgconfig] +# CMAKEDIR CMake config directory [$(LIBDIR)/cmake/SnakeDrv] +# DESTDIR staging root [empty] +# -CXX := g++ -CXXFLAGS := -std=c++17 -Wall -Wextra -O3 -march=native -fPIC +CXX ?= g++ +CXXFLAGS ?= -std=c++17 -Wall -Wextra -O3 -fPIC +CXXFLAGS += -D_FILE_OFFSET_BITS=64 INCLUDES := -Iinclude -LDFLAGS := -shared +LDFLAGS := -shared -Wl,-soname,libsnakedrv.so + +PREFIX ?= /usr/local +LIBDIR ?= $(PREFIX)/lib +INCLUDEDIR ?= $(PREFIX)/include +PCDIR ?= $(LIBDIR)/pkgconfig +CMAKEDIR ?= $(LIBDIR)/cmake/SnakeDrv + +# Repo-relative paths. +PACKAGING_DIR := ../packaging +HEADER_FILE := include/snakedrv.h +BUILD_DIR ?= build + +# Version is the single source of truth: parse it directly from the header. +SNAKEDRV_VERSION_MAJOR := $(shell awk '/^[[:space:]]*#define[[:space:]]+SNAKEDRV_VERSION_MAJOR/{print $$3}' $(HEADER_FILE)) +SNAKEDRV_VERSION_MINOR := $(shell awk '/^[[:space:]]*#define[[:space:]]+SNAKEDRV_VERSION_MINOR/{print $$3}' $(HEADER_FILE)) +SNAKEDRV_VERSION_PATCH := $(shell awk '/^[[:space:]]*#define[[:space:]]+SNAKEDRV_VERSION_PATCH/{print $$3}' $(HEADER_FILE)) +SNAKEDRV_ABI_VERSION := $(shell awk '/^[[:space:]]*#define[[:space:]]+SNAKEDRV_ABI_VERSION/{print $$3}' $(HEADER_FILE)) +SNAKEDRV_VERSION := $(SNAKEDRV_VERSION_MAJOR).$(SNAKEDRV_VERSION_MINOR).$(SNAKEDRV_VERSION_PATCH) SRCS := src/libsnakedrv.cpp src/libsnakedrv_scanner.cpp src/snakedrv_injector.cpp -OBJS := build/libsnakedrv.o build/libsnakedrv_scanner.o build/snakedrv_injector.o +OBJS := $(patsubst src/%.cpp,$(BUILD_DIR)/%.o,$(SRCS)) TARGET := libsnakedrv.so +MANUALMAP_PAYLOAD := $(BUILD_DIR)/manual_map_payload.so +MANUALMAP_ELF_TEST := $(BUILD_DIR)/test_manualmap_elf +IOCTL_ABI_TEST := $(BUILD_DIR)/test_ioctl_abi + +# Headers shipped to consumers (everything under include/). +PUBLIC_HEADERS := $(wildcard include/*.h) $(wildcard include/*.hpp) -.PHONY: all clean install +# DESTDIR-prefixed install paths. +D_LIBDIR := $(DESTDIR)$(LIBDIR) +D_INCLUDEDIR := $(DESTDIR)$(INCLUDEDIR)/snakedrv +D_PCDIR := $(DESTDIR)$(PCDIR) +D_CMAKEDIR := $(DESTDIR)$(CMAKEDIR) + +INSTALL ?= install + +.PHONY: all clean install test test-manualmap test-ioctl-abi \ + install-lib install-headers install-pkgconfig install-cmake \ + print-version all: $(TARGET) +print-version: + @echo "snakedrv $(SNAKEDRV_VERSION) (ABI $(SNAKEDRV_ABI_VERSION))" + +# ─── Build the shared library ────────────────────────────────────────── + $(TARGET): $(OBJS) - @echo "Linking $(TARGET)..." - $(CXX) $(LDFLAGS) $^ -o $@ + @echo " LD $@" + @$(CXX) $(LDFLAGS) $^ -o $@ -build/%.o: src/%.cpp - @mkdir -p build - @echo "Compiling $<..." - $(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@ +$(BUILD_DIR)/%.o: src/%.cpp + @mkdir -p $(BUILD_DIR) + @echo " CXX $<" + @$(CXX) $(CXXFLAGS) $(INCLUDES) -MMD -MP -c $< -o $@ -clean: - rm -rf build $(TARGET) +$(MANUALMAP_PAYLOAD): selftests/manual_map_payload.cpp + @mkdir -p $(BUILD_DIR) + @echo " CXX $@" + @$(CXX) $(CXXFLAGS) -shared -fPIC $< -o $@ + +$(MANUALMAP_ELF_TEST): selftests/test_manualmap_elf.cpp src/snakedrv_injector.cpp + @mkdir -p $(BUILD_DIR) + @echo " CXX $@" + @$(CXX) $(CXXFLAGS) $(INCLUDES) $^ -o $@ + +$(IOCTL_ABI_TEST): selftests/test_ioctl_abi.cpp + @mkdir -p $(BUILD_DIR) + @echo " CXX $@" + @$(CXX) $(CXXFLAGS) $(INCLUDES) $< -o $@ + +test: test-manualmap test-ioctl-abi + +test-manualmap: $(MANUALMAP_PAYLOAD) $(MANUALMAP_ELF_TEST) + @$(MANUALMAP_ELF_TEST) $(MANUALMAP_PAYLOAD) -install: $(TARGET) - sudo cp $(TARGET) /usr/local/lib/ - sudo ldconfig +test-ioctl-abi: $(IOCTL_ABI_TEST) + @$(IOCTL_ABI_TEST) + +-include $(OBJS:.o=.d) + +# ─── sed substitution shared by all rendered packaging files ─────────── +# +# Packaging files (.pc, CMake config) embed the install paths, so they +# MUST be re-rendered with the current PREFIX/LIBDIR/INCLUDEDIR at +# install time — caching the rendered file would lock in stale paths. + +SED_SUBS = \ + -e 's|@PREFIX@|$(PREFIX)|g' \ + -e 's|@LIBDIR@|$(LIBDIR)|g' \ + -e 's|@INCLUDEDIR@|$(INCLUDEDIR)/snakedrv|g' \ + -e 's|@SNAKEDRV_VERSION@|$(SNAKEDRV_VERSION)|g' \ + -e 's|@SNAKEDRV_VERSION_MAJOR@|$(SNAKEDRV_VERSION_MAJOR)|g' \ + -e 's|@SNAKEDRV_VERSION_MINOR@|$(SNAKEDRV_VERSION_MINOR)|g' \ + -e 's|@SNAKEDRV_VERSION_PATCH@|$(SNAKEDRV_VERSION_PATCH)|g' \ + -e 's|@SNAKEDRV_ABI_VERSION@|$(SNAKEDRV_ABI_VERSION)|g' + +# ─── Install ─────────────────────────────────────────────────────────── + +install: install-lib install-headers install-pkgconfig install-cmake + +install-lib: $(TARGET) + @echo " INSTALL $(D_LIBDIR)/$(TARGET)" + @$(INSTALL) -d $(D_LIBDIR) + @$(INSTALL) -m 0755 $(TARGET) $(D_LIBDIR)/ + +install-headers: + @echo " INSTALL $(D_INCLUDEDIR)/" + @$(INSTALL) -d $(D_INCLUDEDIR) + @$(INSTALL) -m 0644 $(PUBLIC_HEADERS) $(D_INCLUDEDIR)/ + +install-pkgconfig: + @echo " INSTALL $(D_PCDIR)/snakedrv.pc" + @$(INSTALL) -d $(D_PCDIR) + @sed $(SED_SUBS) $(PACKAGING_DIR)/pkgconfig/snakedrv.pc.in \ + > $(D_PCDIR)/snakedrv.pc + @chmod 0644 $(D_PCDIR)/snakedrv.pc + +install-cmake: + @echo " INSTALL $(D_CMAKEDIR)/" + @$(INSTALL) -d $(D_CMAKEDIR) + @sed $(SED_SUBS) $(PACKAGING_DIR)/cmake/SnakeDrvConfig.cmake.in \ + > $(D_CMAKEDIR)/SnakeDrvConfig.cmake + @sed $(SED_SUBS) $(PACKAGING_DIR)/cmake/SnakeDrvConfigVersion.cmake.in \ + > $(D_CMAKEDIR)/SnakeDrvConfigVersion.cmake + @chmod 0644 $(D_CMAKEDIR)/SnakeDrvConfig.cmake \ + $(D_CMAKEDIR)/SnakeDrvConfigVersion.cmake + +# ─── Clean ───────────────────────────────────────────────────────────── + +clean: + rm -rf $(BUILD_DIR) $(TARGET) diff --git a/userland/include/libsnakedrv.hpp b/userland/include/libsnakedrv.hpp index a0817bf..942bfc8 100755 --- a/userland/include/libsnakedrv.hpp +++ b/userland/include/libsnakedrv.hpp @@ -851,12 +851,8 @@ class Driver { std::optional getProcessInfo(ProcessId pid); /** * getKernelThreads - Query thread list from the driver - * - * Note: This is currently a stub and returns an empty list until - * the kernel-side implementation is added. - * * @pid: Target PID - * @return Vector of KernelThreadInfo (currently empty) + * @return Vector of KernelThreadInfo */ std::vector getKernelThreads(ProcessId pid); diff --git a/userland/include/snakedrv.h b/userland/include/snakedrv.h index 1809ba5..df21cc9 100755 --- a/userland/include/snakedrv.h +++ b/userland/include/snakedrv.h @@ -23,10 +23,23 @@ * Version & Device Information * ============================================================================ */ -#define SNAKEDRV_VERSION_MAJOR 1 +#define SNAKEDRV_VERSION_MAJOR 2 #define SNAKEDRV_VERSION_MINOR 0 #define SNAKEDRV_VERSION_PATCH 0 -#define SNAKEDRV_VERSION_STRING "1.0.0" +#define SNAKEDRV_VERSION_STRING "2.0.0" + +/* + * Wire-compatibility ABI version. + * + * Bumped ONLY when an existing IOCTL changes its struct layout, semantics, + * or is removed. Adding new IOCTLs or appending into reserved[] regions of + * existing structs does NOT bump the ABI. + * + * Userland clients should compare this against the value returned by + * SNAKE_IOCTL_GET_INFO.abi_version and refuse to operate if the running + * driver reports a lower ABI than the one the client was built against. + */ +#define SNAKEDRV_ABI_VERSION 2 #define SNAKEDRV_DEVICE_NAME "snakedrv" #define SNAKEDRV_DEVICE_PATH "/dev/snakedrv" @@ -384,24 +397,34 @@ struct snake_virt_to_phys { /** * @struct snake_driver_info * @brief Driver information and capabilities + * + * NOTE on layout: appending fields at the end of this struct changes the + * struct size, which in turn changes the IOCTL number encoded by _IOR(). + * That means any field addition is a breaking change at the IOCTL level. + * To grow the struct without bumping SNAKEDRV_ABI_VERSION, repurpose bytes + * from `reserved[]`. */ struct snake_driver_info { uint32_t version_major; uint32_t version_minor; uint32_t version_patch; char version_string[32]; - + uint32_t capabilities; /* Capability flags */ uint32_t max_breakpoints;/* Maximum hardware breakpoints */ uint32_t max_attached; /* Maximum attached processes */ - + uint64_t kernel_version; /* Running kernel version */ char kernel_release[64]; - + uint32_t arch; /* Architecture (x86_64 = 1) */ uint32_t page_size; /* System page size */ - + int32_t result; + + /* === Added in ABI 1 === */ + uint32_t abi_version; /* SNAKEDRV_ABI_VERSION reported by the driver */ + uint8_t reserved[16]; /* Zero-filled; for future fields within ABI 1 */ } __attribute__((packed)); /* Capability flags */ diff --git a/userland/include/snakedrv_scanner.h b/userland/include/snakedrv_scanner.h index c1957c2..6220e17 100644 --- a/userland/include/snakedrv_scanner.h +++ b/userland/include/snakedrv_scanner.h @@ -10,6 +10,8 @@ #ifndef _SNAKEDRV_SCANNER_H_ #define _SNAKEDRV_SCANNER_H_ +#include "snakedrv.h" + #include #include @@ -231,15 +233,13 @@ struct snake_scan_options { * IOCTL Definitions * ============================================================================ */ -#ifndef SNAKEDRV_IOCTL_MAGIC -#define SNAKEDRV_IOCTL_MAGIC 'S' -#endif - -/* Scanner IOCTLs (0x60-0x6F) */ -#define SNAKE_IOCTL_SCAN_EXECUTE _IOWR(SNAKEDRV_IOCTL_MAGIC, 0x60, struct snake_scan_execute) -#define SNAKE_IOCTL_SCAN_GET_RESULTS _IOWR(SNAKEDRV_IOCTL_MAGIC, 0x61, struct snake_scan_execute) -#define SNAKE_IOCTL_SCAN_FREE_RESULTS _IOW(SNAKEDRV_IOCTL_MAGIC, 0x62, __u32) -#define SNAKE_IOCTL_SCAN_GET_INFO _IOWR(SNAKEDRV_IOCTL_MAGIC, 0x63, struct snake_scan_result_set_info) +/* Scanner IOCTLs (0x90-0x9F) + * Keep this range separate from injection/shadow memory (0x60-0x6F). + */ +#define SNAKE_IOCTL_SCAN_EXECUTE _IOWR(SNAKEDRV_IOCTL_MAGIC, 0x90, struct snake_scan_execute) +#define SNAKE_IOCTL_SCAN_GET_RESULTS _IOWR(SNAKEDRV_IOCTL_MAGIC, 0x91, struct snake_scan_execute) +#define SNAKE_IOCTL_SCAN_FREE_RESULTS _IOW(SNAKEDRV_IOCTL_MAGIC, 0x92, __u32) +#define SNAKE_IOCTL_SCAN_GET_INFO _IOWR(SNAKEDRV_IOCTL_MAGIC, 0x93, struct snake_scan_result_set_info) /* Backend IOCTLs (0x70-0x7F) */ #define SNAKE_IOCTL_GET_BACKEND_INFO _IOR(SNAKEDRV_IOCTL_MAGIC, 0x70, struct snake_backend_info) diff --git a/userland/selftests/manual_map_payload.cpp b/userland/selftests/manual_map_payload.cpp new file mode 100644 index 0000000..36d7957 --- /dev/null +++ b/userland/selftests/manual_map_payload.cpp @@ -0,0 +1,4 @@ +extern "C" void ManualMapEntry(void* arg) { + volatile void* sink = arg; + (void)sink; +} diff --git a/userland/selftests/test_ioctl_abi.cpp b/userland/selftests/test_ioctl_abi.cpp new file mode 100644 index 0000000..e44a862 --- /dev/null +++ b/userland/selftests/test_ioctl_abi.cpp @@ -0,0 +1,64 @@ +#include "snakedrv.h" +#include "snakedrv_scanner.h" + +#include +#include +#include +#include + +constexpr std::array kIoctls = { + _IOC_NR(SNAKE_IOCTL_READ_MEMORY), + _IOC_NR(SNAKE_IOCTL_WRITE_MEMORY), + _IOC_NR(SNAKE_IOCTL_QUERY_MEMORY), + _IOC_NR(SNAKE_IOCTL_READ_PHYS), + _IOC_NR(SNAKE_IOCTL_WRITE_PHYS), + _IOC_NR(SNAKE_IOCTL_VIRT_TO_PHYS), + _IOC_NR(SNAKE_IOCTL_PROCESS_OP), + _IOC_NR(SNAKE_IOCTL_GET_PROC_INFO), + _IOC_NR(SNAKE_IOCTL_DEBUG_ATTACH), + _IOC_NR(SNAKE_IOCTL_DEBUG_CONTROL), + _IOC_NR(SNAKE_IOCTL_SET_BREAKPOINT), + _IOC_NR(SNAKE_IOCTL_CLEAR_BREAKPOINT), + _IOC_NR(SNAKE_IOCTL_POLL_EVENTS), + _IOC_NR(SNAKE_IOCTL_GET_REGS), + _IOC_NR(SNAKE_IOCTL_SET_REGS), + _IOC_NR(SNAKE_IOCTL_GET_INFO), + _IOC_NR(SNAKE_IOCTL_INJECT_ALLOC), + _IOC_NR(SNAKE_IOCTL_INJECT_PROTECT), + _IOC_NR(SNAKE_IOCTL_INJECT_THREAD), + _IOC_NR(SNAKE_IOCTL_INJECT_STEALTH), + _IOC_NR(SNAKE_IOCTL_SHADOW_ALLOC), + _IOC_NR(SNAKE_IOCTL_SHADOW_WRITE), + _IOC_NR(SNAKE_IOCTL_SHADOW_FREE), + _IOC_NR(SNAKE_IOCTL_SCAN_EXECUTE), + _IOC_NR(SNAKE_IOCTL_SCAN_GET_RESULTS), + _IOC_NR(SNAKE_IOCTL_SCAN_FREE_RESULTS), + _IOC_NR(SNAKE_IOCTL_SCAN_GET_INFO), + _IOC_NR(SNAKE_IOCTL_GET_BACKEND_INFO), + _IOC_NR(SNAKE_IOCTL_SET_BACKEND), + _IOC_NR(SNAKE_IOCTL_GET_PERF_STATS), + _IOC_NR(SNAKE_IOCTL_RESET_PERF_STATS), + _IOC_NR(SNAKE_IOCTL_GET_SCAN_OPTIONS), + _IOC_NR(SNAKE_IOCTL_SET_SCAN_OPTIONS), +}; + +constexpr bool all_unique() +{ + for (std::size_t i = 0; i < kIoctls.size(); ++i) { + for (std::size_t j = i + 1; j < kIoctls.size(); ++j) { + if (kIoctls[i] == kIoctls[j]) { + return false; + } + } + } + return true; +} + +static_assert(all_unique(), "SnakeDrv public IOCTL numbers must be unique"); + +int main() +{ + std::cout << "SNAKEDRV_ABI_VERSION=" << SNAKEDRV_ABI_VERSION << "\n"; + std::cout << "public IOCTL numbers are unique\n"; + return 0; +} diff --git a/userland/selftests/test_manualmap_elf.cpp b/userland/selftests/test_manualmap_elf.cpp new file mode 100644 index 0000000..df88e90 --- /dev/null +++ b/userland/selftests/test_manualmap_elf.cpp @@ -0,0 +1,26 @@ +#include "snakedrv_elf.hpp" + +#include +#include + +int main(int argc, char** argv) { + if (argc != 2) { + std::cerr << "usage: test_manualmap_elf \n"; + return 2; + } + + snakedrv::ElfParser parser(argv[1]); + if (!parser.parse()) { + std::cerr << "failed to parse ELF payload\n"; + return 1; + } + + uint64_t entry = parser.get_symbol_offset("ManualMapEntry"); + if (entry == 0) { + std::cerr << "ManualMapEntry was not found\n"; + return 1; + } + + std::cout << "ManualMapEntry=0x" << std::hex << entry << "\n"; + return 0; +} diff --git a/userland/src/libsnakedrv.cpp b/userland/src/libsnakedrv.cpp index 63c25ef..e4684b2 100755 --- a/userland/src/libsnakedrv.cpp +++ b/userland/src/libsnakedrv.cpp @@ -20,6 +20,8 @@ #include #include +extern "C" int snake_inject_library(int fd, int pid, const char* path); + namespace snake { /* ============================================================================ @@ -861,16 +863,36 @@ Driver::InjectionResult Driver::injectThread(ProcessId pid, Address entryPoint, } /** - * Driver::manualMapLibrary - Manual map an ELF shared object (stub) - * - * This is not yet implemented in the userland wrapper. + * Driver::manualMapLibrary - Manual map an ELF shared object */ Driver::InjectionResult Driver::manualMapLibrary(ProcessId pid, const std::string& libraryPath) { InjectionResult result{false, 0, 0, ""}; - // TODO: Implement full manual mapping - // For now, return a placeholder error - result.errorMsg = "Manual mapping not yet implemented. Use Allocate + Write + Protect workflow."; + if (!isOpen()) { + result.errorMsg = "Driver not open"; + return result; + } + + if (libraryPath.empty()) { + result.errorMsg = "Library path is empty"; + return result; + } + + bool wasAttached = isAttached() && attachedPid() == pid; + if (!wasAttached && !attach(pid)) { + result.errorMsg = "Failed to attach for manual mapping"; + return result; + } + + if (snake_inject_library(impl_->fd, pid, libraryPath.c_str()) != 0) { + result.errorCode = errno; + result.errorMsg = "Manual mapping failed"; + if (!wasAttached) detach(); + return result; + } + + if (!wasAttached) detach(); + result.success = true; return result; } @@ -989,12 +1011,46 @@ std::optional Driver::getProcessInfo(ProcessId pid) } /** - * Driver::getKernelThreads - Return kernel thread list (not implemented) - * - * TODO: Implement thread enumeration via kernel IOCTL. + * Driver::getKernelThreads - Return thread list for a process */ std::vector Driver::getKernelThreads(ProcessId pid) { - return {}; + std::vector result; + std::string taskDir = "/proc/" + std::to_string(pid) + "/task"; + DIR* dir = opendir(taskDir.c_str()); + if (!dir) return result; + + struct dirent* entry; + while ((entry = readdir(dir)) != nullptr) { + char* endptr = nullptr; + long tid = strtol(entry->d_name, &endptr, 10); + if (*endptr != '\0' || tid <= 0) continue; + + KernelThreadInfo info{}; + info.tid = static_cast(tid); + info.kernelStack = 0; + + std::string commPath = taskDir + "/" + entry->d_name + "/comm"; + std::ifstream commFile(commPath); + if (commFile.is_open()) { + std::getline(commFile, info.name); + } + + std::string statPath = taskDir + "/" + entry->d_name + "/stat"; + std::ifstream statFile(statPath); + std::string statLine; + if (std::getline(statFile, statLine)) { + auto closeParen = statLine.rfind(')'); + if (closeParen != std::string::npos && + closeParen + 2 < statLine.size()) { + info.state = static_cast(statLine[closeParen + 2]); + } + } + + result.push_back(std::move(info)); + } + + closedir(dir); + return result; } /* ============================================================================ diff --git a/userland/src/snakedrv_injector.cpp b/userland/src/snakedrv_injector.cpp index 46ec801..eed6c76 100644 --- a/userland/src/snakedrv_injector.cpp +++ b/userland/src/snakedrv_injector.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,45 @@ namespace snakedrv { +static bool checked_add_u64(uint64_t a, uint64_t b, uint64_t& out) { + if (b > std::numeric_limits::max() - a) return false; + out = a + b; + return true; +} + +static bool range_fits(size_t container_size, uint64_t offset, uint64_t size) { + uint64_t end; + return checked_add_u64(offset, size, end) && end <= container_size; +} + +static uint64_t image_base_vaddr(const ElfImage& image) { + uint64_t base = std::numeric_limits::max(); + for (const auto& seg : image.segments) { + if (seg.virtual_address < base) base = seg.virtual_address; + } + return base == std::numeric_limits::max() ? 0 : base; +} + +static uint8_t* image_vaddr_ptr(ElfImage& image, uint64_t vaddr, size_t size) { + uint64_t base = image_base_vaddr(image); + uint64_t end; + if (!checked_add_u64(vaddr, size, end)) return nullptr; + + for (const auto& seg : image.segments) { + uint64_t seg_end; + if (!checked_add_u64(seg.virtual_address, seg.memory_size, seg_end)) + continue; + if (vaddr < seg.virtual_address || end > seg_end) + continue; + + uint64_t offset = vaddr - base; + if (!range_fits(image.raw_image.size(), offset, size)) + return nullptr; + return image.raw_image.data() + offset; + } + return nullptr; +} + /* * Remote Process Reader Implementation * Handles communication with the driver and caching of remote module info. @@ -46,7 +86,7 @@ class DriverRemoteReader : public RemoteReader { */ DriverRemoteReader(int fd, pid_t pid) : driver_fd(fd), target_pid(pid) { // Must attach to process to perform read operations - struct snake_debug_attach attach = {0}; + struct snake_debug_attach attach{}; attach.pid = pid; attach.flags = 0; // No suspend needed attach.result = 0; @@ -77,7 +117,7 @@ class DriverRemoteReader : public RemoteReader { while (remaining > 0) { size_t chunk = (remaining > MAX_CHUNK) ? MAX_CHUNK : remaining; - struct snake_memory_op op = {0}; + struct snake_memory_op op{}; op.pid = target_pid; op.address = current_addr; op.size = chunk; @@ -294,7 +334,7 @@ class DriverRemoteReader : public RemoteReader { std::vector dyns(dyn_size / sizeof(Elf64_Dyn)); if (!read(dyn_addr, dyns.data(), dyn_size)) return; - uint64_t strtab = 0, symtab = 0, syment = 0; + uint64_t strtab = 0, symtab = 0, hash = 0; size_t strsz = 0; for (const auto& d : dyns) { @@ -302,38 +342,47 @@ class DriverRemoteReader : public RemoteReader { case DT_STRTAB: strtab = d.d_un.d_ptr; break; case DT_SYMTAB: symtab = d.d_un.d_ptr; break; case DT_STRSZ: strsz = d.d_un.d_val; break; - case DT_SYMENT: syment = d.d_un.d_val; break; + case DT_HASH: hash = d.d_un.d_ptr; break; } } - if (!strtab || !symtab) return; + if (!strtab || !symtab || !strsz) return; // Adjust pointers if they are offsets (common in PIE/PIC) if (strtab < base) strtab += base; if (symtab < base) symtab += base; - - // 4. Bulk Read Symbol Table and String Table - // This is the Optimization Key: Read huge chunks instead of ping-ponging - - if (strtab < base) strtab += base; - if (symtab < base) symtab += base; + if (hash && hash < base) hash += base; // Dynamic Size Calculation size_t sym_data_size = 0; size_t str_data_size = strsz; + size_t num_syms = 0; + + if (hash) { + uint32_t hash_header[2] = {}; + if (read(hash, hash_header, sizeof(hash_header))) { + num_syms = hash_header[1]; + sym_data_size = num_syms * sizeof(Elf64_Sym); + } + } - if (strtab > symtab) { + if (sym_data_size == 0 && strtab > symtab) { // Common case: symtab is immediately followed by strtab sym_data_size = strtab - symtab; - } else { + num_syms = sym_data_size / sizeof(Elf64_Sym); + } + + if (sym_data_size == 0) { // Fallback if layout is weird (e.g. strtab before symtab) // Read a reasonable amount, but try not to over-read. sym_data_size = 512 * 1024; // 512KB safe bet? + num_syms = sym_data_size / sizeof(Elf64_Sym); } // Safety cap for fallback if (str_data_size > 32 * 1024 * 1024) str_data_size = 32 * 1024 * 1024; // Cap at 32MB if (sym_data_size > 32 * 1024 * 1024) sym_data_size = 32 * 1024 * 1024; + num_syms = std::min(num_syms, sym_data_size / sizeof(Elf64_Sym)); std::vector sym_data(sym_data_size); std::vector str_data(str_data_size); @@ -356,16 +405,17 @@ class DriverRemoteReader : public RemoteReader { } // 5. Parse Symbols locally - size_t num_syms = sym_data_size / sizeof(Elf64_Sym); Elf64_Sym* syms = (Elf64_Sym*)sym_data.data(); LOG_INFO("DEBUG: Parsing %zu symbols...", num_syms); auto& cache = export_cache[base]; - int debug_count = 0; for (size_t i = 0; i < num_syms; i++) { if (syms[i].st_name >= str_data_size) continue; // Out of read bounds + if (memchr(str_data.data() + syms[i].st_name, '\0', + str_data_size - syms[i].st_name) == nullptr) + continue; // Only care about defined global/weak functions unsigned char type = ELF64_ST_TYPE(syms[i].st_info); @@ -416,6 +466,8 @@ bool ElfParser::load_file() { if (!file.is_open()) return false; std::streamsize size = file.tellg(); + if (size < static_cast(sizeof(Elf64_Ehdr))) + return false; file.seekg(0, std::ios::beg); file_data.resize(size); @@ -423,7 +475,19 @@ bool ElfParser::load_file() { ehdr = (Elf64_Ehdr*)file_data.data(); if (memcmp(ehdr->e_ident, ELFMAG, SELFMAG) != 0) return false; + if (ehdr->e_ident[EI_CLASS] != ELFCLASS64) return false; + if (ehdr->e_ident[EI_DATA] != ELFDATA2LSB) return false; if (ehdr->e_machine != EM_X86_64) return false; + if (ehdr->e_phentsize != sizeof(Elf64_Phdr)) return false; + if (!range_fits(file_data.size(), ehdr->e_phoff, + static_cast(ehdr->e_phnum) * sizeof(Elf64_Phdr))) + return false; + if (ehdr->e_shnum > 0) { + if (ehdr->e_shentsize != sizeof(Elf64_Shdr)) return false; + if (!range_fits(file_data.size(), ehdr->e_shoff, + static_cast(ehdr->e_shnum) * sizeof(Elf64_Shdr))) + return false; + } phdr = (Elf64_Phdr*)(file_data.data() + ehdr->e_phoff); return true; @@ -440,13 +504,23 @@ bool ElfParser::parse() { for (int i = 0; i < ehdr->e_phnum; i++) { if (phdr[i].p_type == PT_LOAD) { + uint64_t seg_end; + if (phdr[i].p_filesz > phdr[i].p_memsz) + return false; + if (!checked_add_u64(phdr[i].p_vaddr, phdr[i].p_memsz, seg_end)) + return false; + if (!range_fits(file_data.size(), phdr[i].p_offset, phdr[i].p_filesz)) + return false; if (phdr[i].p_vaddr < min_vaddr) min_vaddr = phdr[i].p_vaddr; - if (phdr[i].p_vaddr + phdr[i].p_memsz > max_vaddr) - max_vaddr = phdr[i].p_vaddr + phdr[i].p_memsz; + if (seg_end > max_vaddr) max_vaddr = seg_end; } } + if (min_vaddr == UINT64_MAX || max_vaddr <= min_vaddr) + return false; image.total_size = max_vaddr - min_vaddr; + if (image.total_size > 512ULL * 1024ULL * 1024ULL) + return false; image.base_address = 0; image.entry_point = ehdr->e_entry; image.raw_image.resize(image.total_size, 0); @@ -461,7 +535,10 @@ bool ElfParser::parse() { seg.flags = phdr[i].p_flags; if (seg.file_size > 0) { - memcpy(image.raw_image.data() + (seg.virtual_address - min_vaddr), + uint64_t image_offset = seg.virtual_address - min_vaddr; + if (!range_fits(image.raw_image.size(), image_offset, seg.file_size)) + return false; + memcpy(image.raw_image.data() + image_offset, file_data.data() + seg.file_offset, seg.file_size); } @@ -477,23 +554,18 @@ bool ElfParser::parse() { * ElfParser::collect_imports - Collect external relocations */ void ElfParser::collect_imports() { - // Helper to find segment pointer - auto vaddr_to_ptr = [&](uint64_t vaddr) -> uint8_t* { - for(const auto& seg : image.segments) { - if(vaddr >= seg.virtual_address && vaddr < seg.virtual_address + seg.file_size) { - return image.raw_image.data() + (vaddr - image.segments[0].virtual_address); - } - } - return nullptr; - }; - Elf64_Dyn* dyn = nullptr; - uint64_t dyn_size = 0; + size_t dyn_count = 0; for (int i = 0; i < ehdr->e_phnum; i++) { if (phdr[i].p_type == PT_DYNAMIC) { + if (phdr[i].p_filesz % sizeof(Elf64_Dyn) != 0 || + !range_fits(file_data.size(), phdr[i].p_offset, phdr[i].p_filesz)) { + LOG_ERR("Invalid PT_DYNAMIC bounds"); + return; + } dyn = (Elf64_Dyn*)(file_data.data() + phdr[i].p_offset); - dyn_size = phdr[i].p_filesz / sizeof(Elf64_Dyn); + dyn_count = phdr[i].p_filesz / sizeof(Elf64_Dyn); break; } } @@ -506,8 +578,10 @@ void ElfParser::collect_imports() { uint64_t jmprel_sz = 0; uint64_t symtab_offset = 0; uint64_t strtab_offset = 0; + uint64_t strtab_size = 0; + uint64_t hash_offset = 0; - for (size_t i = 0; i < dyn_size; i++) { + for (size_t i = 0; i < dyn_count && dyn[i].d_tag != DT_NULL; i++) { switch (dyn[i].d_tag) { case DT_RELA: rela_offset = dyn[i].d_un.d_ptr; break; case DT_RELASZ: rela_sz = dyn[i].d_un.d_val; break; @@ -516,55 +590,124 @@ void ElfParser::collect_imports() { case DT_PLTRELSZ: jmprel_sz = dyn[i].d_un.d_val; break; case DT_SYMTAB: symtab_offset = dyn[i].d_un.d_ptr; break; case DT_STRTAB: strtab_offset = dyn[i].d_un.d_ptr; break; + case DT_STRSZ: strtab_size = dyn[i].d_un.d_val; break; + case DT_HASH: hash_offset = dyn[i].d_un.d_ptr; break; } } - Elf64_Sym* symtab = (Elf64_Sym*)vaddr_to_ptr(symtab_offset); - char* strtab = (char*)vaddr_to_ptr(strtab_offset); + size_t sym_count = 0; + if (ehdr->e_shnum > 0) { + const Elf64_Shdr* shdr = + (const Elf64_Shdr*)(file_data.data() + ehdr->e_shoff); + for (uint16_t i = 0; i < ehdr->e_shnum; i++) { + if ((shdr[i].sh_type == SHT_DYNSYM || shdr[i].sh_type == SHT_SYMTAB) && + shdr[i].sh_addr == symtab_offset && + shdr[i].sh_entsize == sizeof(Elf64_Sym)) { + sym_count = shdr[i].sh_size / sizeof(Elf64_Sym); + if (shdr[i].sh_link < ehdr->e_shnum && strtab_size == 0) + strtab_size = shdr[shdr[i].sh_link].sh_size; + break; + } + } + } + + if (sym_count == 0 && hash_offset) { + uint32_t* hash = (uint32_t*)image_vaddr_ptr(image, hash_offset, + 2 * sizeof(uint32_t)); + if (hash) + sym_count = hash[1]; + } + + if (!symtab_offset || !strtab_offset || !strtab_size || !sym_count) { + LOG_ERR("ELF dynamic symbol metadata is incomplete"); + return; + } + if (sym_count > image.raw_image.size() / sizeof(Elf64_Sym)) { + LOG_ERR("ELF dynamic symbol count is out of bounds"); + return; + } + + Elf64_Sym* symtab = (Elf64_Sym*)image_vaddr_ptr( + image, symtab_offset, sym_count * sizeof(Elf64_Sym)); + char* strtab = (char*)image_vaddr_ptr(image, strtab_offset, strtab_size); if (!symtab || !strtab) { - // LOG_DBG("Could not map dynamic sections to local image"); + LOG_ERR("Dynamic symbol/string table is out of mapped image bounds"); return; } - // Process DT_RELA (Data Relocations) - if (rela_offset != 0 && rela_sz > 0) { - Elf64_Rela* rela = (Elf64_Rela*)vaddr_to_ptr(rela_offset); - if (rela) { - size_t count = rela_sz / rela_ent; - for (size_t i = 0; i < count; i++) { - uint32_t type = ELF64_R_TYPE(rela[i].r_info); - uint32_t sym_idx = ELF64_R_SYM(rela[i].r_info); - - if (type == R_X86_64_GLOB_DAT || type == R_X86_64_JUMP_SLOT) { - if (sym_idx != 0) { - if (symtab[sym_idx].st_shndx != SHN_UNDEF) { - // Internal - image.internal_relocs.push_back(rela[i].r_offset); - uint64_t* patch_loc = (uint64_t*)(image.raw_image.data() + rela[i].r_offset); - *patch_loc = symtab[sym_idx].st_value; - } else { - // External - std::string name = strtab + symtab[sym_idx].st_name; - image.pending_imports.push_back({name, rela[i].r_offset, type}); - } - } + auto symbol_name = [&](uint32_t sym_idx) -> const char* { + if (sym_idx >= sym_count || symtab[sym_idx].st_name >= strtab_size) + return nullptr; + char* name = strtab + symtab[sym_idx].st_name; + size_t remaining = strtab_size - symtab[sym_idx].st_name; + return memchr(name, '\0', remaining) ? name : nullptr; + }; + + auto collect_rela = [&](uint64_t rela_vaddr, uint64_t rela_size, + uint64_t rela_entry_size) { + if (!rela_vaddr || !rela_size) return; + if (rela_entry_size == 0) rela_entry_size = sizeof(Elf64_Rela); + if (rela_entry_size != sizeof(Elf64_Rela) || + rela_size % sizeof(Elf64_Rela) != 0) { + LOG_ERR("Unsupported RELA entry size"); + return; + } + + size_t count = rela_size / sizeof(Elf64_Rela); + Elf64_Rela* rela = (Elf64_Rela*)image_vaddr_ptr(image, rela_vaddr, + rela_size); + if (!rela) { + LOG_ERR("Relocation table out of mapped image bounds"); + return; + } + + for (size_t i = 0; i < count; i++) { + uint32_t type = ELF64_R_TYPE(rela[i].r_info); + uint32_t sym_idx = ELF64_R_SYM(rela[i].r_info); + + if (type != R_X86_64_GLOB_DAT && type != R_X86_64_JUMP_SLOT) + continue; + if (sym_idx == 0 || sym_idx >= sym_count) + continue; + + uint64_t* patch_loc = (uint64_t*)image_vaddr_ptr( + image, rela[i].r_offset, sizeof(uint64_t)); + if (!patch_loc) { + LOG_ERR("Relocation target 0x%lx out of mapped image bounds", + rela[i].r_offset); + continue; + } + + if (symtab[sym_idx].st_shndx != SHN_UNDEF) { + image.internal_relocs.push_back(rela[i].r_offset); + *patch_loc = symtab[sym_idx].st_value; + } else { + const char* name = symbol_name(sym_idx); + if (!name || name[0] == '\0') { + LOG_ERR("Invalid external symbol name at relocation %zu", i); + continue; } + image.pending_imports.push_back({name, rela[i].r_offset, type}); } } - } + }; + + collect_rela(rela_offset, rela_sz, rela_ent); /* Collect .init_array entries from DT_INIT_ARRAY / DT_INIT_ARRAYSZ */ { uint64_t init_arr_addr = 0, init_arr_sz = 0; - for (size_t i = 0; i < dyn_size; i++) { + for (size_t i = 0; i < dyn_count && dyn[i].d_tag != DT_NULL; i++) { if (dyn[i].d_tag == DT_INIT_ARRAY) init_arr_addr = dyn[i].d_un.d_ptr; if (dyn[i].d_tag == DT_INIT_ARRAYSZ) init_arr_sz = dyn[i].d_un.d_val; } - if (init_arr_addr && init_arr_sz) { - uint64_t *arr = (uint64_t *)vaddr_to_ptr(init_arr_addr); + if (init_arr_addr && init_arr_sz && + init_arr_sz % sizeof(uint64_t) == 0) { + uint64_t *arr = (uint64_t *)image_vaddr_ptr(image, init_arr_addr, + init_arr_sz); size_t count = init_arr_sz / sizeof(uint64_t); if (arr) { for (size_t i = 0; i < count; i++) { @@ -576,59 +719,24 @@ void ElfParser::collect_imports() { } } - // Process DT_JMPREL (PLT Relocations - Functions) - if (jmprel_offset != 0 && jmprel_sz > 0) { - Elf64_Rela* rela = (Elf64_Rela*)vaddr_to_ptr(jmprel_offset); - if (rela) { - size_t count = jmprel_sz / sizeof(Elf64_Rela); - for (size_t i = 0; i < count; i++) { - uint32_t type = ELF64_R_TYPE(rela[i].r_info); - uint32_t sym_idx = ELF64_R_SYM(rela[i].r_info); - - if (type == R_X86_64_JUMP_SLOT || type == R_X86_64_GLOB_DAT) { - if (sym_idx != 0) { - if (symtab[sym_idx].st_shndx != SHN_UNDEF) { - // Internal - image.internal_relocs.push_back(rela[i].r_offset); - uint64_t* patch_loc = (uint64_t*)(image.raw_image.data() + rela[i].r_offset); - *patch_loc = symtab[sym_idx].st_value; - } else { - // External - std::string name = strtab + symtab[sym_idx].st_name; - image.pending_imports.push_back({name, rela[i].r_offset, type}); - } - } - } - } - } - } + collect_rela(jmprel_offset, jmprel_sz, sizeof(Elf64_Rela)); } /** * ElfParser::relocate_base - Apply base relocations */ bool ElfParser::relocate_base(uint64_t target_base) { - // Helper to find segment pointer - auto vaddr_to_ptr = [&](uint64_t vaddr) -> uint8_t* { - for(const auto& seg : image.segments) { - if(vaddr >= seg.virtual_address && vaddr < seg.virtual_address + seg.memory_size) { - // Check if offset is within file size (data exists) - if (vaddr < seg.virtual_address + seg.file_size) { - return image.raw_image.data() + (vaddr - image.segments[0].virtual_address); - } - } - } - return nullptr; - }; - // Find DYNAMIC segment Elf64_Dyn* dyn = nullptr; - uint64_t dyn_size = 0; + size_t dyn_count = 0; for (int i = 0; i < ehdr->e_phnum; i++) { if (phdr[i].p_type == PT_DYNAMIC) { + if (phdr[i].p_filesz % sizeof(Elf64_Dyn) != 0 || + !range_fits(file_data.size(), phdr[i].p_offset, phdr[i].p_filesz)) + return false; dyn = (Elf64_Dyn*)(file_data.data() + phdr[i].p_offset); - dyn_size = phdr[i].p_filesz / sizeof(Elf64_Dyn); + dyn_count = phdr[i].p_filesz / sizeof(Elf64_Dyn); break; } } @@ -639,7 +747,7 @@ bool ElfParser::relocate_base(uint64_t target_base) { uint64_t rela_sz = 0; uint64_t rela_ent = 0; - for (size_t i = 0; i < dyn_size; i++) { + for (size_t i = 0; i < dyn_count && dyn[i].d_tag != DT_NULL; i++) { switch (dyn[i].d_tag) { case DT_RELA: rela_offset = dyn[i].d_un.d_ptr; break; case DT_RELASZ: rela_sz = dyn[i].d_un.d_val; break; @@ -648,19 +756,24 @@ bool ElfParser::relocate_base(uint64_t target_base) { } if (rela_offset == 0) return true; // No relocations needed + if (rela_ent == 0) rela_ent = sizeof(Elf64_Rela); + if (rela_ent != sizeof(Elf64_Rela) || + rela_sz % sizeof(Elf64_Rela) != 0) + return false; - Elf64_Rela* rela = (Elf64_Rela*)vaddr_to_ptr(rela_offset); + Elf64_Rela* rela = (Elf64_Rela*)image_vaddr_ptr(image, rela_offset, rela_sz); if (!rela) { LOG_DBG("Could not map relocation table"); return false; } - size_t count = rela_sz / rela_ent; + size_t count = rela_sz / sizeof(Elf64_Rela); int rel_count = 0; for (size_t i = 0; i < count; i++) { uint32_t type = ELF64_R_TYPE(rela[i].r_info); - uint64_t *target = (uint64_t *)vaddr_to_ptr(rela[i].r_offset); + uint64_t *target = (uint64_t *)image_vaddr_ptr(image, rela[i].r_offset, + sizeof(uint64_t)); if (!target) continue; @@ -702,7 +815,8 @@ bool ElfParser::relocate_base(uint64_t target_base) { // Also handle internal GOT entries (discovered during import collection) for (uint64_t offset : image.internal_relocs) { - uint64_t* target = (uint64_t*)vaddr_to_ptr(offset); + uint64_t* target = (uint64_t*)image_vaddr_ptr(image, offset, + sizeof(uint64_t)); if (target) { // The value at *target is already the internal offset (st_value) // We just add the base. @@ -736,24 +850,14 @@ bool ElfParser::resolve_imports(RemoteReader& reader) { return false; } - // Patch the GOT/PLT entry in our LOCAL raw image - // imp.offset is the VADDR in the image - // We need to map this VADDR to our local buffer - auto vaddr_to_ptr = [&](uint64_t vaddr) -> uint8_t* { - for(const auto& seg : image.segments) { - if(vaddr >= seg.virtual_address && vaddr < seg.virtual_address + seg.file_size) { - return image.raw_image.data() + (vaddr - image.segments[0].virtual_address); - } - } - return nullptr; - }; - - uint64_t* patch_loc = (uint64_t*)vaddr_to_ptr(imp.offset); + uint64_t* patch_loc = (uint64_t*)image_vaddr_ptr( + image, imp.offset, sizeof(uint64_t)); if (patch_loc) { *patch_loc = remote_addr; resolved_count++; } else { LOG_ERR("Failed to patch import at offset %lx (out of bounds)", imp.offset); + return false; } } @@ -765,53 +869,41 @@ bool ElfParser::resolve_imports(RemoteReader& reader) { * ElfParser::get_symbol_offset - Lookup a symbol offset in the local image */ uint64_t ElfParser::get_symbol_offset(const std::string& name) { - auto vaddr_to_ptr = [&](uint64_t vaddr) -> uint8_t* { - for(const auto& seg : image.segments) { - if(vaddr >= seg.virtual_address && vaddr < seg.virtual_address + seg.memory_size) { - if (vaddr < seg.virtual_address + seg.file_size) { - return image.raw_image.data() + (vaddr - image.segments[0].virtual_address); - } - } - } - return nullptr; - }; + if (ehdr->e_shnum == 0) + return 0; - // Helper to find Dynamic Section (repeated logic, should be refactored but safe here) - Elf64_Dyn* dyn = nullptr; - uint64_t dyn_size = 0; - for (int i = 0; i < ehdr->e_phnum; i++) { - if (phdr[i].p_type == PT_DYNAMIC) { - dyn = (Elf64_Dyn*)(file_data.data() + phdr[i].p_offset); - dyn_size = phdr[i].p_filesz / sizeof(Elf64_Dyn); - break; - } - } - if (!dyn) return 0; + const Elf64_Shdr* shdr = + (const Elf64_Shdr*)(file_data.data() + ehdr->e_shoff); - uint64_t symtab_off = 0, strtab_off = 0; - - for (size_t i = 0; i < dyn_size; i++) { - if (dyn[i].d_tag == DT_SYMTAB) symtab_off = dyn[i].d_un.d_ptr; - if (dyn[i].d_tag == DT_STRTAB) strtab_off = dyn[i].d_un.d_ptr; - } - - if (!symtab_off || !strtab_off) return 0; - - Elf64_Sym* symtab = (Elf64_Sym*)vaddr_to_ptr(symtab_off); - char* strtab = (char*)vaddr_to_ptr(strtab_off); - - if (!symtab || !strtab) return 0; - - // We don't know the number of symbols easily without hash table - // But we can iterate until we hit invalid memory or a reasonable limit - // Heuristic: iterate 5000 symbols max - for (int i = 0; i < 5000; i++) { - // Basic bounds check (unsafe if we are at end of page, but ok for now) - if (symtab[i].st_name == 0 && i > 0 && symtab[i].st_value == 0) continue; - - const char* sym_name = strtab + symtab[i].st_name; - if (name == sym_name) { - return symtab[i].st_value; + for (uint16_t s = 0; s < ehdr->e_shnum; s++) { + if (shdr[s].sh_type != SHT_DYNSYM && shdr[s].sh_type != SHT_SYMTAB) + continue; + if (shdr[s].sh_entsize != sizeof(Elf64_Sym) || + shdr[s].sh_link >= ehdr->e_shnum || + !range_fits(file_data.size(), shdr[s].sh_offset, shdr[s].sh_size)) + continue; + + const Elf64_Shdr& str_sh = shdr[shdr[s].sh_link]; + if (str_sh.sh_type != SHT_STRTAB || + !range_fits(file_data.size(), str_sh.sh_offset, str_sh.sh_size)) + continue; + + const Elf64_Sym* symtab = + (const Elf64_Sym*)(file_data.data() + shdr[s].sh_offset); + const char* strtab = + (const char*)(file_data.data() + str_sh.sh_offset); + size_t sym_count = shdr[s].sh_size / sizeof(Elf64_Sym); + size_t str_size = str_sh.sh_size; + + for (size_t i = 0; i < sym_count; i++) { + if (symtab[i].st_name >= str_size) + continue; + const char* sym_name = strtab + symtab[i].st_name; + size_t remaining = str_size - symtab[i].st_name; + if (!memchr(sym_name, '\0', remaining)) + continue; + if (name == sym_name) + return symtab[i].st_value; } } @@ -903,12 +995,9 @@ class ManualMapper { LOG_INFO("Payload written (%zu bytes)", img.total_size); /* - * Stealth (VMA hide) is deferred — applying it before - * execution causes SIGSEGV on TLB misses. The payload can - * request stealth later via ioctl once fully initialized. - * - * TODO: implement deferred stealth from within ManualMapEntry - * after all pages have been faulted in (mlock equivalent). + * Stealth (VMA hide) is intentionally not applied here. + * Hiding before all pages have faulted in can SIGSEGV the target; + * a future implementation must make deferred hiding page-fault-safe. */ /* Step 6: Execute ManualMapEntry via thread hijack */ @@ -920,8 +1009,8 @@ class ManualMapper { LOG_INFO("Entry: ManualMapEntry at +0x%lx", manual_entry); thread.start_address = alloc.address + manual_entry; } else { - LOG_INFO("Entry: ELF entry at +0x%lx", img.entry_point); - thread.start_address = alloc.address + img.entry_point; + LOG_ERR("ManualMapEntry export not found"); + return false; } if (ioctl(driver_fd, SNAKE_IOCTL_INJECT_THREAD, &thread) < 0) {