deploy-agent: Rust deployment automation (server + stub + signed payloads)
- 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:
@@ -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"
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod proto;
|
||||
pub mod crypto;
|
||||
pub mod tarball;
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user