use crate::core::error::{Error, Result};
use blake3::Hasher;
use blueprint_std::path::Path;
use tokio::process::Command;
use blueprint_core::{info, warn};
pub struct SecureBinaryInstaller {
expected_hash: String,
gpg_public_key: Option<String>,
download_url: String,
}
impl SecureBinaryInstaller {
pub fn new(download_url: String, expected_hash: String) -> Result<Self> {
if !download_url.starts_with("https://") {
return Err(Error::ConfigurationError(
"Download URL must use HTTPS".into()
));
}
if expected_hash.len() != 64 {
return Err(Error::ConfigurationError(
"Expected hash must be 64-character SHA256".into()
));
}
Ok(Self {
expected_hash,
gpg_public_key: None,
download_url,
})
}
pub fn with_gpg_verification(mut self, public_key: String) -> Self {
self.gpg_public_key = Some(public_key);
self
}
pub async fn install_blueprint_runtime(&self) -> Result<()> {
info!("Starting secure Blueprint runtime installation");
self.create_secure_directories().await?;
let temp_binary = "/tmp/blueprint-runtime-download";
self.secure_download(temp_binary).await?;
self.verify_hash(temp_binary).await?;
if self.gpg_public_key.is_some() {
self.verify_signature(temp_binary).await?;
} else {
warn!("GPG signature verification not configured - supply chain attacks possible");
}
self.install_binary(temp_binary).await?;
self.create_secure_systemd_service().await?;
let _ = tokio::fs::remove_file(temp_binary).await;
info!("Blueprint runtime installed securely");
Ok(())
}
async fn create_secure_directories(&self) -> Result<()> {
let create_dirs = r#"
sudo mkdir -p /opt/blueprint/{bin,config,data,logs}
sudo useradd -r -s /bin/false -d /opt/blueprint blueprint 2>/dev/null || true
sudo chown -R blueprint:blueprint /opt/blueprint
sudo chmod 755 /opt/blueprint
sudo chmod 750 /opt/blueprint/{config,data,logs}
sudo chmod 755 /opt/blueprint/bin
"#;
let output = Command::new("sh")
.arg("-c")
.arg(create_dirs)
.output()
.await
.map_err(|e| Error::ConfigurationError(format!("Directory creation failed: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::ConfigurationError(format!(
"Directory creation failed: {}", stderr
)));
}
Ok(())
}
async fn secure_download(&self, dest_path: &str) -> Result<()> {
let download_cmd = format!(
"curl --fail --location --max-time 300 --max-filesize 104857600 \
--proto =https --tlsv1.2 --ciphers ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS \
--output {} {}",
shell_escape::escape(dest_path.into()),
shell_escape::escape(self.download_url.as_str().into())
);
info!("Downloading Blueprint runtime from: {}", self.download_url);
let output = Command::new("sh")
.arg("-c")
.arg(&download_cmd)
.output()
.await
.map_err(|e| Error::ConfigurationError(format!("Download failed: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::ConfigurationError(format!(
"Download failed: {}", stderr
)));
}
Ok(())
}
async fn verify_hash(&self, file_path: &str) -> Result<()> {
info!("Verifying cryptographic hash");
let file_content = tokio::fs::read(file_path).await
.map_err(|e| Error::ConfigurationError(format!("Cannot read downloaded file: {}", e)))?;
let mut hasher = Hasher::new();
hasher.update(&file_content);
let actual_hash = hasher.finalize();
let actual_hash_hex = hex::encode(actual_hash.as_bytes());
if actual_hash_hex != self.expected_hash {
return Err(Error::ConfigurationError(format!(
"Hash verification failed! Expected: {}, Actual: {}",
self.expected_hash, actual_hash_hex
)));
}
info!("Hash verification successful");
Ok(())
}
async fn verify_signature(&self, file_path: &str) -> Result<()> {
info!("Verifying GPG signature");
let sig_url = format!("{}.sig", self.download_url);
let sig_path = format!("{}.sig", file_path);
let download_sig = format!(
"curl --fail --location --max-time 60 --proto =https --tlsv1.2 --output {} {}",
shell_escape::escape(sig_path.as_str().into()),
shell_escape::escape(sig_url.as_str().into())
);
let output = Command::new("sh")
.arg("-c")
.arg(&download_sig)
.output()
.await
.map_err(|e| Error::ConfigurationError(format!("Signature download failed: {}", e)))?;
if !output.status.success() {
warn!("Signature file not available - proceeding without GPG verification");
return Ok(());
}
let verify_cmd = format!(
"gpg --batch --verify {} {}",
shell_escape::escape(sig_path.as_str().into()),
shell_escape::escape(file_path.into())
);
let output = Command::new("sh")
.arg("-c")
.arg(&verify_cmd)
.output()
.await
.map_err(|e| Error::ConfigurationError(format!("GPG verification failed: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::ConfigurationError(format!(
"GPG signature verification failed: {}", stderr
)));
}
let _ = tokio::fs::remove_file(&sig_path).await;
info!("GPG signature verification successful");
Ok(())
}
async fn install_binary(&self, temp_path: &str) -> Result<()> {
let install_cmd = format!(
"sudo cp {} /opt/blueprint/bin/blueprint-runtime && \
sudo chown root:root /opt/blueprint/bin/blueprint-runtime && \
sudo chmod 755 /opt/blueprint/bin/blueprint-runtime",
shell_escape::escape(temp_path.into())
);
let output = Command::new("sh")
.arg("-c")
.arg(&install_cmd)
.output()
.await
.map_err(|e| Error::ConfigurationError(format!("Binary installation failed: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::ConfigurationError(format!(
"Binary installation failed: {}", stderr
)));
}
Ok(())
}
async fn create_secure_systemd_service(&self) -> Result<()> {
let service_content = r#"[Unit]
Description=Blueprint Runtime
After=network.target
Wants=network.target
[Service]
Type=simple
User=blueprint
Group=blueprint
WorkingDirectory=/opt/blueprint
ExecStart=/opt/blueprint/bin/blueprint-runtime
Restart=always
RestartSec=10
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictSUIDSGID=true
RemoveIPC=true
PrivateTmp=true
PrivateDevices=true
ProtectHostname=true
ProtectClock=true
ProtectKernelLogs=true
ProtectProc=invisible
ProcSubset=pid
RestrictNamespaces=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallFilter=@system-service
SystemCallFilter=~@debug @mount @cpu-emulation @obsolete @privileged @reboot @swap
SystemCallErrorNumber=EPERM
# Resource limits
LimitNOFILE=1024
LimitNPROC=256
TasksMax=256
# Directories
ReadWritePaths=/opt/blueprint/data /opt/blueprint/logs
ReadOnlyPaths=/opt/blueprint/config
[Install]
WantedBy=multi-user.target"#;
let write_service = format!(
"sudo tee /etc/systemd/system/blueprint-runtime.service > /dev/null << 'EOF'\n{}\nEOF",
service_content
);
let output = Command::new("sh")
.arg("-c")
.arg(&write_service)
.output()
.await
.map_err(|e| Error::ConfigurationError(format!("Service creation failed: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::ConfigurationError(format!(
"Service creation failed: {}", stderr
)));
}
let enable_service = "sudo systemctl daemon-reload && sudo systemctl enable blueprint-runtime && sudo systemctl start blueprint-runtime";
let output = Command::new("sh")
.arg("-c")
.arg(enable_service)
.output()
.await
.map_err(|e| Error::ConfigurationError(format!("Service activation failed: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::ConfigurationError(format!(
"Service activation failed: {}", stderr
)));
}
Ok(())
}
pub async fn verify_installation(&self) -> Result<()> {
let status_cmd = "sudo systemctl is-active blueprint-runtime";
let output = Command::new("sh")
.arg("-c")
.arg(status_cmd)
.output()
.await
.map_err(|e| Error::ConfigurationError(format!("Status check failed: {}", e)))?;
let status = String::from_utf8_lossy(&output.stdout).trim().to_string();
if status == "active" {
info!("Blueprint runtime is running successfully");
Ok(())
} else {
Err(Error::ConfigurationError(format!(
"Blueprint runtime is not active: {}", status
)))
}
}
}
impl Default for SecureBinaryInstaller {
fn default() -> Self {
Self {
download_url: "https://github.com/tangle-network/blueprint/releases/latest/download/blueprint-runtime".to_string(),
expected_hash: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), gpg_public_key: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_secure_installer_validation() {
let installer = SecureBinaryInstaller::new(
"https://example.com/binary".to_string(),
"a".repeat(64)
);
assert!(installer.is_ok());
let installer = SecureBinaryInstaller::new(
"http://example.com/binary".to_string(),
"a".repeat(64)
);
assert!(installer.is_err());
let installer = SecureBinaryInstaller::new(
"https://example.com/binary".to_string(),
"short_hash".to_string()
);
assert!(installer.is_err());
}
}