Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ Cargo.lock
.patcher/
.DS_Store
.env

owner_key*
12 changes: 1 addition & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,4 @@ resolver = "2"
members = [
"crates/rustpatcher-macros",
"crates/rustpatcher",
]

default-members = [
"crates/rustpatcher-macros",
"crates/rustpatcher",
]

[profile.release]
panic = "abort"
debug = true
lto = "thin"
]
275 changes: 190 additions & 85 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,126 +1,231 @@
# Rust Patcher
*Secure Decentralized Software Updates* - Working work in progress

[![Crates.io](https://img.shields.io/crates/v/rustpatcher.svg)](https://crates.io/crates/rustpatcher)
[![Docs.rs](https://docs.rs/rustpatcher/badge.svg)](https://docs.rs/rustpatcher)
![License](https://img.shields.io/badge/License-MIT-green)

## Implementation Flow
# Rust Patcher
Secure fully decentralized software updates.


### 1. Add Dependency (Crates.io)
## Implementation Flow

### 1. Add dependency
```toml
# Cargo.toml
[dependencies]
rustpatcher = "0.1"
rustpatcher = "0.2"
tokio = { version = "1", features = ["rt-multi-thread","macros"] }
```


### 2. Initialize Patcher
### 2. Embed owner public key and start the updater
```rust
// main.rs
use rustpatcher::Patcher;

#[rustpatcher::main]
#[tokio::main]
#[rustpatcher::public_key("axegnqus3miex47g1kxf1j7j8spczbc57go7jgpeixq8nxjfz7gy")]
async fn main() -> anyhow::Result<()> {
let patcher = Patcher::new()
.build()
.await?;

// 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
loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
println!("Exiting on Ctrl-C");
break;
}
}
}
Ok(())
}
```

### 3. Initialize Cryptographic Identity
### 3. Generate signing key (one-time)
```bash
cargo run -- rustpatcher init
```
**Output:**
```text
New keys generated:
Trusted-Key = mw6iuq1iu7qd5gcz59qpjnu6tw9yn7pn4gxxkdbqwwwxfzyziuro
Shared-Secret = 8656fg8j6s43a4jndkzdysjuof588zezsn6s8sd6wwcpwf6b3r9y
cargo install rustpatcher
rustpatcher gen ./owner_key
```
Output 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("<pubkey>")]

### 4. Extend main with keys
```rust
// main.rs
use rustpatcher::Patcher;
### 4. Build and sign releases
```bash
# build your binary
cargo build --release

#[rustpatcher::main]
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let patcher = Patcher::new()
.trusted_key_from_z32_str("mw6iuq1iu7qd5gcz59qpjnu6tw9yn7pn4gxxkdbqwwwxfzyziuro")
.shared_secret_key_from_z32_str("mw6iuq1iu7qd5gcz59qpjnu6tw9yn7pn4gxxkdbqwwwxfzyziuro"))
.build()
.await?;
}
# sign the compiled binary in-place
rustpatcher sign target/release/<your-bin> --key-file=./owner_key
```

### 5. Publish Updates (Master Node)
```bash
# Increment version in Cargo.toml first
cargo run -- rustpatcher publish
### 5. Publish updates
- 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.


---

## Run Example: simple
```sh
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.
```
Creates signed package with:
- SHA-256 executable hash
- Version metadata (major.minor.patch)
- Ed25519 publisher signature
- PKARR DHT record

---

## Network Architecture
## Network Architecture

### Master Node Flow
```mermaid
sequenceDiagram
Master->>+PKARR: Publish signed package
Master->>+Iroh: Announce version topic
Master-->>Network: Propagate via DHT
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
```

### Client Node Flow
```mermaid
sequenceDiagram
Client->>+PKARR: Check version records
PKARR-->>-Client: Return latest signed package
Client->>+Iroh: Discover peers via topic
Iroh-->>-Client: Return node list
Client->>Peer: Establish P2P connection
Peer-->>Client: Stream verified update
Client->>Self: Safe replace via self_replace
- Discovery: [distributed-topic-tracker](https://github.com/rustonbsd/distributed-topic-tracker) minute-slotted records over the DHT
- Transport: [iroh](https://github.com/n0-computer/iroh) QUIC, ALPN namespaced per owner key
- Authentication: rotating hash auth per minute bucket

---

## Key Processes

1. 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.

2. 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).

3. Self-update mechanism
- Write to temp file
- Atomic [self-replace](https://crates.io/crates/self-replace)
- Optional immediate restart via execv (UpdaterMode::Now) or deferred (OnRestart / At(hh, mm))

---

## Data Embedded in the Binary

- 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

---

## CLI Reference (rustpatcher)

- gen <key-path>
- Generates a new ed25519 signing key in z-base-32; prints the public key and attribute snippet.
- sign <binary> --key-file <key-path>
- Reads the compiled binary, computes PatchInfo, and writes it into the embedded region.

DO NOT COMMIT YOUR PRIVATE KEY!

```sh
# add this to your .gitignore
owner_key*
```

## Key Processes
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).

---

## Library API (overview)

- #[rustpatcher::public_key("<zbase32-ed25519-pubkey>")]
- 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)}

---

## How It Changed (vs previous rustpatcher)

- 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

---

## Example

```rust
use rustpatcher::UpdaterMode;

#[tokio::main]
#[rustpatcher::public_key("axegnqus3miex47g1kxf1j7j8spczbc57go7jgpeixq8nxjfz7gy")]
async fn main() -> anyhow::Result<()> {

1. **Version Propagation**
- Master nodes sign packages with secret key
- PKARR DHT stores version records with TTL
- Iroh topic tracker maintains peer list per version
rustpatcher::spawn(UpdaterMode::At(02, 30)).await?;

// app code...
Ok(())
}
```

2. **Update Verification**
```rust
// Verification chain
if pub_key.verify(&data, &sig).is_ok()
&& compute_hash(data) == stored_hash
&& version > current_version {
apply_update()
}
```
## Release Workflow

3. **Self-Update Mechanism**
- Hash and Signature verification after data download
- Temp file write with atomic replacement
- Execv syscall for instant reload
1) Generate key (once):
- rustpatcher gen ./owner_key

## CLI Reference
2) Build + sign each release:
- cargo build --release
- rustpatcher sign target/release/<your-bin> --key-file=./owner_key

| Command | Function |
|-----------------|--------------------------------------|
| `init` | Generate cryptographic identity |
| `publish` | Create/distribute signed package |
3) 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.

*Zero configuration needed for peer discovery - automatic via Iroh Topic Tracker*
```make
build:
cargo build --release
rustpatcher sign target/release/<your-bin> --key-file ./owner_key

## Old Architecture Diagram
![Rough outline of how everythong works](media/patcher_diagram.svg "Patcher diagram")
publish:
target/release/<your-bin>
```
8 changes: 4 additions & 4 deletions crates/rustpatcher-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "rustpatcher-macros"
version = "0.1.0"
edition = "2021"
version = "0.2.0"
edition = "2024"
authors = ["Zacharias Boehler <rustonbsd@mailfence.com>"]
description = "p2p patching system"
license = "MIT"
Expand All @@ -14,7 +14,7 @@ categories = ["network-programming"]
proc-macro = true

[dependencies]
syn = { version = "2.0", features = ["full"] }
syn = { version = "2.0", default-features=false, features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
ctor = "0.4.2"
ctor = "0.5.0"
25 changes: 18 additions & 7 deletions crates/rustpatcher-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
use syn::{parse_macro_input, ItemFn, Expr};

#[proc_macro_attribute]
pub fn main(_args: TokenStream, input: TokenStream) -> TokenStream {
pub fn public_key(args: TokenStream, input: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(input as ItemFn);
let public_key_expr = parse_macro_input!(args as Expr);

let expanded = quote! {
// Create a static initializer that runs before main
#[ctor::ctor]
fn __init_version() {
rustpatcher::version_embed::__set_version(env!("CARGO_PKG_VERSION"));
}
const _: () = {
#[::ctor::ctor]
fn __rustpatcher_init_version() {
let __rustpatcher_public_key: &'static str = {
let __cow: ::std::borrow::Cow<'static, str> =
::std::convert::Into::<::std::borrow::Cow<'static, str>>::into(#public_key_expr);
match __cow {
::std::borrow::Cow::Borrowed(s) => s,
::std::borrow::Cow::Owned(s) => ::std::boxed::Box::leak(s.into_boxed_str()),
}
};
::rustpatcher::embed::embed(env!("CARGO_PKG_VERSION"), __rustpatcher_public_key);
}
};

#input_fn
};
Expand Down
Loading