Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e7e178c0f | ||
|
|
892abe0ccf | ||
|
|
782716c53b | ||
|
|
b2f5d06570 |
@@ -0,0 +1,5 @@
|
||||
signer/target
|
||||
signer/dist
|
||||
signer/.git
|
||||
nostr_core_lib_rust/target
|
||||
nostr_core_lib_rust/.git
|
||||
@@ -1,3 +1,4 @@
|
||||
/target/
|
||||
/dist/
|
||||
*.log
|
||||
*.tar.gz
|
||||
|
||||
Generated
+3
-3
@@ -1446,7 +1446,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-core"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64",
|
||||
@@ -1468,7 +1468,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-nips"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"block-modes",
|
||||
@@ -2207,7 +2207,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "signer"
|
||||
version = "0.0.19"
|
||||
version = "0.0.22"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chacha20poly1305",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "signer"
|
||||
version = "0.0.19"
|
||||
version = "0.0.23"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Attended Nostr signing daemon — Rust port of n_signer"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
FROM rust:1.88.0-alpine3.20
|
||||
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
binutils \
|
||||
file \
|
||||
musl-dev \
|
||||
openssl-dev \
|
||||
openssl-libs-static \
|
||||
perl \
|
||||
pkgconf \
|
||||
linux-headers \
|
||||
&& rustup target add x86_64-unknown-linux-musl
|
||||
|
||||
ENV CARGO_NET_OFFLINE=false \
|
||||
RUSTFLAGS=-Ctarget-cpu=x86-64
|
||||
|
||||
WORKDIR /workspace/signer
|
||||
|
||||
COPY nostr_core_lib_rust /workspace/nostr_core_lib_rust
|
||||
COPY signer /workspace/signer
|
||||
|
||||
RUN cargo build --release --target x86_64-unknown-linux-musl
|
||||
RUN file target/x86_64-unknown-linux-musl/release/signer \
|
||||
&& file target/x86_64-unknown-linux-musl/release/signer-client \
|
||||
&& ! readelf -l target/x86_64-unknown-linux-musl/release/signer | grep -q 'INTERP'
|
||||
|
||||
CMD ["sh", "-c", "mkdir -p /out && cp target/x86_64-unknown-linux-musl/release/signer /out/signer && cp target/x86_64-unknown-linux-musl/release/signer-client /out/signer_client"]
|
||||
@@ -728,12 +728,43 @@ git submodule update --init ratatui
|
||||
|
||||
### 8.2 Local dev build
|
||||
|
||||
Native builds are intended for local development and use the host Rust toolchain and libc:
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
./target/debug/signer --version
|
||||
```
|
||||
|
||||
### 8.3 Release build
|
||||
### 8.3 Portable static musl release build
|
||||
|
||||
Portable release binaries are built in Docker for x86_64 Linux using the `x86_64-unknown-linux-musl` target. This avoids a runtime dependency on the target system's glibc version. The build expects the sibling `nostr_core_lib_rust` checkout described above.
|
||||
|
||||
```bash
|
||||
./build_musl.sh
|
||||
```
|
||||
|
||||
The artifacts are written directly to `dist/`:
|
||||
|
||||
- `signer`
|
||||
- `signer_client`
|
||||
|
||||
Verify the output:
|
||||
|
||||
```bash
|
||||
file dist/signer
|
||||
ldd dist/signer
|
||||
./dist/signer --version
|
||||
```
|
||||
|
||||
`ldd` should report that the executable is not dynamically linked. Static musl removes the glibc runtime dependency, but the binaries still require a compatible Linux kernel, x86_64 CPU, terminal environment, Qubes/qrexec environment where applicable, and sufficient `RLIMIT_MEMLOCK` for locked secret memory.
|
||||
|
||||
To deploy the portable binaries locally:
|
||||
|
||||
```bash
|
||||
./deploy_local.sh --musl
|
||||
```
|
||||
|
||||
### 8.4 Native release build
|
||||
|
||||
The release profile is tuned for a small, optimized, stripped binary:
|
||||
|
||||
@@ -751,12 +782,14 @@ cargo build --release
|
||||
./target/release/signer --version
|
||||
```
|
||||
|
||||
### 8.4 Tests
|
||||
### 8.5 Tests
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
The portable build runs the release compilation and static-linkage checks inside Docker. Runtime smoke tests should be performed on the intended Qubes/Linux deployment environment, including Unix sockets, TCP/HTTP, qrexec, TUI startup, signing, and `mlock` behavior.
|
||||
|
||||
## 9. Project layout
|
||||
|
||||
| Path | Purpose |
|
||||
@@ -769,6 +802,8 @@ cargo test
|
||||
| [`src/role_table.rs`](src/role_table.rs:1) | Role registry, path-template parsing, purpose/curve enforcement |
|
||||
| [`src/selector.rs`](src/selector.rs:1) | Role selector resolution (`role` + `role_path`) |
|
||||
| [`src/enforcement.rs`](src/enforcement.rs:1) | Verb/algorithm/purpose/curve enforcement matrix |
|
||||
| [`Dockerfile.musl`](Dockerfile.musl:1) | Reproducible Docker environment for static musl releases |
|
||||
| [`build_musl.sh`](build_musl.sh:1) | Builds and validates portable x86_64 musl binaries |
|
||||
| [`src/key_store.rs`](src/key_store.rs:1) | BIP-32 / SLIP-0010 key derivation and storage |
|
||||
| [`src/mnemonic.rs`](src/mnemonic.rs:1) | BIP-39 mnemonic loading and seed derivation |
|
||||
| [`src/pq_crypto.rs`](src/pq_crypto.rs:1) | Post-quantum keygen (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) |
|
||||
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Build portable static x86_64 Linux binaries in a pinned musl container.
|
||||
# The Docker build context is the parent directory because Cargo.toml uses
|
||||
# ../nostr_core_lib_rust as a path dependency. Set SIGNER_NOSTR_CORE_DIR
|
||||
# when the sibling checkout is stored elsewhere.
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_NAME="$(basename "${PROJECT_DIR}")"
|
||||
PARENT_DIR="$(dirname "${PROJECT_DIR}")"
|
||||
IMAGE_NAME="${SIGNER_MUSL_IMAGE:-signer-musl-build:rust-1.88.0-alpine3.20}"
|
||||
OUTPUT_DIR="${SIGNER_MUSL_OUTPUT_DIR:-${PROJECT_DIR}/dist}"
|
||||
DEPENDENCY_DIR="${SIGNER_NOSTR_CORE_DIR:-${PARENT_DIR}/nostr_core_lib_rust}"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "error: Docker is required for the musl build" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${PROJECT_DIR}/Cargo.toml" ]]; then
|
||||
echo "error: Cargo.toml not found in ${PROJECT_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${DEPENDENCY_DIR}/core/Cargo.toml" || ! -f "${DEPENDENCY_DIR}/nips/Cargo.toml" ]]; then
|
||||
echo "error: invalid nostr_core_lib_rust checkout: ${DEPENDENCY_DIR}" >&2
|
||||
echo "expected core/Cargo.toml and nips/Cargo.toml" >&2
|
||||
echo "hint: clone the complete sibling checkout or set SIGNER_NOSTR_CORE_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
rm -f "${OUTPUT_DIR}/signer" \
|
||||
"${OUTPUT_DIR}/signer_client"
|
||||
|
||||
CONTAINER_NAME="signer-musl-build-$$-${RANDOM}"
|
||||
cleanup() {
|
||||
docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
printf '[INFO] Building musl image %s\n' "${IMAGE_NAME}"
|
||||
if [[ "${DEPENDENCY_DIR}" != "${PARENT_DIR}/nostr_core_lib_rust" ]]; then
|
||||
echo "error: SIGNER_NOSTR_CORE_DIR must point to the sibling ../nostr_core_lib_rust directory when using Docker" >&2
|
||||
exit 1
|
||||
fi
|
||||
docker build \
|
||||
--file "${PROJECT_DIR}/Dockerfile.musl" \
|
||||
--tag "${IMAGE_NAME}" \
|
||||
"${PARENT_DIR}"
|
||||
|
||||
printf '[INFO] Extracting portable binaries\n'
|
||||
docker create --name "${CONTAINER_NAME}" "${IMAGE_NAME}" >/dev/null
|
||||
docker cp "${CONTAINER_NAME}:/workspace/signer/target/x86_64-unknown-linux-musl/release/signer" \
|
||||
"${OUTPUT_DIR}/signer"
|
||||
docker cp "${CONTAINER_NAME}:/workspace/signer/target/x86_64-unknown-linux-musl/release/signer-client" \
|
||||
"${OUTPUT_DIR}/signer_client"
|
||||
chmod 0755 "${OUTPUT_DIR}/signer" \
|
||||
"${OUTPUT_DIR}/signer_client"
|
||||
|
||||
for binary in \
|
||||
"${OUTPUT_DIR}/signer" \
|
||||
"${OUTPUT_DIR}/signer_client"; do
|
||||
file "${binary}"
|
||||
if ! file "${binary}" | grep -Eq 'ELF 64-bit LSB (pie )?executable, x86-64'; then
|
||||
echo "error: ${binary} is not an x86_64 ELF executable" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ldd "${binary}" 2>&1 | grep -Eqi 'not a dynamic executable|statically linked'; then
|
||||
:
|
||||
else
|
||||
echo "error: ${binary} appears to be dynamically linked" >&2
|
||||
ldd "${binary}" 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
"${binary}" --version
|
||||
done
|
||||
|
||||
printf '[SUCCESS] Portable binaries written to %s\n' "${OUTPUT_DIR}"
|
||||
+36
-17
@@ -3,11 +3,12 @@ set -e
|
||||
|
||||
# signer (Rust) — Local Deploy Script
|
||||
#
|
||||
# Builds release binaries and installs them to /usr/local/bin/.
|
||||
# Builds binaries and installs them to /usr/local/bin/.
|
||||
#
|
||||
# USAGE:
|
||||
# ./deploy_local.sh # build release + install
|
||||
# ./deploy_local.sh --debug # build debug + install
|
||||
# ./deploy_local.sh # native release build + install
|
||||
# ./deploy_local.sh --musl # portable static musl release + install
|
||||
# ./deploy_local.sh --debug # native debug build + install
|
||||
# ./deploy_local.sh -h, --help
|
||||
#
|
||||
# Installs:
|
||||
@@ -29,6 +30,7 @@ print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
||||
PROFILE="release"
|
||||
CARGO_FLAG="--release"
|
||||
TARGET_DIR="target/release"
|
||||
BUILD_KIND="native"
|
||||
|
||||
show_usage() {
|
||||
echo "signer (Rust) Local Deploy Script"
|
||||
@@ -37,7 +39,8 @@ show_usage() {
|
||||
echo " $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "OPTIONS:"
|
||||
echo " --debug Build debug profile instead of release"
|
||||
echo " --musl Build static x86_64 musl binaries in Docker"
|
||||
echo " --debug Build native debug profile instead of release"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Installs to /usr/local/bin/:"
|
||||
@@ -47,7 +50,18 @@ show_usage() {
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--musl)
|
||||
BUILD_KIND="musl"
|
||||
PROFILE="release"
|
||||
CARGO_FLAG=""
|
||||
TARGET_DIR="dist"
|
||||
shift
|
||||
;;
|
||||
--debug)
|
||||
if [[ "$BUILD_KIND" == "musl" ]]; then
|
||||
print_error "--musl currently supports release builds only"
|
||||
exit 1
|
||||
fi
|
||||
PROFILE="debug"
|
||||
CARGO_FLAG=""
|
||||
TARGET_DIR="target/debug"
|
||||
@@ -78,14 +92,20 @@ if ! grep -q 'name = "signer"' Cargo.toml || ! grep -q 'name = "signer-client"'
|
||||
fi
|
||||
|
||||
# Build
|
||||
print_status "Building ${PROFILE} binaries (cargo build ${CARGO_FLAG})..."
|
||||
cargo build ${CARGO_FLAG} 2>&1 | tail -5 || {
|
||||
print_error "Build failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
SIGNER_BIN="${TARGET_DIR}/signer"
|
||||
CLIENT_BIN="${TARGET_DIR}/signer-client"
|
||||
if [[ "$BUILD_KIND" == "musl" ]]; then
|
||||
print_status "Building static musl release binaries in Docker..."
|
||||
./build_musl.sh
|
||||
SIGNER_BIN="${TARGET_DIR}/signer-linux-x86_64-musl"
|
||||
CLIENT_BIN="${TARGET_DIR}/signer-client-linux-x86_64-musl"
|
||||
else
|
||||
print_status "Building ${PROFILE} binaries (cargo build ${CARGO_FLAG})..."
|
||||
cargo build ${CARGO_FLAG} 2>&1 | tail -5 || {
|
||||
print_error "Build failed"
|
||||
exit 1
|
||||
}
|
||||
SIGNER_BIN="${TARGET_DIR}/signer"
|
||||
CLIENT_BIN="${TARGET_DIR}/signer-client"
|
||||
fi
|
||||
|
||||
for bin in "$SIGNER_BIN" "$CLIENT_BIN"; do
|
||||
if [[ ! -f "$bin" ]]; then
|
||||
@@ -106,19 +126,18 @@ fi
|
||||
|
||||
install_binary() {
|
||||
local src="$1"
|
||||
local name
|
||||
name=$(basename "$src")
|
||||
local name="$2"
|
||||
print_status "Installing $name to $DEST_DIR/..."
|
||||
sudo install -m 0755 "$src" "$DEST_DIR/$name"
|
||||
print_success "Installed: $DEST_DIR/$name"
|
||||
}
|
||||
|
||||
install_binary "$SIGNER_BIN"
|
||||
install_binary "$CLIENT_BIN"
|
||||
install_binary "$SIGNER_BIN" "signer"
|
||||
install_binary "$CLIENT_BIN" "signer-client"
|
||||
|
||||
# Verify
|
||||
print_status "Verification:"
|
||||
"$DEST_DIR/signer" --version || true
|
||||
"$DEST_DIR/signer-client" --version || true
|
||||
|
||||
print_success "Local deploy completed (${PROFILE} profile)"
|
||||
print_success "Local deploy completed (${PROFILE} ${BUILD_KIND} profile)"
|
||||
|
||||
+11
-10
@@ -169,18 +169,19 @@ verify_binary_version() {
|
||||
}
|
||||
|
||||
build_release_binary() {
|
||||
print_status "Building release binaries (cargo build --release)..."
|
||||
cargo build --release 2>&1 | tail -5 || return 1
|
||||
print_status "Building static musl release binaries..."
|
||||
./build_musl.sh || return 1
|
||||
|
||||
local bin_path="target/release/signer"
|
||||
local bin_path="dist/signer"
|
||||
verify_binary_version "$bin_path" "$NEW_VERSION" || return 1
|
||||
|
||||
local client_path="target/release/signer-client"
|
||||
local client_path="dist/signer_client"
|
||||
if [[ -f "$client_path" ]]; then
|
||||
print_success "Release binary built: $client_path"
|
||||
verify_binary_version "$client_path" "$NEW_VERSION" || return 1
|
||||
print_success "Portable client binary built: $client_path"
|
||||
fi
|
||||
|
||||
print_success "Release binary built: $bin_path"
|
||||
print_success "Portable signer binary built: $bin_path"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -284,8 +285,8 @@ main() {
|
||||
|
||||
git_commit_and_push
|
||||
|
||||
local binary_path="target/release/signer"
|
||||
local client_path="target/release/signer-client"
|
||||
local binary_path="dist/signer"
|
||||
local client_path="dist/signer_client"
|
||||
local tarball_path=""
|
||||
tarball_path=$(create_source_tarball || true)
|
||||
|
||||
@@ -303,8 +304,8 @@ main() {
|
||||
print_status "Uploading signer-client..."
|
||||
curl -s -X POST "$assets_url" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@$client_path;filename=signer-client" \
|
||||
-F "name=signer-client" > /dev/null
|
||||
-F "attachment=@$client_path;filename=signer_client" \
|
||||
-F "name=signer_client" > /dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
+19
-7
@@ -15,11 +15,15 @@ set -euo pipefail
|
||||
#
|
||||
# Optional env vars:
|
||||
# SIGNER_VERSION=vX.Y.Z # optional override; default is latest release tag
|
||||
# SIGNER_GITEA_TOKEN=<token> # if signer release assets are private
|
||||
# SIGNER_GITEA_TOKEN=<token> # if signer release assets are private
|
||||
# SIGNER_BINARY_URL=<direct url to signer binary>
|
||||
# SIGNER_CLIENT_BINARY_URL=<direct url to signer-client binary>
|
||||
# SIGNER_BINARY_ASSET=<asset name> # optional release asset override
|
||||
# SIGNER_CLIENT_BINARY_ASSET=<asset name> # optional release asset override
|
||||
|
||||
SIGNER_VERSION="${SIGNER_VERSION:-}"
|
||||
SIGNER_BINARY_ASSET="${SIGNER_BINARY_ASSET:-signer}"
|
||||
SIGNER_CLIENT_BINARY_ASSET="${SIGNER_CLIENT_BINARY_ASSET:-signer_client}"
|
||||
|
||||
PREFIX_BIN="${HOME}/.local/bin"
|
||||
|
||||
@@ -41,9 +45,11 @@ Options:
|
||||
|
||||
Optional env vars:
|
||||
SIGNER_VERSION=vX.Y.Z # optional override; default is latest release tag
|
||||
SIGNER_GITEA_TOKEN=<token> # required if signer release assets are private
|
||||
SIGNER_GITEA_TOKEN=<token> # required if signer release assets are private
|
||||
SIGNER_BINARY_URL=<direct url to signer binary>
|
||||
SIGNER_CLIENT_BINARY_URL=<direct url to signer-client binary>
|
||||
SIGNER_BINARY_ASSET=<asset name> # defaults to signer-linux-x86_64-musl
|
||||
SIGNER_CLIENT_BINARY_ASSET=<asset name> # defaults to signer-client-linux-x86_64-musl
|
||||
|
||||
Install paths:
|
||||
~/.local/bin/signer
|
||||
@@ -102,7 +108,7 @@ resolve_signer_version() {
|
||||
}
|
||||
|
||||
# Resolve a release asset URL by asset name suffix.
|
||||
# $1 = asset name to match exactly (e.g. "signer" or "signer-client")
|
||||
# $1 = asset name to match exactly (e.g. "signer-linux-x86_64-musl")
|
||||
download_signer_asset_url() {
|
||||
local asset_name="$1"
|
||||
local headers=()
|
||||
@@ -162,6 +168,12 @@ download_binary() {
|
||||
asset_url="$(download_signer_asset_url "${asset_name}")"
|
||||
fi
|
||||
|
||||
# Older releases used unqualified asset names. Keep that fallback while
|
||||
# preferring explicit portable musl assets for new releases.
|
||||
if [[ -z "${asset_url}" && "${asset_name}" == *-linux-x86_64-musl ]]; then
|
||||
asset_url="$(download_signer_asset_url "${asset_name%-linux-x86_64-musl}")"
|
||||
fi
|
||||
|
||||
if [[ -z "${asset_url}" ]]; then
|
||||
err "Could not find downloadable ${asset_name} release binary for ${SIGNER_VERSION}."
|
||||
err "Provide ${override_env} or SIGNER_GITEA_TOKEN so the release asset can be resolved."
|
||||
@@ -186,16 +198,16 @@ install_signer() {
|
||||
log "Installing signer ${SIGNER_VERSION}"
|
||||
log "Release page: ${release_page}"
|
||||
|
||||
# Install the signer daemon binary
|
||||
download_binary "signer" "SIGNER_BINARY_URL" "${PREFIX_BIN}/signer"
|
||||
# Prefer the statically linked musl artifact; fall back to legacy names.
|
||||
download_binary "${SIGNER_BINARY_ASSET}" "SIGNER_BINARY_URL" "${PREFIX_BIN}/signer"
|
||||
verify_installed_version "${SIGNER_VERSION}" "${PREFIX_BIN}/signer"
|
||||
log "Installed ${PREFIX_BIN}/signer from release binary"
|
||||
|
||||
# Install the signer-client CLI binary (best-effort: older releases may not have it)
|
||||
if [[ -z "${SIGNER_CLIENT_BINARY_URL:-}" ]] && ! download_signer_asset_url "signer-client" >/dev/null 2>&1; then
|
||||
if [[ -z "${SIGNER_CLIENT_BINARY_URL:-}" ]] && ! download_signer_asset_url "${SIGNER_CLIENT_BINARY_ASSET}" >/dev/null 2>&1 && ! download_signer_asset_url "signer-client" >/dev/null 2>&1; then
|
||||
warn "No signer-client asset found for ${SIGNER_VERSION}; skipping client install."
|
||||
else
|
||||
download_binary "signer-client" "SIGNER_CLIENT_BINARY_URL" "${PREFIX_BIN}/signer-client"
|
||||
download_binary "${SIGNER_CLIENT_BINARY_ASSET}" "SIGNER_CLIENT_BINARY_URL" "${PREFIX_BIN}/signer-client"
|
||||
log "Installed ${PREFIX_BIN}/signer-client from release binary"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# Static musl portability plan
|
||||
|
||||
## Objective
|
||||
|
||||
Produce portable x86_64 Linux release binaries that do not depend on the target machine's glibc version. The primary release artifacts will be statically linked against musl and will remain compatible with the project's Linux, Qubes, Unix-socket, qrexec, TCP/HTTP, terminal, and secure-memory requirements.
|
||||
|
||||
## Current findings
|
||||
|
||||
- The development host is x86_64 Ubuntu 22.04 with glibc 2.35.
|
||||
- Rust 1.80.1 and Cargo 1.80.1 are installed, but `rustup` is unavailable.
|
||||
- Docker is available, so the build can be isolated from the host toolchain and libc.
|
||||
- The project directly uses conventional Linux APIs through `libc`: `mlock`, `munlock`, `AF_UNIX`, `SO_PEERCRED`, `getuid`, `close`, `listen`, and `localtime_r`.
|
||||
- Cryptography is implemented through Rust crates; no mandatory OpenSSL dependency was found in this repository.
|
||||
- The sibling `nostr_core_lib_rust` checkout is a path dependency declared by `Cargo.toml` and must be present in the container build context.
|
||||
- The vendored `ratatui` submodule and its crossterm backend must compile for the musl target.
|
||||
- Existing release scripts build directly with the host Cargo installation and therefore do not guarantee libc portability.
|
||||
|
||||
## Compatibility policy
|
||||
|
||||
- Primary portable target: `x86_64-unknown-linux-musl`.
|
||||
- Release artifacts are intended for x86_64 Linux systems regardless of the installed glibc version, subject to Linux kernel, CPU, terminal, qrexec, and resource-limit requirements.
|
||||
- Do not use `-C target-cpu=native`; build for a conservative x86_64 baseline.
|
||||
- Keep the normal host-native build for development and debugging.
|
||||
- Treat `RLIMIT_MEMLOCK` separately from libc portability; static linking does not remove the need for appropriate memory-lock limits.
|
||||
|
||||
## Implementation phases
|
||||
|
||||
### 1. Add a reproducible musl build image
|
||||
|
||||
Create a pinned Docker build definition, preferably using a stable musl-based Rust image or a pinned Alpine image with an explicitly installed Rust toolchain.
|
||||
|
||||
The image must provide:
|
||||
|
||||
- Rust and Cargo versions compatible with the project's `Cargo.toml` and lockfile.
|
||||
- The `x86_64-unknown-linux-musl` target.
|
||||
- A C compiler/linker suitable for musl.
|
||||
- Required build utilities and certificate configuration.
|
||||
- No dependency on the host's Rust installation or glibc-linked build output.
|
||||
|
||||
Ensure the container can access both the project and the sibling `nostr_core_lib_rust` path dependency. The build must initialize or use the existing `ratatui` submodule consistently with normal project setup.
|
||||
|
||||
### 2. Add a portable build entry point
|
||||
|
||||
Create `build_musl.sh` with explicit behavior:
|
||||
|
||||
1. Validate that Docker is available.
|
||||
2. Validate that the project root and required sibling path dependency exist.
|
||||
3. Build both `signer` and `signer-client` for `x86_64-unknown-linux-musl` in release mode.
|
||||
4. Use a dedicated output directory such as `dist/musl-x86_64`.
|
||||
5. Copy the binaries using explicit names:
|
||||
- `signer-linux-x86_64-musl`
|
||||
- `signer-client-linux-x86_64-musl`
|
||||
6. Avoid copying host-native binaries into the portable output.
|
||||
7. Run binary validation and version checks before reporting success.
|
||||
8. Return a nonzero status for missing dependencies, failed builds, invalid ELF output, dynamic linkage, or version mismatch.
|
||||
|
||||
Keep the script usable from the repository root and make its Docker invocation safe for ordinary user development.
|
||||
|
||||
### 3. Validate static linkage and runtime behavior
|
||||
|
||||
Add validation to the build flow using tools available in the container:
|
||||
|
||||
- `file` must identify x86_64 ELF executables.
|
||||
- `readelf` must show the expected musl/static characteristics and no glibc loader requirement.
|
||||
- `ldd` must not identify unresolved dynamic runtime dependencies.
|
||||
- `signer --version` and `signer-client --version` must report the Cargo package version.
|
||||
- Run `cargo test --target x86_64-unknown-linux-musl` where the test environment supports it.
|
||||
|
||||
Perform smoke checks for:
|
||||
|
||||
- stdio framing;
|
||||
- Unix abstract socket bind/connect and peer identity;
|
||||
- TCP and HTTP startup;
|
||||
- qrexec subprocess integration where the host provides qrexec;
|
||||
- mnemonic input and key derivation;
|
||||
- representative signing and verification operations;
|
||||
- TUI initialization in a terminal;
|
||||
- `mlock` success or the documented unlocked-memory fallback behavior.
|
||||
|
||||
If a dependency cannot support musl, record the failure and determine whether it is optional, can be feature-gated, or requires retaining a glibc artifact.
|
||||
|
||||
### 4. Integrate release automation
|
||||
|
||||
Update `increment_and_push.sh` so release mode invokes the portable musl builder rather than the host-native `cargo build --release` path.
|
||||
|
||||
Preserve:
|
||||
|
||||
- version incrementing;
|
||||
- source version updates;
|
||||
- binary version verification;
|
||||
- tagging and pushing;
|
||||
- Gitea release creation;
|
||||
- source tarball generation.
|
||||
|
||||
Extend asset upload handling to upload both musl binaries with their platform-specific names. Do not silently publish a host-linked binary under an ambiguous name.
|
||||
|
||||
Decide whether the existing unqualified assets remain as compatibility aliases. The safer default is to publish explicit musl names and retain legacy fallback handling only for older releases.
|
||||
|
||||
### 5. Update local deployment
|
||||
|
||||
Extend `deploy_local.sh` with an explicit portable option, such as `--musl`.
|
||||
|
||||
Suggested behavior:
|
||||
|
||||
- default development/debug builds remain native;
|
||||
- `--musl` invokes the Docker build and installs the resulting portable binaries;
|
||||
- release deployment should clearly report whether the installed binary is native or musl;
|
||||
- installation continues to use `/usr/local/bin/` only when the user has the required privileges.
|
||||
|
||||
Add checks so the script does not mistake a musl artifact for a native build or install an absent/stale binary.
|
||||
|
||||
### 6. Update the installer
|
||||
|
||||
Update `install_signer.sh` to prefer the explicit musl release assets:
|
||||
|
||||
- `signer-linux-x86_64-musl`
|
||||
- `signer-client-linux-x86_64-musl`
|
||||
|
||||
Retain support for existing older releases whose assets are named simply `signer` or `signer-client`. Add an override for users who need a different asset URL or a native glibc build.
|
||||
|
||||
Continue verifying the installed program version, and add target/artifact validation where practical so a mislabeled asset is rejected.
|
||||
|
||||
### 7. Update documentation
|
||||
|
||||
Update the build and platform sections of `README.md` to explain:
|
||||
|
||||
- native builds are for local development;
|
||||
- portable release builds use static musl;
|
||||
- the portable target is x86_64 Linux;
|
||||
- the artifact names and output directory;
|
||||
- how to verify static linkage with `file`, `readelf`, and `ldd`;
|
||||
- static musl removes the glibc runtime dependency but not Linux kernel, CPU, terminal, qrexec, or `RLIMIT_MEMLOCK` requirements;
|
||||
- Qubes users should use the musl release artifact unless a native glibc build is specifically required.
|
||||
|
||||
Update the project layout section with the new build definition and script.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- A clean Docker invocation builds both release binaries without using the host Rust compiler or host glibc.
|
||||
- The resulting binaries are x86_64 and statically linked against musl.
|
||||
- Neither binary requires the glibc dynamic loader or a minimum GLIBC symbol version.
|
||||
- Both binaries pass version checks and the relevant test suite.
|
||||
- Representative Unix, stdio, TCP/HTTP, crypto, TUI, and secure-memory paths are validated.
|
||||
- Release automation uploads unambiguous musl artifacts.
|
||||
- The installer can select the musl artifacts and remains compatible with older release naming.
|
||||
- Documentation explains how to build, verify, install, and deploy the portable binaries.
|
||||
|
||||
## Deferred options
|
||||
|
||||
- Publishing a glibc-2.17-compatible artifact can be added later if a target integration requires glibc behavior.
|
||||
- Additional architectures can be added later, but must have separate build images, artifact names, and validation.
|
||||
- Musl should not be made the only local development target until the smoke tests demonstrate that all supported transports and terminal behavior are equivalent for the project's deployment environments.
|
||||
+17
-11
@@ -64,17 +64,7 @@ impl AlgorithmKeyCache {
|
||||
|
||||
let phrase = mnemonic.phrase().ok_or(SignerError::MnemonicNotLoaded)?;
|
||||
|
||||
// Build the standard derivation path for this algorithm
|
||||
let path = match alg {
|
||||
CryptoAlg::Secp256k1 => format!("m/44'/1237'/{}'/0/0", index),
|
||||
CryptoAlg::Ed25519 => format!("m/44'/102001'/{}'/0'/0'", index),
|
||||
CryptoAlg::X25519 => format!("m/44'/102002'/{}'/0'/0'", index),
|
||||
CryptoAlg::MlDsa65 => format!("m/44'/102003'/{}'/0'/0'", index),
|
||||
CryptoAlg::SlhDsa128s => format!("m/44'/102004'/{}'/0'/0'", index),
|
||||
CryptoAlg::MlKem768 => format!("m/44'/102005'/{}'/0'/0'", index),
|
||||
CryptoAlg::Unknown => return Err(SignerError::InvalidInput),
|
||||
};
|
||||
|
||||
let path = standard_path(alg, index)?;
|
||||
let entry = derive_alg_key(phrase, &path, alg, index)?;
|
||||
self.entries.push(entry);
|
||||
Ok(())
|
||||
@@ -92,6 +82,22 @@ impl Default for AlgorithmKeyCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// The standard derivation path for an algorithm at a given index.
|
||||
///
|
||||
/// secp256k1 uses the NIP-06 path; ed25519/x25519/PQ use their
|
||||
/// per-algorithm SLIP-44 coin types (102001'–102005').
|
||||
pub fn standard_path(alg: CryptoAlg, index: i32) -> Result<String, SignerError> {
|
||||
match alg {
|
||||
CryptoAlg::Secp256k1 => Ok(format!("m/44'/1237'/{}'/0/0", index)),
|
||||
CryptoAlg::Ed25519 => Ok(format!("m/44'/102001'/{}'/0'/0'", index)),
|
||||
CryptoAlg::X25519 => Ok(format!("m/44'/102002'/{}'/0'/0'", index)),
|
||||
CryptoAlg::MlDsa65 => Ok(format!("m/44'/102003'/{}'/0'/0'", index)),
|
||||
CryptoAlg::SlhDsa128s => Ok(format!("m/44'/102004'/{}'/0'/0'", index)),
|
||||
CryptoAlg::MlKem768 => Ok(format!("m/44'/102005'/{}'/0'/0'", index)),
|
||||
CryptoAlg::Unknown => Err(SignerError::InvalidInput),
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive a single algorithm key entry.
|
||||
fn derive_alg_key(
|
||||
mnemonic_phrase: &str,
|
||||
|
||||
+1
-1
@@ -31,4 +31,4 @@ pub mod error;
|
||||
pub use error::SignerError;
|
||||
|
||||
/// Version string (matches C NSIGNER_VERSION).
|
||||
pub const VERSION: &str = "v0.0.19";
|
||||
pub const VERSION: &str = "v0.0.23";
|
||||
|
||||
+6
-2
@@ -241,7 +241,9 @@ fn run_headless(cli: &Cli, listen_mode: ListenMode) -> Result<(), SignerError> {
|
||||
key_store: &mut key_store,
|
||||
alg_key_cache: &mut alg_key_cache,
|
||||
};
|
||||
let _ = server.handle_one(&mut dispatcher);
|
||||
if let Ok(Some(activity)) = server.handle_one(&mut dispatcher) {
|
||||
println!("{}", activity);
|
||||
}
|
||||
server.stop();
|
||||
} else {
|
||||
// Tcp / Http poll loop
|
||||
@@ -253,7 +255,9 @@ fn run_headless(cli: &Cli, listen_mode: ListenMode) -> Result<(), SignerError> {
|
||||
alg_key_cache: &mut alg_key_cache,
|
||||
};
|
||||
match server.handle_one(&mut dispatcher) {
|
||||
Ok(Some(_activity)) => {}
|
||||
Ok(Some(activity)) => {
|
||||
println!("{}", activity);
|
||||
}
|
||||
Ok(None) => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
|
||||
+180
-10
@@ -62,6 +62,38 @@ impl CallerIdentity {
|
||||
auth_label: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a rich identity string for the activity log — as much as can be
|
||||
/// identified about the caller.
|
||||
///
|
||||
/// - Unix socket: `uid:<n> gid:<n> pid:<n>`
|
||||
/// - Qrexec: `qubes:<vm>`
|
||||
/// - TCP/HTTP: `tcp:<addr>`
|
||||
/// - Auth envelope verified: `pubkey:<hex> label:<label>` appended.
|
||||
pub fn identity_str(&self) -> String {
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
match self.kind {
|
||||
ListenMode::Unix => {
|
||||
parts.push(format!("uid:{}", self.uid));
|
||||
if self.gid != 0 {
|
||||
parts.push(format!("gid:{}", self.gid));
|
||||
}
|
||||
if self.pid != 0 {
|
||||
parts.push(format!("pid:{}", self.pid));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
parts.push(self.caller_id.clone());
|
||||
}
|
||||
}
|
||||
if self.auth_present {
|
||||
parts.push(format!("pubkey:{}", self.auth_pubkey_hex));
|
||||
if !self.auth_label.is_empty() {
|
||||
parts.push(format!("label:{}", self.auth_label));
|
||||
}
|
||||
}
|
||||
parts.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
/// Server context.
|
||||
@@ -294,7 +326,7 @@ impl ServerContext {
|
||||
Err((code, msg)) => {
|
||||
if self.auth_mode == AuthMode::Required {
|
||||
let response = make_auth_error(&request, code, msg);
|
||||
let activity = format!("{} DENIED:{}", caller.caller_id, msg);
|
||||
let activity = format!("{} DENIED:{}", caller.identity_str(), msg);
|
||||
return (response, activity);
|
||||
}
|
||||
// Optional: continue without auth
|
||||
@@ -308,7 +340,7 @@ impl ServerContext {
|
||||
None => {
|
||||
// Malformed request — let the dispatcher produce the error
|
||||
let response = crate::dispatcher::handle_request(dispatcher, request);
|
||||
let activity = format!("{} DENIED:malformed", caller.caller_id);
|
||||
let activity = format!("{} DENIED:malformed", caller.identity_str());
|
||||
return (response, activity);
|
||||
}
|
||||
};
|
||||
@@ -316,21 +348,28 @@ impl ServerContext {
|
||||
// get_info is metadata — no key material
|
||||
if method == crate::enforcement::VERB_GET_INFO {
|
||||
let response = crate::dispatcher::handle_request(dispatcher, request);
|
||||
let activity = format!("{} - -", caller.caller_id);
|
||||
let activity = format!("{} {}()", caller.identity_str(), method);
|
||||
return (response, activity);
|
||||
}
|
||||
|
||||
// Algorithm-based verbs (bypass role table) — no authorization
|
||||
if crate::enforcement::is_algorithm_verb(&method) {
|
||||
let response = self.process_algorithm_verb(dispatcher, request, &selector_req);
|
||||
let activity = format!("{} - -", caller.caller_id);
|
||||
// Activity: caller method(algorithm,index) — the algorithm's
|
||||
// standard derivation path identifies the key material.
|
||||
let (alg_name, path) = extract_algorithm_and_path(request);
|
||||
let activity = match (alg_name, path) {
|
||||
(Some(a), Some(p)) => format!("{} {}({},{} {})", caller.identity_str(), method, a, selector_req.index, p),
|
||||
(Some(a), None) => format!("{} {}({})", caller.identity_str(), method, a),
|
||||
_ => format!("{} {}()", caller.identity_str(), method),
|
||||
};
|
||||
return (response, activity);
|
||||
}
|
||||
|
||||
// OTP verbs
|
||||
if method == crate::enforcement::VERB_ENCRYPT || method == crate::enforcement::VERB_DECRYPT {
|
||||
let response = crate::dispatcher::handle_request(dispatcher, request);
|
||||
let activity = format!("{} - -", caller.caller_id);
|
||||
let activity = format!("{} {}()", caller.identity_str(), method);
|
||||
return (response, activity);
|
||||
}
|
||||
|
||||
@@ -342,7 +381,7 @@ impl ServerContext {
|
||||
let response = make_selector_error(&request, e);
|
||||
let activity = format!(
|
||||
"{} {}() DENIED:{}",
|
||||
caller.caller_id,
|
||||
caller.identity_str(),
|
||||
method,
|
||||
e.as_str()
|
||||
);
|
||||
@@ -351,15 +390,29 @@ impl ServerContext {
|
||||
};
|
||||
|
||||
// Role entry from the resolved selector — used for the activity
|
||||
// message (curve + key path).
|
||||
// message (role name, curve, and the concrete key path requested).
|
||||
let role_entry = &dispatcher.role_table.entries[role_index];
|
||||
let role_name = role_entry.name.clone();
|
||||
let curve = role_entry.curve_str.clone();
|
||||
let path = role_entry.display_path();
|
||||
// The actual key path the caller requested/accessed — the concrete
|
||||
// role_path from the request when supplied, otherwise the role's
|
||||
// fixed/derived path. (Not the allowed range.)
|
||||
let actual_path = if selector_req.has_role_path {
|
||||
selector_req.role_path.clone()
|
||||
} else {
|
||||
role_entry.display_path()
|
||||
};
|
||||
|
||||
// ── Dispatch ───────────────────────────────────────────────
|
||||
let response = crate::dispatcher::handle_request(dispatcher, request);
|
||||
// Activity format: uid curve path (timestamp is added by the log).
|
||||
let activity = format!("{} {} {}", caller.caller_id, curve, path);
|
||||
// Activity format: uid role curve path (timestamp is added by the log).
|
||||
let activity = format!(
|
||||
"{} {} {} {}",
|
||||
caller.identity_str(),
|
||||
role_name,
|
||||
curve,
|
||||
actual_path
|
||||
);
|
||||
(response, activity)
|
||||
}
|
||||
|
||||
@@ -509,6 +562,40 @@ fn extract_method_and_selector(request: &str) -> Option<(String, SelectorRequest
|
||||
Some((method, sel))
|
||||
}
|
||||
|
||||
/// Extract the algorithm name and its standard derivation path from an
|
||||
/// algorithm-verb request's options (for the activity log).
|
||||
///
|
||||
/// Returns `(algorithm_name, Some(path))` when the request carries a valid
|
||||
/// algorithm; `(None, None)` otherwise.
|
||||
fn extract_algorithm_and_path(request: &str) -> (Option<String>, Option<String>) {
|
||||
let root: serde_json::Value = match serde_json::from_str(request) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return (None, None),
|
||||
};
|
||||
let alg_str = root
|
||||
.get("params")
|
||||
.and_then(|p| p.as_array())
|
||||
.and_then(|p| p.last())
|
||||
.and_then(|o| o.get("algorithm"))
|
||||
.and_then(|v| v.as_str());
|
||||
let Some(alg_str) = alg_str else {
|
||||
return (None, None);
|
||||
};
|
||||
let alg = crate::pq_crypto::CryptoAlg::from_str(alg_str);
|
||||
if alg == crate::pq_crypto::CryptoAlg::Unknown {
|
||||
return (Some(alg_str.to_string()), None);
|
||||
}
|
||||
let index = root
|
||||
.get("params")
|
||||
.and_then(|p| p.as_array())
|
||||
.and_then(|p| p.last())
|
||||
.and_then(|o| o.get("index"))
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0) as i32;
|
||||
let path = crate::alg_cache::standard_path(alg, index).ok();
|
||||
(Some(alg.as_str().to_string()), path)
|
||||
}
|
||||
|
||||
/// Build an auth error response.
|
||||
fn make_auth_error(request: &str, code: i32, message: &str) -> String {
|
||||
let id = extract_id(request);
|
||||
@@ -551,3 +638,86 @@ fn extract_id(request: &str) -> String {
|
||||
})
|
||||
.unwrap_or_else(|| "null".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Build the activity line for an algorithm-verb request, exactly as
|
||||
/// `process_request` does.
|
||||
fn activity_for(request: &str) -> String {
|
||||
let (method, selector_req) =
|
||||
extract_method_and_selector(request).expect("valid request");
|
||||
assert!(crate::enforcement::is_algorithm_verb(&method));
|
||||
let (alg_name, path) = extract_algorithm_and_path(request);
|
||||
let caller_id = "uid:1000";
|
||||
match (alg_name, path) {
|
||||
(Some(a), Some(p)) => {
|
||||
format!("{} {}({},{} {})", caller_id, method, a, selector_req.index, p)
|
||||
}
|
||||
(Some(a), None) => format!("{} {}({})", caller_id, method, a),
|
||||
_ => format!("{} {}()", caller_id, method),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activity_all_algorithms() {
|
||||
// Every algorithm must produce a non-blank activity line with its
|
||||
// curve name and standard derivation path.
|
||||
let cases = [
|
||||
("secp256k1", "m/44'/1237'/0'/0/0"),
|
||||
("ed25519", "m/44'/102001'/0'/0'/0'"),
|
||||
("x25519", "m/44'/102002'/0'/0'/0'"),
|
||||
("ml-dsa-65", "m/44'/102003'/0'/0'/0'"),
|
||||
("slh-dsa-128s", "m/44'/102004'/0'/0'/0'"),
|
||||
("ml-kem-768", "m/44'/102005'/0'/0'/0'"),
|
||||
];
|
||||
for (alg, expected_path) in cases {
|
||||
let req = format!(
|
||||
r#"{{"id":"1","method":"get_public_key","params":[{{"algorithm":"{}","index":0}}]}}"#,
|
||||
alg
|
||||
);
|
||||
let activity = activity_for(&req);
|
||||
assert!(
|
||||
activity.contains(alg),
|
||||
"activity '{}' must contain algorithm '{}'",
|
||||
activity,
|
||||
alg
|
||||
);
|
||||
assert!(
|
||||
activity.contains(expected_path),
|
||||
"activity '{}' must contain path '{}'",
|
||||
activity,
|
||||
expected_path
|
||||
);
|
||||
assert!(
|
||||
!activity.contains("- -"),
|
||||
"activity '{}' must not contain the blank placeholder",
|
||||
activity
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activity_index_substituted() {
|
||||
let req = r#"{"id":"1","method":"sign","params":["00ff",{"algorithm":"ml-dsa-65","index":7}]}"#;
|
||||
let activity = activity_for(req);
|
||||
assert!(activity.contains("m/44'/102003'/7'/0'/0'"), "activity: {}", activity);
|
||||
assert!(activity.contains("sign(ml-dsa-65,7"), "activity: {}", activity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activity_unknown_algorithm() {
|
||||
let req = r#"{"id":"1","method":"sign","params":["00ff",{"algorithm":"bogus","index":0}]}"#;
|
||||
let activity = activity_for(req);
|
||||
// Unknown algorithm: name echoed, no path (dispatcher will reject).
|
||||
assert!(activity.contains("sign(bogus)"), "activity: {}", activity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activity_missing_algorithm() {
|
||||
let req = r#"{"id":"1","method":"sign","params":["00ff",{}]}"#;
|
||||
let activity = activity_for(req);
|
||||
assert!(activity.contains("sign()"), "activity: {}", activity);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user