Secure fully decentralized software updates.
| Platform | Architecture | Supported |
|---|---|---|
| Linux | x86_64 | Yes |
| Linux | ARM64 | Yes |
| macOS | x86_64 | Yes |
| macOS | ARM64 | Yes |
| Windows | - | Not yet |
Note: windows support will follow, windows build err: libc is not available in nix pkg
# Cargo.toml
[dependencies]
rustpatcher = "0.2"
rustpatcher-macros = "0.2"
tokio = { version = "1", features = ["rt-multi-thread","macros"] }#[tokio::main]
#[rustpatcher::public_key("axegnqus3miex47g1kxf1j7j8spczbc57go7jgpeixq8nxjfz7gy")]
async fn main() -> anyhow::Result<()> {
// Only in --release builds, not intended for debug builds
rustpatcher::spawn(rustpatcher::UpdaterMode::At(13, 40)).await?;
println!("my version is {:?}", rustpatcher::Version::current()?);
// your app code after this
tokio::select! {
_ = tokio::signal::ctrl_c() => {
println!("Exiting on Ctrl-C");
}
}
Ok(())
}cargo install rustpatcher
rustpatcher gen ./owner_keyOutput includes:
- Owner signing key saved to ./owner_key (z-base-32 encoded)
- Owner public key (z-base-32)
- Attribute snippet to paste into main: #[rustpatcher::public_key("")]
# build your binary
cargo build --release
# sign the compiled binary in-place
rustpatcher sign target/release/<your-bin> --key-file=./owner_key- Run the newly signed binary on at least one node until a couple of peers have updated themselfs.
- The running process periodically publishes the latest PatchInfo to the DHT.
- Clients discover new PatchInfo, fetch the patch from peers, verify, and self-replace.
git clone https://github.com/rustonbsd/rustpatcher
cd rustpatcher
cargo build --release --example simple
cargo run --bin rustpatcher sign target/release/examples/simple --key-file ./owner_key_example
# Run signed app:
./target/release/examples/simple
# if you increase the version in /crates/rustpatcher/Cargo.toml
# and build+sign+start another node, then the first
# node will update via the second node.sequenceDiagram
participant Owner as Owner Node (new version)
participant DTT as DHT Topic Tracker
participant Peer as Peer Node (old)
Owner->>DTT: Publish PatchInfo(version, size, hash, sig)
Peer->>DTT: Query latest PatchInfo (minute slots)
DTT-->>Peer: Return newest records
Peer->>Owner: Iroh connect (ALPN /rustpatcher/<owner>/v0)
Peer->>Owner: Auth = sha512(pubkey || unix_minute)
Owner-->>Peer: OK + Patch (postcard)
Peer->>Peer: Verify(hash, size, ed25519(pubkey))
Peer->>Peer: Atomic replace + optional execv restart
- Discovery: distributed-topic-tracker minute-slotted records over the DHT
- Transport: iroh QUIC, ALPN namespaced per owner key
- Authentication: rotating hash auth per minute bucket
- Version propagation
- Running a node publishes a PatchInfo record roughly every minute.
- Records are minute scoped with short TTL to avoid staleness.
- Peers scan current and previous minute for latest version.
- Patch fetch + verification
- Peer connects to other peers with newer version via iroh using an ALPN derived from the owner pubkey.
- Auth: sha512(owner_pub_key || unix_minute(t)) for t ∈ {-1..1}.
- Owner sends the signed patch (postcard-encoded).
- Self-update mechanism
- Write to temp file
- Atomic self-replace
- Optional immediate restart via execv (UpdaterMode::Now) or deferred (OnRestart / At(hh, mm))
- Fixed-size embedded region in a dedicated link section (.embedded_signature)
- Layout:
- 28 bytes: bounds start marker
- 32 bytes: binary hash (sha512 truncated to 32)
- 8 bytes: binary size (LE)
- 64 bytes: ed25519 signature
- 16 bytes: ASCII version (padded)
- 28 bytes: bounds end marker
At runtime, the library:
- Locates the embedded region
- Parses version/hash/size/signature
- Verifies the binary contents against the signed metadata
- gen
- Generates a new ed25519 signing key in z-base-32; prints the public key and attribute snippet.
- sign --key-file
- Reads the compiled binary, computes PatchInfo, and writes it into the embedded region.
DO NOT COMMIT YOUR PRIVATE KEY!
# add this to your .gitignore
owner_key*Notes:
- Keys are z-base-32 encoded on disk, the public key is embedded in code via #[rustpatcher::public_key("...")].
- Signing must be re-run after each new build that is intendet to self update.
- For every build target a seperate keypair is required (we don't want the arm users patching in x86 binaries).
- #[rustpatcher::public_key("")]
- Embeds the owner public key and the package version for verification
- rustpatcher::spawn(mode: UpdaterMode) -> Future<Result<()>>
- Starts discovery, publishing, distribution server, and updater
- UpdaterMode::{Now, OnRestart, At(h, m)}
- Single embedded region with explicit bounds, constant size, and zero-allocation compile-time construction
- Signature scheme clarified and minimal:
- sha512(data_no_embed) -> first 32 bytes as hash
- sign sha512(version || hash || size_le) with ed25519
- Owner key embedding via attribute macro, version captured from CARGO_PKG_VERSION and embedded as fixed-length ASCII
- Minute-slotted record publishing and discovery via distributed-topic-tracker
- iroh-based distributor with rotating minute auth derived from owner public key
- Simple updater modes: Now, OnRestart, At(hh:mm)
- CLI split: cargo install rustpatcher to manage keys and sign releases
use rustpatcher::UpdaterMode;
#[tokio::main]
#[rustpatcher::public_key("axegnqus3miex47g1kxf1j7j8spczbc57go7jgpeixq8nxjfz7gy")]
async fn main() -> anyhow::Result<()> {
rustpatcher::spawn(UpdaterMode::At(02, 30)).await?;
// app code...
Ok(())
}- Generate key (once):
- rustpatcher gen ./owner_key
- Build + sign each release:
- cargo build --release
- rustpatcher sign target/release/ --key-file=./owner_key
- Deploy and run the signed binary on at least one node:
- It will publish PatchInfo and serve patches to peers.
- No need to have any exposed ports.
build:
cargo build --release
rustpatcher sign target/release/<your-bin> --key-file ./owner_key
publish:
target/release/<your-bin>