deploy-agent: Rust deployment automation (server + stub + signed payloads)
build / build-windows-x86_64 (push) Has been cancelled
build / build-linux-x86_64 (push) Failing after 1m13s
build / build-linux-aarch64 (push) Failing after 1m8s

- Workspace: common (protocol/crypto/tarball), server (axum+rustls), stub (registration, poll, verify-before-execute)
- Ed25519 payload signing + request-signature auth on /poll, /report
- flate2/miniz_oxide pure-Rust tarball, no system deps
- CI matrix: linux musl x86_64/aarch64, windows x86_64
- Compiles clean: cargo build --workspace, 0 errors 0 warnings
This commit is contained in:
2026-07-30 23:57:25 -07:00
commit e96d27f35c
22 changed files with 3447 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "common"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
ed25519-dalek = "2.1"
rand = "0.8"
flate2 = { version = "1.0", default-features = false, features = ["rust_backend"] }
tar = "0.4"
hex = "0.4"
sha2 = "0.10"
thiserror = "1.0"
+33
View File
@@ -0,0 +1,33 @@
use ed25519_dalek::Verifier;
use ed25519_dalek::{SigningKey, VerifyingKey, Signature};
use rand::rngs::OsRng;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum CryptoError {
#[error("signature verification failed")]
VerificationFailed,
#[error("keypair generate failed")]
KeyGenFailed,
}
pub fn generate_ed25519_keypair() -> Result<(SigningKey, VerifyingKey), CryptoError> {
use rand::RngCore;
let mut seed = [0u8; 32];
OsRng.fill_bytes(&mut seed);
let keypair = SigningKey::from_bytes(&seed);
let verify = keypair.verifying_key();
Ok((keypair, verify))
}
pub fn verify_signature(payload: &[u8], signature: &[u8], pubkey: &VerifyingKey) -> Result<(), CryptoError> {
let sig = Signature::from_slice(signature).map_err(|_| CryptoError::VerificationFailed)?;
pubkey.verify(payload, &sig).map_err(|_| CryptoError::VerificationFailed)
}
/// Sign an arbitrary byte payload (e.g. the canonical body of a request)
/// with an Ed25519 signing key.
pub fn sign(payload: &[u8], key: &SigningKey) -> Vec<u8> {
use ed25519_dalek::Signer;
key.sign(payload).to_bytes().to_vec()
}
+3
View File
@@ -0,0 +1,3 @@
pub mod proto;
pub mod crypto;
pub mod tarball;
+44
View File
@@ -0,0 +1,44 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MachineId(pub String);
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusReport {
pub success: bool,
pub log: String,
pub payload_id: Option<ContentAddress>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Instruction {
FetchPayload { payload_id: ContentAddress, signature: Vec<u8> },
RunPayload { payload_id: ContentAddress, execution: ExecutionSpec },
ReportStatus { status: StatusReport },
NoOp,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub struct ContentAddress(pub [u8; 32]); // SHA256
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionSpec {
pub command: String,
pub args: Vec<String>,
}
/// Authenticated request envelope.
/// Every machine -> server request carries this: the machine signs the
/// canonical body with its Ed25519 private key; the server verifies against
/// the pubkey registered for `machine_id`. This prevents unauth'd clients
/// from polling instructions or fetching payloads.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedRequest {
pub machine_id: String,
/// Canonical JSON bytes of the inner body that was signed.
pub body: Vec<u8>,
/// Ed25519 signature (64 bytes) over `body`.
pub signature: Vec<u8>,
/// Monotonic nonce to prevent replay (e.g. unix millis + random).
pub nonce: u64,
}
+35
View File
@@ -0,0 +1,35 @@
use std::fs::File;
use std::path::Path;
use flate2::{Compression, write::GzEncoder, read::GzDecoder};
use std::fs;
use sha2::{Sha256, Digest};
use crate::proto::ContentAddress;
pub fn create_tar_gz<P: AsRef<Path>>(output: P, files: Vec<(&str, &str)>) -> std::io::Result<ContentAddress> {
use tar::Builder;
let out = File::create(&output)?;
let mut enc = GzEncoder::new(out, Compression::default());
{
let mut builder = Builder::new(&mut enc);
for (path, real_path) in files {
builder.append_path_with_name(real_path, path)?;
}
builder.finish()?;
}
enc.finish()?;
let mut f = File::open(&output)?;
let mut hasher = Sha256::new();
let _ = std::io::copy(&mut f, &mut hasher)?;
let hash: [u8;32] = hasher.finalize().into();
Ok(ContentAddress(hash))
}
pub fn extract_tar_gz<P: AsRef<Path>>(input: P, dest: P) -> std::io::Result<()> {
use tar::Archive;
fs::create_dir_all(&dest)?;
let f = File::open(input)?;
let dec = GzDecoder::new(f);
let mut archive = Archive::new(dec);
archive.unpack(dest)?;
Ok(())
}